mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-10-29 17:52:25 +00:00
[WIFI-13031] Add support for radius endpoints
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
281
package-lock.json
generated
281
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.11.0(37)",
|
||||
"version": "2.11.0(56)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.11.0(37)",
|
||||
"version": "2.11.0(56)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/anatomy": "^2.1.1",
|
||||
@@ -30,6 +30,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"chart.js": "^4.4.0",
|
||||
"country-state-city": "^3.2.0",
|
||||
"cronstrue": "2.26.0",
|
||||
"currency-codes": "^2.1.0",
|
||||
"dagre": "^0.8.5",
|
||||
@@ -132,11 +133,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.21.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
|
||||
"integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==",
|
||||
"version": "7.22.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
|
||||
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.18.6"
|
||||
"@babel/highlight": "^7.22.13",
|
||||
"chalk": "^2.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -182,12 +184,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz",
|
||||
"integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
|
||||
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.21.5",
|
||||
"@babel/types": "^7.23.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jsesc": "^2.5.1"
|
||||
@@ -312,34 +314,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-environment-visitor": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz",
|
||||
"integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
|
||||
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz",
|
||||
"integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
|
||||
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.20.7",
|
||||
"@babel/types": "^7.21.0"
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/types": "^7.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
|
||||
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
|
||||
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -468,29 +470,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
|
||||
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
|
||||
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz",
|
||||
"integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==",
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
|
||||
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
|
||||
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -534,12 +536,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
|
||||
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
|
||||
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"chalk": "^2.0.0",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"chalk": "^2.4.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -547,9 +549,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.21.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz",
|
||||
"integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -1717,33 +1719,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
|
||||
"integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
|
||||
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7"
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/parser": "^7.22.15",
|
||||
"@babel/types": "^7.22.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz",
|
||||
"integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==",
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.21.4",
|
||||
"@babel/generator": "^7.21.5",
|
||||
"@babel/helper-environment-visitor": "^7.21.5",
|
||||
"@babel/helper-function-name": "^7.21.0",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.21.5",
|
||||
"@babel/types": "^7.21.5",
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-function-name": "^7.23.0",
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
@@ -1752,12 +1754,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz",
|
||||
"integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
|
||||
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.21.5",
|
||||
"@babel/helper-validator-identifier": "^7.19.1",
|
||||
"@babel/helper-string-parser": "^7.22.5",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5591,7 +5593,7 @@
|
||||
"node_modules/chalk/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
@@ -5701,7 +5703,7 @@
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
@@ -5819,6 +5821,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/country-state-city": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/country-state-city/-/country-state-city-3.2.1.tgz",
|
||||
"integrity": "sha512-kxbanqMc6izjhc/EHkGPCTabSPZ2G6eG4/97akAYHJUN4stzzFEvQPZoF8oXDQ+10gM/O/yUmISCR1ZVxyb6EA=="
|
||||
},
|
||||
"node_modules/cronstrue": {
|
||||
"version": "2.26.0",
|
||||
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.26.0.tgz",
|
||||
@@ -7664,7 +7671,7 @@
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -9302,9 +9309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
||||
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -11938,11 +11945,12 @@
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.21.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
|
||||
"integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==",
|
||||
"version": "7.22.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
|
||||
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.18.6"
|
||||
"@babel/highlight": "^7.22.13",
|
||||
"chalk": "^2.4.2"
|
||||
}
|
||||
},
|
||||
"@babel/compat-data": {
|
||||
@@ -11975,12 +11983,12 @@
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz",
|
||||
"integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
|
||||
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.21.5",
|
||||
"@babel/types": "^7.23.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jsesc": "^2.5.1"
|
||||
@@ -12077,28 +12085,28 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-environment-visitor": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz",
|
||||
"integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
|
||||
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz",
|
||||
"integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
|
||||
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.20.7",
|
||||
"@babel/types": "^7.21.0"
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/types": "^7.23.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-hoist-variables": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
|
||||
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
|
||||
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.22.5"
|
||||
}
|
||||
},
|
||||
"@babel/helper-member-expression-to-functions": {
|
||||
@@ -12194,23 +12202,23 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-split-export-declaration": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
|
||||
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
|
||||
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.22.5"
|
||||
}
|
||||
},
|
||||
"@babel/helper-string-parser": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz",
|
||||
"integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w=="
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
|
||||
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw=="
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
|
||||
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
"version": "7.21.0",
|
||||
@@ -12242,19 +12250,19 @@
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
|
||||
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
|
||||
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"chalk": "^2.0.0",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"chalk": "^2.4.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.21.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz",
|
||||
"integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||
@@ -13035,41 +13043,41 @@
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
|
||||
"integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
|
||||
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7"
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/parser": "^7.22.15",
|
||||
"@babel/types": "^7.22.15"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz",
|
||||
"integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==",
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.21.4",
|
||||
"@babel/generator": "^7.21.5",
|
||||
"@babel/helper-environment-visitor": "^7.21.5",
|
||||
"@babel/helper-function-name": "^7.21.0",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.21.5",
|
||||
"@babel/types": "^7.21.5",
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-function-name": "^7.23.0",
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz",
|
||||
"integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
|
||||
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.21.5",
|
||||
"@babel/helper-validator-identifier": "^7.19.1",
|
||||
"@babel/helper-string-parser": "^7.22.5",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -15844,7 +15852,7 @@
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15934,7 +15942,7 @@
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
|
||||
},
|
||||
"colorette": {
|
||||
"version": "2.0.20",
|
||||
@@ -16026,6 +16034,11 @@
|
||||
"yaml": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"country-state-city": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/country-state-city/-/country-state-city-3.2.1.tgz",
|
||||
"integrity": "sha512-kxbanqMc6izjhc/EHkGPCTabSPZ2G6eG4/97akAYHJUN4stzzFEvQPZoF8oXDQ+10gM/O/yUmISCR1ZVxyb6EA=="
|
||||
},
|
||||
"cronstrue": {
|
||||
"version": "2.26.0",
|
||||
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.26.0.tgz",
|
||||
@@ -17413,7 +17426,7 @@
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
|
||||
},
|
||||
"has-property-descriptors": {
|
||||
"version": "1.0.0",
|
||||
@@ -18576,9 +18589,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
||||
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"requires": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.11.0(37)",
|
||||
"version": "2.11.0(56)",
|
||||
"description": "",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
@@ -35,6 +35,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"chart.js": "^4.4.0",
|
||||
"country-state-city": "^3.2.0",
|
||||
"cronstrue": "2.26.0",
|
||||
"currency-codes": "^2.1.0",
|
||||
"dagre": "^0.8.5",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"one": "Benachrichtigung",
|
||||
"other": "Benachrichtigungen"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Pool-Strategie",
|
||||
"radius_endpoint_one": "Radiusendpunkt",
|
||||
"radius_endpoint_other": "Radiusendpunkte"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar",
|
||||
"delete_operator": "Betreiber löschen",
|
||||
@@ -971,6 +976,27 @@
|
||||
"title": "Beschränkungen",
|
||||
"tty": "TTY-Zugriff"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "Neues Konto erstellt!",
|
||||
"account_deleted": "Konto gelöscht!",
|
||||
"account_one": "Konto",
|
||||
"account_other": "Konten",
|
||||
"certificate_deleted": "Zertifikat gelöscht!",
|
||||
"certificate_one": "Zertifikat",
|
||||
"certificate_other": "Zertifikate",
|
||||
"city": "Stadt",
|
||||
"common_name": "Gemeinsamen Namen",
|
||||
"country": "Land",
|
||||
"global_reach": "Globale Reichweite",
|
||||
"global_reach_account_id": "Konto-ID",
|
||||
"invalid_certificate": "Ungültiges Zertifikat",
|
||||
"invalid_key": "Ungültiger privater Schlüssel",
|
||||
"location_details_title": "Ort",
|
||||
"organization": "Organisation",
|
||||
"private_key": "Privat Schlüssel",
|
||||
"province": "Provinz",
|
||||
"state": "Zustand"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithmus",
|
||||
"algorithm_other": "Algorithmen",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"one": "Notification",
|
||||
"other": "Notifications"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Pool Strategy",
|
||||
"radius_endpoint_one": "Radius Endpoint",
|
||||
"radius_endpoint_other": "Radius Endpoints"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible",
|
||||
"delete_operator": "Delete Operator",
|
||||
@@ -971,6 +976,27 @@
|
||||
"title": "Restrictions",
|
||||
"tty": "TTY Access"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "New account created!",
|
||||
"account_deleted": "Deleted account!",
|
||||
"account_one": "Account",
|
||||
"account_other": "Accounts",
|
||||
"certificate_deleted": "Deleted certificate!",
|
||||
"certificate_one": "Certificate",
|
||||
"certificate_other": "Certificates",
|
||||
"city": "City",
|
||||
"common_name": "Common Name",
|
||||
"country": "Country",
|
||||
"global_reach": "Global Reach",
|
||||
"global_reach_account_id": " Account ID",
|
||||
"invalid_certificate": "Invalid certificate",
|
||||
"invalid_key": "Invalid private key",
|
||||
"location_details_title": "Location",
|
||||
"organization": "Organization",
|
||||
"private_key": "Private Key",
|
||||
"province": "Province",
|
||||
"state": "State"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithm",
|
||||
"algorithm_other": "Algorithms",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"one": "Notificación",
|
||||
"other": "Notificaciones"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Estrategia de piscina",
|
||||
"radius_endpoint_one": "Punto final del radio",
|
||||
"radius_endpoint_other": "Puntos finales de radio"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.",
|
||||
"delete_operator": "Eliminar operador",
|
||||
@@ -971,6 +976,27 @@
|
||||
"title": "Las restricciones",
|
||||
"tty": "Acceso TTY"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "¡Nueva cuenta creada!",
|
||||
"account_deleted": "¡Cuenta eliminada!",
|
||||
"account_one": "Cuenta",
|
||||
"account_other": "Cuentas",
|
||||
"certificate_deleted": "Certificado eliminado!",
|
||||
"certificate_one": "Certificado",
|
||||
"certificate_other": "Certificados",
|
||||
"city": "ciudad",
|
||||
"common_name": "Nombre común",
|
||||
"country": "País",
|
||||
"global_reach": "Alcance global",
|
||||
"global_reach_account_id": "ID de cuenta ",
|
||||
"invalid_certificate": "Certificado inválido",
|
||||
"invalid_key": "Clave privada no válida",
|
||||
"location_details_title": "Ubicación",
|
||||
"organization": "Organización",
|
||||
"private_key": "Llave privada",
|
||||
"province": "Provincia",
|
||||
"state": "Estado"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algoritmo",
|
||||
"algorithm_other": "Algoritmos",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"one": "Notification",
|
||||
"other": "Les notifications"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Stratégie de pool",
|
||||
"radius_endpoint_one": "Point final de rayon",
|
||||
"radius_endpoint_other": "Points de terminaison du rayon"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible",
|
||||
"delete_operator": "Supprimer l'opérateur",
|
||||
@@ -971,6 +976,27 @@
|
||||
"title": "Restrictions",
|
||||
"tty": "Accès ATS"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "Nouveau compte créé !",
|
||||
"account_deleted": "Compte supprimé !",
|
||||
"account_one": "Compte",
|
||||
"account_other": "Comptes",
|
||||
"certificate_deleted": "Certificat supprimé !",
|
||||
"certificate_one": "Certificat",
|
||||
"certificate_other": "Certificats",
|
||||
"city": "Ville",
|
||||
"common_name": "Nom commun",
|
||||
"country": "Pays",
|
||||
"global_reach": "Portée mondiale",
|
||||
"global_reach_account_id": "ID de compte ",
|
||||
"invalid_certificate": "certificat invalide",
|
||||
"invalid_key": "Clé privée invalide",
|
||||
"location_details_title": "Emplacement",
|
||||
"organization": "Organisation",
|
||||
"private_key": "Clé privée",
|
||||
"province": "province",
|
||||
"state": "Etat"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithme",
|
||||
"algorithm_other": "Algorithmes",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"one": "Notificação",
|
||||
"other": "Notificações"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Estratégia de pool",
|
||||
"radius_endpoint_one": "Ponto final do raio",
|
||||
"radius_endpoint_other": "Pontos finais de raio"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível",
|
||||
"delete_operator": "Excluir operador",
|
||||
@@ -971,6 +976,27 @@
|
||||
"title": "RESTRIÇÕES",
|
||||
"tty": "Acesso TTY"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "Nova conta criada!",
|
||||
"account_deleted": "Conta excluída!",
|
||||
"account_one": "Conta",
|
||||
"account_other": "Contas",
|
||||
"certificate_deleted": "Certificado excluído!",
|
||||
"certificate_one": "Certificado",
|
||||
"certificate_other": "Certificados",
|
||||
"city": "Cidade",
|
||||
"common_name": "Nome comum",
|
||||
"country": "País",
|
||||
"global_reach": "Alcance global",
|
||||
"global_reach_account_id": "ID da conta",
|
||||
"invalid_certificate": "Certificado inválido",
|
||||
"invalid_key": "Chave privada inválida",
|
||||
"location_details_title": "Localização",
|
||||
"organization": "Organização",
|
||||
"private_key": "Chave privada",
|
||||
"province": "província",
|
||||
"state": "Estado"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algoritmo",
|
||||
"algorithm_other": "Algoritmos",
|
||||
|
||||
@@ -40,13 +40,15 @@ export type DataGridOptions<TValue extends object> = {
|
||||
onRowClick?: (row: TValue) => (() => void) | undefined;
|
||||
refetch?: () => void;
|
||||
showAsCard?: boolean;
|
||||
hideTablePreferences?: boolean;
|
||||
};
|
||||
|
||||
export type DataGridProps<TValue extends object> = {
|
||||
innerTableKey?: string | number;
|
||||
controller: UseDataGridReturn;
|
||||
columns: DataGridColumn<TValue>[];
|
||||
header: {
|
||||
title: string;
|
||||
title: string | React.ReactNode;
|
||||
objectListed: string;
|
||||
leftContent?: React.ReactNode;
|
||||
addButton?: React.ReactNode;
|
||||
@@ -58,6 +60,7 @@ export type DataGridProps<TValue extends object> = {
|
||||
};
|
||||
|
||||
export const DataGrid = <TValue extends object>({
|
||||
innerTableKey,
|
||||
controller,
|
||||
columns,
|
||||
header,
|
||||
@@ -149,6 +152,20 @@ export const DataGrid = <TValue extends object>({
|
||||
...tableOptions,
|
||||
});
|
||||
|
||||
// If this is a manual DataTable, with a page index that is higher than 0 and higher than the max possible page, we send to index 0
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
options.isManual &&
|
||||
!isLoading &&
|
||||
data &&
|
||||
pagination.pageIndex > 0 &&
|
||||
options.count !== undefined &&
|
||||
Math.ceil(options.count / pagination.pageSize) - 1 < pagination.pageIndex
|
||||
) {
|
||||
controller.onPaginationChange({ pageIndex: 0, pageSize: pagination.pageSize });
|
||||
}
|
||||
}, [options.count, isLoading, pagination, data]);
|
||||
|
||||
if (isLoading && !options.showAsCard && data.length === 0) {
|
||||
return (
|
||||
<Center>
|
||||
@@ -160,25 +177,29 @@ export const DataGrid = <TValue extends object>({
|
||||
return options.showAsCard ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
{typeof header.title === 'string' ? (
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
) : (
|
||||
header.title
|
||||
)}
|
||||
{header.leftContent}
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
{
|
||||
{options.hideTablePreferences ? null : (
|
||||
// @ts-ignore
|
||||
<TableSettingsModal<TValue> controller={controller} columns={columns} />
|
||||
}
|
||||
)}
|
||||
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody display="flex" flexDirection="column">
|
||||
<LoadingOverlay isLoading={isLoading}>
|
||||
<TableContainer minH={minimumHeight}>
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
@@ -224,7 +245,7 @@ export const DataGrid = <TValue extends object>({
|
||||
</Flex>
|
||||
<LoadingOverlay isLoading={isLoading}>
|
||||
<TableContainer minH={minimumHeight}>
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
|
||||
@@ -23,6 +23,7 @@ interface StringInputProps extends FieldInputProps<string | undefined | string[]
|
||||
isArea: boolean;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||
explanation?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const StringInput: React.FC<StringInputProps> = ({
|
||||
@@ -39,6 +40,7 @@ const StringInput: React.FC<StringInputProps> = ({
|
||||
isDisabled,
|
||||
definitionKey,
|
||||
explanation,
|
||||
placeholder,
|
||||
h,
|
||||
...props
|
||||
}) => {
|
||||
@@ -97,6 +99,7 @@ const StringInput: React.FC<StringInputProps> = ({
|
||||
autoComplete="off"
|
||||
border="2px solid"
|
||||
_disabled={{ opacity: 0.8, cursor: 'not-allowed' }}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{hideButton && (
|
||||
<InputRightElement width="4.5rem">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FieldProps } from 'models/Form';
|
||||
interface StringFieldProps extends FieldProps, LayoutProps {
|
||||
hideButton?: boolean;
|
||||
explanation?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const StringField: React.FC<StringFieldProps> = ({
|
||||
@@ -20,6 +21,7 @@ const StringField: React.FC<StringFieldProps> = ({
|
||||
emptyIsUndefined = false,
|
||||
definitionKey,
|
||||
explanation,
|
||||
placeholder,
|
||||
...props
|
||||
}) => {
|
||||
const { value, error, isError, onChange, onBlur } = useFastField<string | undefined>({ name });
|
||||
@@ -47,6 +49,7 @@ const StringField: React.FC<StringFieldProps> = ({
|
||||
isDisabled={isDisabled}
|
||||
definitionKey={definitionKey}
|
||||
explanation={explanation}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import InterfaceSsidResource from '../Sections/InterfaceSsid';
|
||||
import InterfaceSsidRadiusResource from '../Sections/InterfaceSsidRadius';
|
||||
import InterfaceVlanResource from '../Sections/InterfaceVlan';
|
||||
import InterfaceIpv4Resource from '../Sections/Ipv4';
|
||||
import OpenRoamingSSID from '../Sections/OpenRoamingSsid';
|
||||
import SingleRadioResource from '../Sections/SingleRadio';
|
||||
import InterfaceTunnelResource from '../Sections/Tunnel';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
@@ -22,6 +23,7 @@ import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { useGetRadiusEndpoints } from 'hooks/Network/RadiusEndpoints';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
|
||||
interface Props {
|
||||
@@ -30,12 +32,13 @@ interface Props {
|
||||
isVenue?: boolean;
|
||||
}
|
||||
|
||||
const CreateResourceModal = ({ refresh, entityId, isVenue = false }: Props) => {
|
||||
const CreateResourceModal: React.FC<Props> = ({ refresh, entityId, isVenue = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [selectedVariable, setSelectedVariable] = useState('interface.ssid');
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const { form, formRef } = useFormRef();
|
||||
const getRadiusEndpoints = useGetRadiusEndpoints();
|
||||
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
@@ -48,7 +51,7 @@ const CreateResourceModal = ({ refresh, entityId, isVenue = false }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateButton ml={2} onClick={onOpen} />
|
||||
<CreateButton ml={2} onClick={onOpen} isCompact />
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
@@ -68,16 +71,22 @@ const CreateResourceModal = ({ refresh, entityId, isVenue = false }: Props) => {
|
||||
<ModalBody>
|
||||
<FormControl isRequired mb={4}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal">
|
||||
{t('resources.variable')}
|
||||
Configuration Section
|
||||
</FormLabel>
|
||||
<Select value={selectedVariable} onChange={onVariableChange} borderRadius="15px" fontSize="sm" w="200px">
|
||||
<option value="interface.captive">interface.captive</option>
|
||||
<option value="interface.ipv4">interface.ipv4</option>
|
||||
<option value="radio">radio</option>
|
||||
<option value="interface.ssid">interface.ssid</option>
|
||||
<option
|
||||
value="interface.ssid.openroaming"
|
||||
hidden={!getRadiusEndpoints.data || getRadiusEndpoints.data.length === 0}
|
||||
>
|
||||
Open Roaming SSID
|
||||
</option>
|
||||
<option value="interface.ssid.radius">interface.ssid.radius</option>
|
||||
<option value="interface.tunnel">interface.tunnel</option>
|
||||
<option value="interface.vlan">interface.vlan</option>
|
||||
<option value="radio">radio</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedVariable === 'interface.captive' && (
|
||||
@@ -100,6 +109,17 @@ const CreateResourceModal = ({ refresh, entityId, isVenue = false }: Props) => {
|
||||
parent={{ entity: isVenue ? undefined : entityId, venue: isVenue ? entityId : undefined }}
|
||||
/>
|
||||
)}
|
||||
{selectedVariable === 'interface.ssid.openroaming' && getRadiusEndpoints.data && (
|
||||
<OpenRoamingSSID
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
refresh={refresh}
|
||||
isDisabled={false}
|
||||
formRef={formRef}
|
||||
parent={{ entity: isVenue ? undefined : entityId, venue: isVenue ? entityId : undefined }}
|
||||
radiusEndpoints={getRadiusEndpoints.data}
|
||||
/>
|
||||
)}
|
||||
{selectedVariable === 'interface.tunnel' && (
|
||||
<InterfaceTunnelResource
|
||||
isOpen={isOpen}
|
||||
@@ -120,16 +140,6 @@ const CreateResourceModal = ({ refresh, entityId, isVenue = false }: Props) => {
|
||||
parent={{ entity: isVenue ? undefined : entityId, venue: isVenue ? entityId : undefined }}
|
||||
/>
|
||||
)}
|
||||
{selectedVariable === 'interface.ipv4' && (
|
||||
<InterfaceIpv4Resource
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
refresh={refresh}
|
||||
isDisabled={false}
|
||||
formRef={formRef}
|
||||
parent={{ entity: isVenue ? undefined : entityId, venue: isVenue ? entityId : undefined }}
|
||||
/>
|
||||
)}
|
||||
{selectedVariable === 'interface.ssid' && (
|
||||
<InterfaceSsidResource
|
||||
isOpen={isOpen}
|
||||
@@ -150,6 +160,16 @@ const CreateResourceModal = ({ refresh, entityId, isVenue = false }: Props) => {
|
||||
isDisabled={false}
|
||||
/>
|
||||
)}
|
||||
{selectedVariable === 'interface.ipv4' && (
|
||||
<InterfaceIpv4Resource
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
refresh={refresh}
|
||||
formRef={formRef}
|
||||
parent={{ entity: isVenue ? undefined : entityId, venue: isVenue ? entityId : undefined }}
|
||||
isDisabled={false}
|
||||
/>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
<ConfirmCloseAlert isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||
|
||||
@@ -15,6 +15,7 @@ import InterfaceSsidResource from '../Sections/InterfaceSsid';
|
||||
import InterfaceSsidRadiusResource from '../Sections/InterfaceSsidRadius';
|
||||
import InterfaceVlanResource from '../Sections/InterfaceVlan';
|
||||
import InterfaceIpv4Resource from '../Sections/Ipv4';
|
||||
import OpenRoamingSSID from '../Sections/OpenRoamingSsid';
|
||||
import SingleRadioResource from '../Sections/SingleRadio';
|
||||
import InterfaceTunnelResource from '../Sections/Tunnel';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
@@ -22,6 +23,7 @@ import EditButton from 'components/Buttons/EditButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { useGetRadiusEndpoints } from 'hooks/Network/RadiusEndpoints';
|
||||
import { useGetResource } from 'hooks/Network/Resources';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { Resource } from 'models/Resource';
|
||||
@@ -33,7 +35,7 @@ interface Props {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
const EditResourceModal: React.FC<Props> = ({ isOpen, onClose, resource, refresh }) => {
|
||||
const { t } = useTranslation();
|
||||
const [editing, setEditing] = useBoolean();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
@@ -46,7 +48,7 @@ const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
id: resource?.id ?? '',
|
||||
enabled: resource?.id !== '' && isOpen,
|
||||
});
|
||||
|
||||
const getRadiusEndpoints = useGetRadiusEndpoints();
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
@@ -75,7 +77,9 @@ const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
</Center>
|
||||
);
|
||||
|
||||
if (getType() === 'interface.captive')
|
||||
const resourceType = getType();
|
||||
|
||||
if (resourceType === 'interface.captive')
|
||||
return (
|
||||
<InterfaceCaptiveResource
|
||||
resource={resourceData}
|
||||
@@ -87,7 +91,7 @@ const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
/>
|
||||
);
|
||||
|
||||
if (getType() === 'interface.ssid.radius')
|
||||
if (resourceType === 'interface.ssid.radius')
|
||||
return (
|
||||
<InterfaceSsidRadiusResource
|
||||
resource={resourceData}
|
||||
@@ -98,7 +102,7 @@ const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
);
|
||||
if (getType() === 'interface.tunnel')
|
||||
if (resourceType === 'interface.tunnel')
|
||||
return (
|
||||
<InterfaceTunnelResource
|
||||
resource={resourceData}
|
||||
@@ -110,19 +114,20 @@ const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
/>
|
||||
);
|
||||
|
||||
if (getType() === 'interface.ipv4')
|
||||
if (resourceType === 'interface.ssid.openroaming')
|
||||
return (
|
||||
<InterfaceIpv4Resource
|
||||
<OpenRoamingSSID
|
||||
resource={resourceData}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
refresh={refreshAll}
|
||||
formRef={formRef}
|
||||
isDisabled={!editing}
|
||||
radiusEndpoints={getRadiusEndpoints.data ?? []}
|
||||
/>
|
||||
);
|
||||
|
||||
if (getType() === 'interface.vlan')
|
||||
if (resourceType === 'interface.vlan')
|
||||
return (
|
||||
<InterfaceVlanResource
|
||||
resource={resourceData}
|
||||
@@ -134,7 +139,7 @@ const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
/>
|
||||
);
|
||||
|
||||
if (getType() === 'interface.ssid')
|
||||
if (resourceType === 'interface.ssid')
|
||||
return (
|
||||
<InterfaceSsidResource
|
||||
resource={resourceData}
|
||||
@@ -145,8 +150,19 @@ const EditResourceModal = ({ isOpen, onClose, resource, refresh }: Props) => {
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
);
|
||||
if (resourceType === 'interface.ipv4')
|
||||
return (
|
||||
<InterfaceIpv4Resource
|
||||
resource={resourceData}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
refresh={refreshAll}
|
||||
formRef={formRef}
|
||||
isDisabled={!editing}
|
||||
/>
|
||||
);
|
||||
|
||||
if (getType() === 'radio')
|
||||
if (resourceType === 'radio')
|
||||
return (
|
||||
<SingleRadioResource
|
||||
resource={resourceData}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SimpleGrid, Tab, TabList, TabPanel, TabPanels, Tabs, useToast } from '@chakra-ui/react';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -9,6 +8,7 @@ import InterfaceSsidForm from './Form';
|
||||
import NotesTable from 'components/CustomFields/NotesTable';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import { useCreateResource, useUpdateResource } from 'hooks/Network/Resources';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { Note } from 'models/Note';
|
||||
import { Resource } from 'models/Resource';
|
||||
import { INTERFACE_SSID_SCHEMA } from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants';
|
||||
@@ -34,7 +34,15 @@ interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
const InterfaceSsidResource = ({ isOpen, onClose, refresh, formRef, resource, isDisabled = false, parent }: Props) => {
|
||||
const InterfaceSsidResource: React.FC<Props> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
refresh,
|
||||
formRef,
|
||||
resource,
|
||||
isDisabled = false,
|
||||
parent,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [formKey, setFormKey] = useState(uuid());
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/* eslint-disable max-len */
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Heading, SimpleGrid } from '@chakra-ui/react';
|
||||
import { getIn, useFormikContext } from 'formik';
|
||||
import OpenRoamingEncryption from './OpenRoamingEncryption';
|
||||
import MultiSelectField from 'components/FormFields/MultiSelectField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import AdvancedSettings from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/AdvancedSettings';
|
||||
import PassPoint from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/SingleInterface/SsidList/PassPoint';
|
||||
|
||||
const namePrefix = 'editing';
|
||||
|
||||
const InterfaceSsidResourceForm = ({
|
||||
isDisabled,
|
||||
radiusEndpoints,
|
||||
}: {
|
||||
isDisabled: boolean;
|
||||
radiusEndpoints: RadiusEndpoint[];
|
||||
}) => {
|
||||
const { values } = useFormikContext<{
|
||||
editing?: {
|
||||
radius?: {
|
||||
__radiusEndpoint: string;
|
||||
};
|
||||
};
|
||||
}>();
|
||||
|
||||
const foundRadiusEndpoint = React.useMemo(
|
||||
() => radiusEndpoints.find(({ id }) => id === values?.editing?.radius?.__radiusEndpoint),
|
||||
[values?.editing?.radius?.__radiusEndpoint],
|
||||
);
|
||||
|
||||
const isPasspoint = useMemo(
|
||||
// @ts-ignore
|
||||
() => values !== undefined && values['pass-point'] !== undefined && values['pass-point'] !== null,
|
||||
[getIn(values, `${namePrefix}`)],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading size="md" mt={6} mb={2} textDecoration="underline">
|
||||
OpenRoaming SSID
|
||||
</Heading>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mt={2}>
|
||||
<StringField
|
||||
name={`${namePrefix}.name`}
|
||||
label="name"
|
||||
definitionKey="interface.ssid.name"
|
||||
isDisabled={isDisabled}
|
||||
isRequired
|
||||
/>
|
||||
<SelectField
|
||||
name={`${namePrefix}.bss-mode`}
|
||||
label="bss-mode"
|
||||
definitionKey="interface.ssid.bss-mode"
|
||||
isDisabled={isDisabled}
|
||||
options={[
|
||||
{ value: 'ap', label: 'ap' },
|
||||
{ value: 'sta', label: 'sta' },
|
||||
{ value: 'mesh', label: 'mesh' },
|
||||
{ value: 'wds-ap', label: 'wds-ap' },
|
||||
{ value: 'wds-sta', label: 'wds-sta' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
<MultiSelectField
|
||||
name={`${namePrefix}.wifi-bands`}
|
||||
label="wifi-bands"
|
||||
definitionKey="interface.ssid.wifi-bands"
|
||||
isDisabled={isDisabled}
|
||||
options={[
|
||||
{ value: '2G', label: '2G' },
|
||||
{ value: '5G', label: '5G' },
|
||||
{ value: '6G', label: '6G' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<OpenRoamingEncryption
|
||||
editing={!isDisabled}
|
||||
ssidName={namePrefix}
|
||||
namePrefix={`${namePrefix}.encryption`}
|
||||
radiusPrefix={`${namePrefix}.radius`}
|
||||
isPasspoint={isPasspoint}
|
||||
/>
|
||||
<Box my={2}>
|
||||
<PassPoint
|
||||
isDisabled={isDisabled}
|
||||
namePrefix={`${namePrefix}.pass-point`}
|
||||
radiusPrefix={`${namePrefix}.radius`}
|
||||
lockConsortium={['orion', 'globalreach'].includes(foundRadiusEndpoint?.Type ?? '')}
|
||||
/>
|
||||
</Box>
|
||||
<AdvancedSettings editing={!isDisabled} namePrefix={namePrefix} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InterfaceSsidResourceForm);
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { Flex, Heading, SimpleGrid } from '@chakra-ui/react';
|
||||
import OpenRoamingRadius from './Radius';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ToggleField from 'components/FormFields/ToggleField';
|
||||
import {
|
||||
ENCRYPTION_OPTIONS,
|
||||
ENCRYPTION_PROTOS_REQUIRE_RADIUS,
|
||||
} from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants';
|
||||
|
||||
interface Props {
|
||||
editing: boolean;
|
||||
namePrefix: string;
|
||||
radiusPrefix: string;
|
||||
onProtoChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
needIeee: boolean;
|
||||
isKeyNeeded: boolean;
|
||||
isPasspoint?: boolean;
|
||||
}
|
||||
|
||||
const OpenRoamingEncryptionForm = ({
|
||||
editing,
|
||||
namePrefix,
|
||||
radiusPrefix,
|
||||
onProtoChange,
|
||||
needIeee,
|
||||
isKeyNeeded,
|
||||
isPasspoint,
|
||||
}: Props) => (
|
||||
<>
|
||||
<Flex mt={4}>
|
||||
<Heading size="md" borderBottom="1px solid">
|
||||
Authentication
|
||||
</Heading>
|
||||
</Flex>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<SelectField
|
||||
name={`${namePrefix}.proto`}
|
||||
label="protocol"
|
||||
definitionKey="interface.ssid.encryption.proto"
|
||||
options={ENCRYPTION_OPTIONS.filter(({ value }) => ENCRYPTION_PROTOS_REQUIRE_RADIUS.includes(value))}
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
onChange={onProtoChange}
|
||||
w="300px"
|
||||
/>
|
||||
{isKeyNeeded && (
|
||||
<StringField
|
||||
name={`${namePrefix}.key`}
|
||||
label="key"
|
||||
definitionKey="interface.ssid.encryption.key"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
hideButton
|
||||
/>
|
||||
)}
|
||||
{needIeee && (
|
||||
<SelectField
|
||||
name={`${namePrefix}.ieee80211w`}
|
||||
label="ieee80211w"
|
||||
definitionKey="interface.ssid.encryption.ieee80211w"
|
||||
options={[
|
||||
{ value: 'disabled', label: 'disabled' },
|
||||
{ value: 'optional', label: 'optional' },
|
||||
{ value: 'required', label: 'required' },
|
||||
]}
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
w="120px"
|
||||
/>
|
||||
)}
|
||||
<ToggleField
|
||||
name={`${namePrefix}.key-caching`}
|
||||
label="key-caching"
|
||||
definitionKey="interface.ssid.encryption.key-caching"
|
||||
isDisabled={!editing}
|
||||
defaultValue
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<OpenRoamingRadius editing={editing} namePrefix={radiusPrefix} isPasspoint={isPasspoint} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default React.memo(OpenRoamingEncryptionForm);
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { Flex, FormControl, FormLabel, Heading, SimpleGrid, Switch } from '@chakra-ui/react';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ToggleField from 'components/FormFields/ToggleField';
|
||||
|
||||
type Props = {
|
||||
editing: boolean;
|
||||
namePrefix: string;
|
||||
onAccountingChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
isAccountingEnabled: boolean;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
isPasspoint?: boolean;
|
||||
};
|
||||
const OpenRoamingRadiusForm = ({ editing, namePrefix, onAccountingChange, isAccountingEnabled }: Props) => (
|
||||
<>
|
||||
<Flex mt={6}>
|
||||
<div>
|
||||
<Heading size="md" display="flex" mt={2} mr={2} borderBottom="1px solid">
|
||||
Radius
|
||||
</Heading>
|
||||
</div>
|
||||
</Flex>
|
||||
<FormControl isDisabled={!editing}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal">
|
||||
Enable Accounting
|
||||
</FormLabel>
|
||||
<Switch
|
||||
onChange={onAccountingChange}
|
||||
isChecked={isAccountingEnabled}
|
||||
borderRadius="15px"
|
||||
size="lg"
|
||||
isDisabled={!editing}
|
||||
_disabled={{ opacity: 0.8, cursor: 'not-allowed' }}
|
||||
/>
|
||||
</FormControl>
|
||||
{isAccountingEnabled && (
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<StringField name={`${namePrefix}.accounting.host`} label="accounting.host" isDisabled={!editing} isRequired />
|
||||
<NumberField
|
||||
name={`${namePrefix}.accounting.port`}
|
||||
label="accounting.port"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
hideArrows
|
||||
w={24}
|
||||
/>
|
||||
<StringField
|
||||
name={`${namePrefix}.accounting.secret`}
|
||||
label="accounting.secret"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
hideButton
|
||||
/>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<StringField
|
||||
name={`${namePrefix}.nas-identifier`}
|
||||
label="nas-identifier"
|
||||
isDisabled={!editing}
|
||||
emptyIsUndefined
|
||||
/>
|
||||
<ToggleField
|
||||
name={`${namePrefix}.chargeable-user-id`}
|
||||
label="chargeable-user-id"
|
||||
isDisabled={!editing}
|
||||
falseIsUndefined
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
|
||||
export default React.memo(OpenRoamingRadiusForm);
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import RadiusForm from './Radius';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = { editing: boolean; namePrefix: string; isPasspoint?: boolean };
|
||||
|
||||
const OpenRoamingRadius = ({ editing, namePrefix, isPasspoint }: Props) => {
|
||||
const { value: accounting, onChange: setAccounting } = useFastField({ name: `${namePrefix}.accounting` });
|
||||
|
||||
const onEnabledAccountingChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setAccounting({
|
||||
host: '192.168.178.192',
|
||||
port: 1813,
|
||||
secret: 'YOUR_SECRET',
|
||||
});
|
||||
} else {
|
||||
setAccounting(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const isAccountingEnabled = useMemo(() => accounting !== undefined, [accounting !== undefined]);
|
||||
|
||||
return (
|
||||
<RadiusForm
|
||||
editing={editing}
|
||||
onAccountingChange={onEnabledAccountingChange}
|
||||
isAccountingEnabled={isAccountingEnabled}
|
||||
namePrefix={namePrefix}
|
||||
isPasspoint={isPasspoint}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(OpenRoamingRadius);
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import OpenRoamingEncryptionForm from './Encryption';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
import {
|
||||
ENCRYPTION_PROTOS_REQUIRE_IEEE,
|
||||
ENCRYPTION_PROTOS_REQUIRE_KEY,
|
||||
NO_MULTI_PROTOS,
|
||||
} from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants';
|
||||
|
||||
const OpenRoamingEncryption = ({
|
||||
editing,
|
||||
ssidName,
|
||||
namePrefix,
|
||||
radiusPrefix,
|
||||
isPasspoint,
|
||||
}: {
|
||||
editing: boolean;
|
||||
namePrefix: string;
|
||||
radiusPrefix: string;
|
||||
ssidName: string;
|
||||
isPasspoint?: boolean;
|
||||
}) => {
|
||||
const { value: encryptionValue, onChange: onEncryptionChange } = useFastField({ name: namePrefix });
|
||||
const { value: radiusValue, onChange: onRadiusChange } = useFastField({ name: radiusPrefix });
|
||||
const { onChange: onMultiPskChange } = useFastField({ name: `${ssidName}.multi-psk` });
|
||||
|
||||
const onProtoChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newEncryption: { proto: string; key?: string; ieee80211w?: string } = {
|
||||
proto: e.target.value,
|
||||
};
|
||||
if (e.target.value === 'none') {
|
||||
onEncryptionChange({ proto: 'none' });
|
||||
onRadiusChange(undefined);
|
||||
} else {
|
||||
if (NO_MULTI_PROTOS.includes(e.target.value)) onMultiPskChange(undefined);
|
||||
if (ENCRYPTION_PROTOS_REQUIRE_KEY.includes(e.target.value)) newEncryption.key = 'YOUR_SECRET';
|
||||
if (ENCRYPTION_PROTOS_REQUIRE_IEEE.includes(e.target.value)) newEncryption.ieee80211w = 'required';
|
||||
onEncryptionChange(newEncryption);
|
||||
}
|
||||
},
|
||||
[isPasspoint],
|
||||
);
|
||||
|
||||
const { isKeyNeeded, needIeee } = useMemo(
|
||||
() => ({
|
||||
isKeyNeeded: ENCRYPTION_PROTOS_REQUIRE_KEY.includes(encryptionValue?.proto ?? ''),
|
||||
needIeee: ENCRYPTION_PROTOS_REQUIRE_IEEE.includes(encryptionValue?.proto ?? ''),
|
||||
}),
|
||||
[encryptionValue?.proto, radiusValue !== undefined],
|
||||
);
|
||||
|
||||
return (
|
||||
<OpenRoamingEncryptionForm
|
||||
editing={editing}
|
||||
namePrefix={namePrefix}
|
||||
radiusPrefix={radiusPrefix}
|
||||
onProtoChange={onProtoChange}
|
||||
needIeee={needIeee}
|
||||
isKeyNeeded={isKeyNeeded}
|
||||
isPasspoint={isPasspoint}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(OpenRoamingEncryption);
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import { useGetRadiusEndpoints } from 'hooks/Network/RadiusEndpoints';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
const CONSORTIUMS = {
|
||||
orion: ['F4F5E8F5F4'],
|
||||
globalreach: ['5A03BA0000'],
|
||||
generic: [],
|
||||
radsec: [],
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const RadiusEndpointSelector = ({ name, isDisabled }: Props) => {
|
||||
const getEndpoints = useGetRadiusEndpoints();
|
||||
const field = useFastField<string>({ name });
|
||||
const consortiumField = useFastField({ name: 'editing.pass-point.roaming-consortium' });
|
||||
|
||||
const options =
|
||||
getEndpoints.data?.map((endpoint) => ({
|
||||
value: endpoint.id,
|
||||
label: endpoint.name,
|
||||
})) ?? [];
|
||||
|
||||
React.useEffect(() => {
|
||||
const found = getEndpoints.data?.find(({ id }) => id === field.value);
|
||||
|
||||
if (found) {
|
||||
const newValue = CONSORTIUMS[found.Type];
|
||||
|
||||
consortiumField.onChange(newValue);
|
||||
}
|
||||
}, [field.value]);
|
||||
|
||||
return (
|
||||
<SelectField
|
||||
name={name}
|
||||
label="Radius Endpoint"
|
||||
options={options}
|
||||
isDisabled={isDisabled || options.length === 0}
|
||||
isRequired
|
||||
w="max-content"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadiusEndpointSelector;
|
||||
@@ -0,0 +1,290 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Heading, Tab, TabList, TabPanel, TabPanels, Tabs, useToast } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { object, string } from 'yup';
|
||||
import InterfaceSsidForm from './Form';
|
||||
import RadiusEndpointSelector from './RadiusEndpointSelector';
|
||||
import NotesTable from 'components/CustomFields/NotesTable';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import { useCreateResource, useUpdateResource } from 'hooks/Network/Resources';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { Note } from 'models/Note';
|
||||
import { Resource } from 'models/Resource';
|
||||
import { INTERFACE_SSID_SCHEMA } from 'pages/ConfigurationPage/ConfigurationCard/ConfigurationSectionsCard/InterfaceSection/interfacesConstants';
|
||||
|
||||
const CONSORTIUMS = {
|
||||
orion: ['F4F5E8F5F4'],
|
||||
globalreach: ['5A03BA0000'],
|
||||
generic: [],
|
||||
radsec: [],
|
||||
} as const;
|
||||
|
||||
const DEFAULT_VALUE = (defaultEndpoint?: RadiusEndpoint) => ({
|
||||
'bss-mode': 'ap',
|
||||
'dtim-period': 2,
|
||||
encryption: {
|
||||
ieee80211w: 'disabled',
|
||||
proto: 'wpa-mixed',
|
||||
},
|
||||
radius: {
|
||||
__radiusEndpoint: defaultEndpoint?.id ?? '',
|
||||
'chargeable-user-id': true,
|
||||
accounting: {
|
||||
host: 'example.com',
|
||||
port: '1813',
|
||||
secret: 'Secret',
|
||||
},
|
||||
},
|
||||
'pass-point': {
|
||||
'venue-group': 1,
|
||||
'venue-type': 1,
|
||||
'auth-type': {
|
||||
type: 'terms-and-conditions',
|
||||
},
|
||||
'anqp-domain': 8888,
|
||||
'access-network-type': 0,
|
||||
internet: true,
|
||||
'wan-metrics': {
|
||||
type: 'up',
|
||||
downlink: 20000,
|
||||
uplink: 20000,
|
||||
},
|
||||
'roaming-consortium': CONSORTIUMS[defaultEndpoint?.Type ?? 'generic'],
|
||||
'domain-name': ['main.example.com'],
|
||||
'friendly-name': ['eng:ExampleWifi', 'fra:ExempleWifi'],
|
||||
'venue-name': ['eng:Example Inc 1', 'fra:Exemple Inc 1'],
|
||||
'venue-url': ['http://www.example.com/info-fra', 'http://www.example.com/info-eng'],
|
||||
},
|
||||
'fils-discovery-interval': 20,
|
||||
'hidden-ssid': false,
|
||||
'isolate-clients': false,
|
||||
'maximum-clients': 64,
|
||||
name: 'OpenRoaming',
|
||||
services: ['radius-gw-proxy', 'wifi-steering'],
|
||||
'tip-information-element': true,
|
||||
'wifi-bands': ['2G', '5G'],
|
||||
});
|
||||
|
||||
export const EDIT_SCHEMA = (t: (str: string) => string) =>
|
||||
object().shape({
|
||||
_unused_name: string().required(t('form.required')).default(''),
|
||||
_unused_description: string().default(''),
|
||||
editing: INTERFACE_SSID_SCHEMA(t),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
refresh: () => void;
|
||||
formRef: React.Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
resource?: Resource;
|
||||
isDisabled?: boolean;
|
||||
parent?: {
|
||||
entity?: string;
|
||||
venue?: string;
|
||||
subscriber?: string;
|
||||
};
|
||||
radiusEndpoints: RadiusEndpoint[];
|
||||
}
|
||||
|
||||
const OpenRoamingSSID = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
refresh,
|
||||
formRef,
|
||||
resource,
|
||||
isDisabled = false,
|
||||
parent,
|
||||
radiusEndpoints,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [formKey, setFormKey] = useState(uuid());
|
||||
|
||||
const create = useCreateResource();
|
||||
const update = useUpdateResource(resource?.id ?? '');
|
||||
|
||||
useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef}
|
||||
key={formKey}
|
||||
initialValues={
|
||||
resource !== undefined && resource.variables[0]
|
||||
? {
|
||||
editing: { ...JSON.parse(resource.variables[0].value) },
|
||||
_unused_name: resource.name,
|
||||
_unused_description: resource.description,
|
||||
entity: resource.entity !== '' ? `ent:${resource.entity}` : `ven:${resource.venue}`,
|
||||
_unused_notes: resource.notes,
|
||||
}
|
||||
: {
|
||||
editing: DEFAULT_VALUE(radiusEndpoints[0] as RadiusEndpoint),
|
||||
_unused_name: 'OpenRoaming SSID',
|
||||
_unused_description: '',
|
||||
_unused_notes: [],
|
||||
}
|
||||
}
|
||||
validationSchema={EDIT_SCHEMA(t)}
|
||||
onSubmit={async (formData, { setSubmitting, resetForm }) => {
|
||||
const after = (success: boolean) => {
|
||||
if (success) {
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
refresh();
|
||||
onClose();
|
||||
} else {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return resource
|
||||
? update.mutateAsync(
|
||||
{
|
||||
variables: [
|
||||
{
|
||||
type: 'json',
|
||||
weight: 0,
|
||||
prefix: 'interface.ssid.openroaming',
|
||||
value: {
|
||||
// @ts-ignore
|
||||
...formData.editing,
|
||||
_unused_name: undefined,
|
||||
_unused_description: undefined,
|
||||
_unused_notes: undefined,
|
||||
entity: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
name: formData._unused_name,
|
||||
description: formData._unused_description,
|
||||
// @ts-ignore
|
||||
notes: formData._unused_notes.filter((note: Note) => note.isNew),
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast({
|
||||
id: 'resource-update-success',
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_update_obj', {
|
||||
obj: t('resources.configuration_resource'),
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
after(true);
|
||||
},
|
||||
// @ts-ignore
|
||||
onError: (e: AxiosError) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_update_obj', {
|
||||
obj: t('resources.configuration_resource'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
after(false);
|
||||
},
|
||||
},
|
||||
)
|
||||
: create.mutateAsync(
|
||||
{
|
||||
variables: [
|
||||
{
|
||||
type: 'json',
|
||||
weight: 0,
|
||||
prefix: 'interface.ssid.openroaming',
|
||||
value: {
|
||||
// @ts-ignore
|
||||
...formData.editing,
|
||||
_unused_name: undefined,
|
||||
_unused_description: undefined,
|
||||
_unused_notes: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
...parent,
|
||||
name: formData._unused_name,
|
||||
description: formData._unused_description,
|
||||
// @ts-ignore
|
||||
notes: formData._unused_notes.filter((note: Note) => note.isNew),
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast({
|
||||
id: 'user-creation-success',
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_create_obj', {
|
||||
obj: t('resources.configuration_resource'),
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
after(true);
|
||||
},
|
||||
// @ts-ignore
|
||||
onError: (e: AxiosError) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_create_obj', {
|
||||
obj: t('resources.configuration_resource'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
after(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Tabs variant="enclosed">
|
||||
<TabList>
|
||||
<Tab>{t('common.main')}</Tab>
|
||||
<Tab>{t('common.notes')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Heading size="md" mb={2} textDecoration="underline">
|
||||
Resource Details
|
||||
</Heading>
|
||||
<RadiusEndpointSelector name="editing.radius.__radiusEndpoint" isDisabled={isDisabled} />
|
||||
<StringField name="_unused_name" label={t('common.name')} isRequired isDisabled={isDisabled} w="300px" />
|
||||
<StringField
|
||||
name="_unused_description"
|
||||
label={t('common.description')}
|
||||
isDisabled={isDisabled}
|
||||
isArea
|
||||
h="40px"
|
||||
/>
|
||||
<InterfaceSsidForm radiusEndpoints={radiusEndpoints} isDisabled={isDisabled} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<NotesTable name="_unused_notes" isDisabled={isDisabled} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenRoamingSSID;
|
||||
@@ -10,6 +10,7 @@ import MultiSelectField from 'components/FormFields/MultiSelectField';
|
||||
import SelectWithSearchField from 'components/FormFields/SelectWithSearchField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import { CreateConfigurationSchema } from 'constants/formSchemas';
|
||||
import { ConfigurationProvider } from 'contexts/ConfigurationProvider';
|
||||
import { useGetEntities } from 'hooks/Network/Entity';
|
||||
import { useGetVenues } from 'hooks/Network/Venues';
|
||||
|
||||
@@ -170,7 +171,9 @@ const CreateConfigurationForm = ({
|
||||
<StringField name="description" label={t('common.description')} errors={errors} touched={touched} />
|
||||
<StringField name="note" label={t('common.note')} errors={errors} touched={touched} />
|
||||
</SimpleGrid>
|
||||
<SpecialConfigurationManager editing isEnabledByDefault isOnlySections onChange={onConfigurationChange} />
|
||||
<ConfigurationProvider entityId={getEntityId()}>
|
||||
<SpecialConfigurationManager editing isEnabledByDefault isOnlySections onChange={onConfigurationChange} />
|
||||
</ConfigurationProvider>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
@@ -69,16 +69,23 @@ export const testLength = ({ val, min, max }: { val?: string; min: number; max:
|
||||
return false;
|
||||
};
|
||||
|
||||
export const testPemCertificate = (val?: string) => {
|
||||
export const testPemCertificate = (val?: string, nonStrict?: boolean) => {
|
||||
if (val) {
|
||||
if (nonStrict) {
|
||||
return val.includes('---BEGIN') && val.includes('---END');
|
||||
}
|
||||
return val.includes('---BEGIN CERTIFICATE---') && val.includes('---END CERTIFICATE---');
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const testPemPrivateKey = (val?: string) => {
|
||||
export const testPemPrivateKey = (val?: string, nonStrict?: boolean) => {
|
||||
if (val) {
|
||||
if (nonStrict) {
|
||||
return val.includes('---BEGIN') && val.includes('---END');
|
||||
}
|
||||
|
||||
return val.includes('---BEGIN PRIVATE KEY---') && val.includes('---END PRIVATE KEY---');
|
||||
}
|
||||
|
||||
@@ -225,7 +232,6 @@ export const isValidPortRanges = (first: string, second: string) => {
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export type TestSelectPortsProps = { ports: string[]; vlan?: number }[];
|
||||
|
||||
export const testSelectPorts = (obj: TestSelectPortsProps) => {
|
||||
@@ -262,4 +268,9 @@ export const testSelectPorts = (obj: TestSelectPortsProps) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const testObjectName = (str?: string) => (str ? str.length <= 50 : false);
|
||||
export const testObjectName = (str?: string) => (str ? str.length <= 30 : false);
|
||||
|
||||
export const isValidEmailAddress = (email: string) =>
|
||||
email.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||
);
|
||||
|
||||
58
src/contexts/ConfigurationProvider/index.tsx
Normal file
58
src/contexts/ConfigurationProvider/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useGetConfiguration } from 'hooks/Network/Configurations';
|
||||
import { useGetEntity } from 'hooks/Network/Entity';
|
||||
import { useGetResources } from 'hooks/Network/Resources';
|
||||
import { useGetVenue } from 'hooks/Network/Venues';
|
||||
import { Resource } from 'models/Resource';
|
||||
|
||||
const ConfigurationContext = React.createContext<{
|
||||
configurationId: string;
|
||||
availableResources?: Resource[];
|
||||
}>({
|
||||
configurationId: '',
|
||||
});
|
||||
|
||||
export const ConfigurationProvider = ({
|
||||
children,
|
||||
configurationId,
|
||||
entityId,
|
||||
}: {
|
||||
children: React.ReactElement;
|
||||
configurationId?: string;
|
||||
entityId?: string;
|
||||
}) => {
|
||||
const getConfig = useGetConfiguration({ id: configurationId, onSuccess: () => {} });
|
||||
const venueId = () => {
|
||||
const split = entityId?.split(':');
|
||||
if (split?.[0] && split?.[1] && split[0] === 'ven') {
|
||||
return split[1];
|
||||
}
|
||||
return getConfig.data?.venue;
|
||||
};
|
||||
const finalEntityId = () => {
|
||||
const split = entityId?.split(':');
|
||||
if (split?.[0] && split?.[1] && split[0] === 'ent') {
|
||||
return split[1];
|
||||
}
|
||||
return getConfig.data?.entity;
|
||||
};
|
||||
|
||||
const getVenue = useGetVenue({ id: venueId() });
|
||||
const getEntity = useGetEntity({ id: finalEntityId() });
|
||||
const getResources = useGetResources({
|
||||
pageInfo: null,
|
||||
select: getVenue.data?.variables ?? getEntity.data?.variables,
|
||||
});
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
configurationId,
|
||||
availableResources: getResources.data,
|
||||
}),
|
||||
[getResources.data],
|
||||
);
|
||||
|
||||
return <ConfigurationContext.Provider value={value}>{children}</ConfigurationContext.Provider>;
|
||||
};
|
||||
|
||||
export const useConfigurationContext = () => React.useContext(ConfigurationContext);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useDefaultPage from 'hooks/useDefaultPage';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
export const useGetConfigurations = () => {
|
||||
@@ -92,7 +92,7 @@ export const useGetAvailableConfigurations = ({ tagId }: { tagId: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetConfiguration = ({ id = null, onSuccess = () => {} }) => {
|
||||
export const useGetConfiguration = ({ id, onSuccess = () => {} }: { id?: string | null; onSuccess?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const goToDefaultPage = useDefaultPage();
|
||||
@@ -101,7 +101,9 @@ export const useGetConfiguration = ({ id = null, onSuccess = () => {} }) => {
|
||||
['get-configuration', id],
|
||||
() => axiosProv.get(`configuration/${id}?withExtendedInfo=true`).then(({ data }) => data),
|
||||
{
|
||||
enabled: id !== null && id !== '',
|
||||
enabled: id !== undefined && id !== null && id !== '',
|
||||
keepPreviousData: true,
|
||||
staleTime: 10 * 1000,
|
||||
onSuccess,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('configuration-fetching-error'))
|
||||
|
||||
251
src/hooks/Network/GlobalReach.ts
Normal file
251
src/hooks/Network/GlobalReach.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Note } from 'models/Note';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
export type GlobalReachAccount = {
|
||||
id: string;
|
||||
created: number;
|
||||
modified: number;
|
||||
name: string;
|
||||
description: string;
|
||||
notes: Note[];
|
||||
privateKey: string;
|
||||
country: string;
|
||||
province: string;
|
||||
city: string;
|
||||
organization: string;
|
||||
commonName: string;
|
||||
CSR: string;
|
||||
GlobalReachAcctId: string;
|
||||
};
|
||||
|
||||
const ACCOUNT_QUERY_KEY_PREFIX = 'globalReach';
|
||||
const ACCOUNT_PATH = '/openroaming/globalreach/account';
|
||||
const ACCOUNTS_PATH = '/openroaming/globalreach/accounts';
|
||||
|
||||
const getGlobalReachAccounts = async (limit: number, offset: number) =>
|
||||
axiosProv.get(`${ACCOUNTS_PATH}?limit=${limit}&offset=${offset}`).then(({ data }) => data as GlobalReachAccount[]);
|
||||
|
||||
const getAllGlobalReachAccounts = async () => {
|
||||
const batchSize = 500;
|
||||
let offset = 0;
|
||||
let accounts: GlobalReachAccount[] = [];
|
||||
let lastBatchSize = 0;
|
||||
|
||||
do {
|
||||
const batch = await getGlobalReachAccounts(batchSize, offset);
|
||||
lastBatchSize = batch.length;
|
||||
accounts = accounts.concat(batch);
|
||||
offset += batchSize;
|
||||
} while (lastBatchSize === batchSize);
|
||||
|
||||
return accounts;
|
||||
};
|
||||
|
||||
export const useGetGlobalReachAccounts = () =>
|
||||
useQuery({
|
||||
queryKey: [ACCOUNT_QUERY_KEY_PREFIX, 'all'],
|
||||
queryFn: getAllGlobalReachAccounts,
|
||||
staleTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const getGlobalReachAccount = async (context: QueryFunctionContext<[string, { id: string }]>) =>
|
||||
axiosProv.get(`${ACCOUNT_PATH}/${context.queryKey[1].id}`).then(({ data }) => data as GlobalReachAccount);
|
||||
|
||||
export const useGetGlobalReachAccount = (id: string) =>
|
||||
useQuery({
|
||||
queryKey: [ACCOUNT_QUERY_KEY_PREFIX, { id }],
|
||||
queryFn: getGlobalReachAccount,
|
||||
staleTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
export type CreateGlobalReachAccountRequest = {
|
||||
name: string;
|
||||
description?: string;
|
||||
notes?: Note[];
|
||||
privateKey: string;
|
||||
country: string;
|
||||
province: string;
|
||||
city: string;
|
||||
organization: string;
|
||||
commonName: string;
|
||||
GlobalReachAcctId: string;
|
||||
};
|
||||
const createGlobalReachAccount = async (req: CreateGlobalReachAccountRequest) =>
|
||||
axiosProv.post(`${ACCOUNT_PATH}/0`, req).then(({ data }) => data as GlobalReachAccount);
|
||||
|
||||
export const useCreateGlobalReachAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createGlobalReachAccount,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([ACCOUNT_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type UpdateGlobalReachAccountRequest = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
notes?: Note[];
|
||||
privateKey?: string;
|
||||
country?: string;
|
||||
province?: string;
|
||||
city?: string;
|
||||
organization?: string;
|
||||
commonName?: string;
|
||||
};
|
||||
|
||||
const updateGlobalReachAccount = async (req: UpdateGlobalReachAccountRequest) =>
|
||||
axiosProv.put(`${ACCOUNT_PATH}/${req.id}`, req).then(({ data }) => data as GlobalReachAccount);
|
||||
|
||||
export const useUpdateGlobalReachAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateGlobalReachAccount,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([ACCOUNT_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteGlobalReachAccount = async (id: string) => axiosProv.delete(`${ACCOUNT_PATH}/${id}`);
|
||||
|
||||
export const useDeleteGlobalReachAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: deleteGlobalReachAccount,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([ACCOUNT_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type GlobalReachCertificate = {
|
||||
id: string;
|
||||
name: string;
|
||||
accountId: string;
|
||||
csr: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
certificateId: string;
|
||||
expiresAt: number;
|
||||
created: number;
|
||||
};
|
||||
|
||||
const CERTIFICATE_QUERY_KEY_PREFIX = 'globalReachCertificate';
|
||||
const CERTIFICATE_PATH = '/openroaming/globalreach/certificate';
|
||||
const CERTIFICATES_PATH = '/openroaming/globalreach/certificates';
|
||||
|
||||
const getGlobalReachCertificatesBatch = async (accountId: string, limit: number, offset: number) =>
|
||||
axiosProv
|
||||
.get(`${CERTIFICATES_PATH}/${accountId}?limit=${limit}&offset=${offset}`)
|
||||
.then(({ data }) => data as GlobalReachCertificate[]);
|
||||
|
||||
const getGlobalReachCertificates = async (context: QueryFunctionContext<[string, { accountId: string }]>) => {
|
||||
const { accountId } = context.queryKey[1];
|
||||
const batchSize = 500;
|
||||
let offset = 0;
|
||||
let certificates: GlobalReachCertificate[] = [];
|
||||
let lastBatchSize = 0;
|
||||
|
||||
do {
|
||||
const batch = await getGlobalReachCertificatesBatch(accountId, batchSize, offset);
|
||||
lastBatchSize = batch.length;
|
||||
certificates = certificates.concat(batch);
|
||||
offset += batchSize;
|
||||
} while (lastBatchSize === batchSize);
|
||||
|
||||
return certificates;
|
||||
};
|
||||
|
||||
export const useGetGlobalReachCertificates = (accountId: string) =>
|
||||
useQuery({
|
||||
queryKey: [CERTIFICATE_QUERY_KEY_PREFIX, { accountId }],
|
||||
queryFn: getGlobalReachCertificates,
|
||||
staleTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const getSelectedGlobalReachCertificates = async (context: QueryFunctionContext<[string, { certIds: string[] }]>) =>
|
||||
axiosProv
|
||||
.get(`${CERTIFICATES_PATH}/*?select=${context.queryKey[1].certIds.join(',')}`)
|
||||
.then(({ data }) => data as GlobalReachCertificate[]);
|
||||
|
||||
export const useGetSelectedGlobalReachCertificates = ({ certIds }: { certIds: string[] }) =>
|
||||
useQuery({
|
||||
queryKey: [CERTIFICATE_QUERY_KEY_PREFIX, { certIds }],
|
||||
queryFn: getSelectedGlobalReachCertificates,
|
||||
staleTime: 1000 * 60,
|
||||
enabled: certIds.length > 0,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const getGlobalReachCertificate = async (context: QueryFunctionContext<[string, { accountId: string; id: string }]>) =>
|
||||
axiosProv
|
||||
.get(`${CERTIFICATE_PATH}/${context.queryKey[1].accountId}/${context.queryKey[1].id}`)
|
||||
.then(({ data }) => data as GlobalReachCertificate);
|
||||
|
||||
export const useGetGlobalReachCertificate = (accountId: string, id: string) =>
|
||||
useQuery({
|
||||
queryKey: [CERTIFICATE_QUERY_KEY_PREFIX, { accountId, id }],
|
||||
queryFn: getGlobalReachCertificate,
|
||||
staleTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
export type CreateGlobalReachCertificateRequest = {
|
||||
accountId: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const createGlobalReachCertificate = async (req: CreateGlobalReachCertificateRequest) =>
|
||||
axiosProv.post(`${CERTIFICATE_PATH}/${req.accountId}/0`, req).then(({ data }) => data as GlobalReachCertificate);
|
||||
|
||||
export const useCreateGlobalReachCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createGlobalReachCertificate,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([CERTIFICATE_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renewGlobalReachCertificate = async ({ accountId, id }: { accountId: string; id: string }) =>
|
||||
axiosProv
|
||||
.put(`${CERTIFICATE_PATH}/${accountId}/${id}?updateCertificate=true`, {})
|
||||
.then(({ data }) => data as GlobalReachCertificate);
|
||||
|
||||
export const useRenewGlobalReachCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: renewGlobalReachCertificate,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([CERTIFICATE_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteGlobalReachCertificate = async ({ accountId, id }: { accountId: string; id: string }) =>
|
||||
axiosProv.delete(`${CERTIFICATE_PATH}/${accountId}/${id}`);
|
||||
|
||||
export const useDeleteGlobalReachCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: deleteGlobalReachCertificate,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([CERTIFICATE_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
118
src/hooks/Network/GoogleOrion.ts
Normal file
118
src/hooks/Network/GoogleOrion.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Note } from 'models/Note';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
export type GoogleOrionAccount = {
|
||||
name: string;
|
||||
description: string;
|
||||
notes: Note[];
|
||||
created: number;
|
||||
modified: number;
|
||||
id: string;
|
||||
privateKey: string;
|
||||
certificate: string;
|
||||
cacerts: string[];
|
||||
};
|
||||
|
||||
const ACCOUNT_QUERY_KEY_PREFIX = 'googleOrion';
|
||||
const ACCOUNT_PATH = '/openroaming/orion/account';
|
||||
const ACCOUNTS_PATH = '/openroaming/orion/accounts';
|
||||
|
||||
const getGoogleOrionAccounts = async (limit: number, offset: number) =>
|
||||
axiosProv.get(`${ACCOUNTS_PATH}?limit=${limit}&offset=${offset}`).then(({ data }) => data as GoogleOrionAccount[]);
|
||||
|
||||
const getAllGoogleOrionAccounts = async () => {
|
||||
const batchSize = 500;
|
||||
let offset = 0;
|
||||
let accounts: GoogleOrionAccount[] = [];
|
||||
let lastBatchSize = 0;
|
||||
|
||||
do {
|
||||
const batch = await getGoogleOrionAccounts(batchSize, offset);
|
||||
lastBatchSize = batch.length;
|
||||
accounts = accounts.concat(batch);
|
||||
offset += batchSize;
|
||||
} while (lastBatchSize === batchSize);
|
||||
|
||||
return accounts;
|
||||
};
|
||||
|
||||
export const useGetGoogleOrionAccounts = () =>
|
||||
useQuery({
|
||||
queryKey: [ACCOUNT_QUERY_KEY_PREFIX, 'all'],
|
||||
queryFn: getAllGoogleOrionAccounts,
|
||||
staleTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const getGoogleOrionAccount = async (context: QueryFunctionContext<[string, { id: string }]>) =>
|
||||
axiosProv.get(`${ACCOUNT_PATH}/${context.queryKey[1].id}`).then(({ data }) => data as GoogleOrionAccount);
|
||||
|
||||
export const useGetGoogleOrionAccount = (id: string) =>
|
||||
useQuery({
|
||||
queryKey: [ACCOUNT_QUERY_KEY_PREFIX, { id }],
|
||||
queryFn: getGoogleOrionAccount,
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
|
||||
export type CreateGoogleOrionAccountRequest = {
|
||||
name: string;
|
||||
description?: string;
|
||||
notes?: Note[];
|
||||
privateKey: string;
|
||||
certificate: string;
|
||||
cacerts: string[];
|
||||
};
|
||||
|
||||
const createGoogleOrionAccount = async (request: CreateGoogleOrionAccountRequest) =>
|
||||
axiosProv.post(`${ACCOUNT_PATH}/0`, request).then(({ data }) => data as GoogleOrionAccount);
|
||||
|
||||
export const useCreateGoogleOrionAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createGoogleOrionAccount,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([ACCOUNT_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type UpdateGoogleOrionAccountRequest = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
notes?: Note[];
|
||||
};
|
||||
|
||||
const updateGoogleOrionAccount = async (request: UpdateGoogleOrionAccountRequest) =>
|
||||
axiosProv
|
||||
.put(`${ACCOUNT_PATH}/${request.id}`, {
|
||||
name: request.name,
|
||||
description: request.description,
|
||||
notes: request.notes,
|
||||
})
|
||||
.then(({ data }) => data as GoogleOrionAccount);
|
||||
|
||||
export const useUpdateGoogleOrionAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateGoogleOrionAccount,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([ACCOUNT_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteGoogleOrionAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => axiosProv.delete(`${ACCOUNT_PATH}/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([ACCOUNT_QUERY_KEY_PREFIX]);
|
||||
},
|
||||
});
|
||||
};
|
||||
200
src/hooks/Network/RadiusEndpoints.ts
Normal file
200
src/hooks/Network/RadiusEndpoints.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AtLeast } from 'models/General';
|
||||
import { Note } from 'models/Note';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
export type RadiusServer = {
|
||||
Hostname: string;
|
||||
IP: string;
|
||||
Port: number;
|
||||
Secret: number;
|
||||
};
|
||||
|
||||
export type RadiusEndpointServer = {
|
||||
Authentication: RadiusServer[];
|
||||
Accounting: RadiusServer[];
|
||||
CoA: RadiusServer[];
|
||||
AccountingInterval: number;
|
||||
};
|
||||
|
||||
export type RadsecServer = {
|
||||
/**
|
||||
* It should be the ID of a google orion account OR the certificate ID of the global reach account
|
||||
* If not empty, only Weight needs to be populated
|
||||
* If empty, all fields need to be populated
|
||||
*/
|
||||
UseOpenRoamingAccount: string;
|
||||
Weight: number;
|
||||
Hostname: string;
|
||||
IP: string;
|
||||
Port: string;
|
||||
/** Default: radsec */
|
||||
Secret: string;
|
||||
Certificate: string;
|
||||
PrivateKey: string;
|
||||
CaCerts: string[];
|
||||
/** Default: false */
|
||||
AllowSelfSigned: boolean;
|
||||
};
|
||||
|
||||
export const RADIUS_ENDPOINT_TYPES = ['generic', 'radsec', 'globalreach', 'orion'] as const;
|
||||
export type RadiusEndpointType = (typeof RADIUS_ENDPOINT_TYPES)[number];
|
||||
|
||||
export const RADIUS_ENDPOINT_POOL_STRATEGIES = ['round_robin', 'weighted', 'random'] as const;
|
||||
export type RadiusEndpointPoolStrategy = (typeof RADIUS_ENDPOINT_POOL_STRATEGIES)[number];
|
||||
|
||||
export type RadiusEndpoint = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
notes: Note[];
|
||||
created: number;
|
||||
modified: number;
|
||||
Type: RadiusEndpointType;
|
||||
PoolStrategy: RadiusEndpointPoolStrategy;
|
||||
/**
|
||||
* If Type is radius, we need at least one entry
|
||||
* Else, it should be empty
|
||||
*/
|
||||
RadiusServers: RadiusEndpointServer[];
|
||||
/**
|
||||
* If Type is radsec, orion or globalreach, we need at least one entry
|
||||
* Else, it should be empty
|
||||
*/
|
||||
RadsecServers: RadsecServer[];
|
||||
/** Default: true */
|
||||
UseGWProxy: boolean;
|
||||
/**
|
||||
* An IP address that should be between 0.0.1.1 and 0.0.2.254
|
||||
*/
|
||||
Index: string;
|
||||
/**
|
||||
* The ids of all configurations using this endpoint
|
||||
*/
|
||||
UsedBy: string[];
|
||||
NasIdentifier: string;
|
||||
AccountingInterval: number;
|
||||
};
|
||||
|
||||
const SINGLE_PATH = '/RADIUSEndPoint';
|
||||
const COLLECTION_PATH = '/RADIUSEndPoints';
|
||||
|
||||
const QUERY_KEY = 'radius-endpoints';
|
||||
|
||||
const getEndpoints = (limit: number, offset: number) =>
|
||||
axiosProv.get(`${COLLECTION_PATH}?limit=${limit}&offset=${offset}`).then((res) => res.data as RadiusEndpoint[]);
|
||||
|
||||
const getAllEndpoints = async () => {
|
||||
const limit = 100;
|
||||
let offset = 0;
|
||||
const endpoints: RadiusEndpoint[] = [];
|
||||
let lastResponse = [] as RadiusEndpoint[];
|
||||
|
||||
do {
|
||||
lastResponse = await getEndpoints(limit, offset);
|
||||
endpoints.push(...lastResponse);
|
||||
offset += limit;
|
||||
} while (lastResponse.length === limit);
|
||||
|
||||
return endpoints;
|
||||
};
|
||||
|
||||
export const useGetRadiusEndpoints = () =>
|
||||
useQuery({
|
||||
queryKey: [QUERY_KEY, 'all'],
|
||||
queryFn: getAllEndpoints,
|
||||
keepPreviousData: true,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const getEndpoint = (
|
||||
context: QueryFunctionContext<
|
||||
[
|
||||
string,
|
||||
{
|
||||
id?: string;
|
||||
},
|
||||
]
|
||||
>,
|
||||
) => axiosProv.get(`${SINGLE_PATH}/${context.queryKey[1].id}`).then((res) => res.data as RadiusEndpoint);
|
||||
|
||||
export const useGetRadiusEndpoint = ({ id }: { id?: string }) =>
|
||||
useQuery({
|
||||
queryKey: [QUERY_KEY, { id }],
|
||||
queryFn: getEndpoint,
|
||||
keepPreviousData: true,
|
||||
staleTime: 60 * 1000,
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const deleteEndpoint = async (id: string) => axiosProv.delete(`${SINGLE_PATH}/${id}`);
|
||||
|
||||
export const useDeleteRadiusEndpoint = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: deleteEndpoint,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateEndpoint = async (endpoint: AtLeast<RadiusEndpoint, 'id'>) =>
|
||||
axiosProv.put(`${SINGLE_PATH}/${endpoint.id}`, endpoint);
|
||||
|
||||
export const useUpdateRadiusEndpoint = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateEndpoint,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createEndpoint = async (endpoint: Omit<RadiusEndpoint, 'id' | 'UsedBy' | 'created' | 'modified'>) =>
|
||||
axiosProv.post(`${SINGLE_PATH}/0`, endpoint);
|
||||
|
||||
export const useCreateRadiusEndpoint = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createEndpoint,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getLastTimeUpdatedOnGateway = async () =>
|
||||
axiosProv.get(`${COLLECTION_PATH}?currentStatus=true`).then(
|
||||
(res) =>
|
||||
res.data as {
|
||||
lastUpdate: number;
|
||||
lastConfigurationChange: number;
|
||||
},
|
||||
);
|
||||
|
||||
export const useGetRadiusEndpointLastGwUpdate = () =>
|
||||
useQuery({
|
||||
queryKey: [QUERY_KEY, { lastUpdatedOnly: true }],
|
||||
queryFn: getLastTimeUpdatedOnGateway,
|
||||
keepPreviousData: true,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const updateEndpointOnGateway = async () => axiosProv.put(`${COLLECTION_PATH}?updateEndpoints=true`);
|
||||
|
||||
export const useUpdateRadiusEndpointsOnGateway = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateEndpointOnGateway,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY]);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { Resource } from 'models/Resource';
|
||||
import { PageInfo } from 'models/Table';
|
||||
import { VariableBlock } from 'models/VariableBlock';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
export const useGetResourcesCount = () => {
|
||||
@@ -78,7 +78,7 @@ export const useGetResources = ({
|
||||
select,
|
||||
count,
|
||||
}: {
|
||||
pageInfo?: PageInfo;
|
||||
pageInfo?: PageInfo | null;
|
||||
select?: string[];
|
||||
count?: number;
|
||||
}) => {
|
||||
@@ -92,7 +92,7 @@ export const useGetResources = ({
|
||||
select.length > 0
|
||||
? axiosProv
|
||||
.get(`variable?withExtendedInfo=true&select=${select}`)
|
||||
.then(({ data }: { data: { variableBlocks: VariableBlock[] } }) => data.variableBlocks)
|
||||
.then(({ data }: { data: { variableBlocks: Resource[] } }) => data.variableBlocks)
|
||||
: [],
|
||||
{
|
||||
onError: (e: AxiosError) => {
|
||||
@@ -123,7 +123,7 @@ export const useGetResources = ({
|
||||
(pageInfo?.limit ?? 10) * (pageInfo?.index ?? 1)
|
||||
}`,
|
||||
)
|
||||
.then(({ data }) => data.variableBlocks),
|
||||
.then(({ data }) => data.variableBlocks as Resource[]),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: pageInfo !== null,
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
interface Props {
|
||||
type UseFormModalProps = {
|
||||
isDirty?: boolean;
|
||||
onModalClose?: () => void;
|
||||
}
|
||||
const useFormModal = ({ isDirty, onModalClose }: Props) => {
|
||||
onCloseSideEffect?: () => void;
|
||||
};
|
||||
|
||||
const useFormModal = ({ isDirty, onModalClose, onCloseSideEffect }: UseFormModalProps) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: isConfirmOpen, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
|
||||
const closeModal = () => {
|
||||
if (isDirty) openConfirm();
|
||||
else if (onModalClose) onModalClose();
|
||||
else onClose();
|
||||
else {
|
||||
onClose();
|
||||
if (onCloseSideEffect) onCloseSideEffect();
|
||||
}
|
||||
};
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
closeConfirm();
|
||||
if (onModalClose) onModalClose();
|
||||
else onClose();
|
||||
else {
|
||||
onClose();
|
||||
if (onCloseSideEffect) onCloseSideEffect();
|
||||
}
|
||||
};
|
||||
|
||||
const toReturn = useMemo(
|
||||
|
||||
80
src/hooks/useNotification.ts
Normal file
80
src/hooks/useNotification.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as React from 'react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { isApiError } from 'models/Axios';
|
||||
|
||||
export type SuccessNotificationProps = {
|
||||
description: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type ApiErrorNotificationProps = {
|
||||
e: unknown;
|
||||
fallbackMessage?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const useNotification = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
const successToast = ({ description, id }: SuccessNotificationProps) => {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.success'),
|
||||
description,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
};
|
||||
|
||||
const apiErrorToast = ({ e, id, fallbackMessage }: ApiErrorNotificationProps) => {
|
||||
if (isApiError(e)) {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.error'),
|
||||
description: e.response?.data.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.error'),
|
||||
description: fallbackMessage,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const errorToast = ({ description, id }: SuccessNotificationProps) => {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.error'),
|
||||
description,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
};
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
successToast,
|
||||
errorToast,
|
||||
apiErrorToast,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
};
|
||||
|
||||
export type UseNotificationReturn = ReturnType<typeof useNotification>;
|
||||
@@ -40,13 +40,14 @@ export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
whiteSpace="normal"
|
||||
>
|
||||
<Flex alignItems="center" w="100%">
|
||||
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={activeTextColor} fontSize="md" fontWeight="bold">
|
||||
{t(route.name)}
|
||||
<Text color={activeTextColor} fontSize="md" fontWeight="bold" textAlign="left">
|
||||
{route.label ?? t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
@@ -64,13 +65,14 @@ export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
whiteSpace="normal"
|
||||
>
|
||||
<Flex alignItems="center" w="100%">
|
||||
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={inactiveTextColor} fontSize="md" fontWeight="bold">
|
||||
{t(route.name)}
|
||||
<Text color={inactiveTextColor} fontSize="md" fontWeight="bold" textAlign="left">
|
||||
{route.label ?? t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
|
||||
@@ -29,8 +29,9 @@ const SubNavigationButton = ({ isActive, route }: Props) => {
|
||||
bg: hoverBg,
|
||||
}}
|
||||
border="none"
|
||||
textAlign="left"
|
||||
>
|
||||
{t(route.name)}
|
||||
{route.label ?? t(route.name)}
|
||||
</Button>
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
@@ -89,7 +89,7 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
||||
</Box>
|
||||
</>
|
||||
),
|
||||
[user?.userRole, location, topNav],
|
||||
[user?.userRole, location, topNav, routes],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -57,7 +57,9 @@ const Layout = () => {
|
||||
name = location.pathname.split('/')[location.pathname.split('/').length - 1] ?? '';
|
||||
}
|
||||
|
||||
if (name.includes('RAW-')) name.replace('RAW-', '');
|
||||
if (name.includes('RAW-')) {
|
||||
return name.replace('RAW-', '');
|
||||
}
|
||||
|
||||
return t(name);
|
||||
}, [t, location.pathname]);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AxiosError as Err } from 'axios';
|
||||
import { AxiosError as Err, isAxiosError } from 'axios';
|
||||
|
||||
export type AxiosError = Err<{ ErrorDescription: string; ErrorCode: number }>;
|
||||
export const isApiError = (e: unknown): e is AxiosError =>
|
||||
isAxiosError(e) && (e as AxiosError).response?.data?.ErrorDescription !== undefined;
|
||||
|
||||
@@ -7,7 +7,8 @@ export type SubRoute = {
|
||||
authorized: string[];
|
||||
path: string;
|
||||
name: RouteName;
|
||||
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
label?: string;
|
||||
component: typeof React.Component | React.LazyExoticComponent<() => JSX.Element | null>;
|
||||
navName?: RouteName;
|
||||
hidden?: boolean;
|
||||
icon?: undefined;
|
||||
@@ -21,6 +22,7 @@ export type RouteGroup = {
|
||||
id: string;
|
||||
authorized: string[];
|
||||
name: RouteName;
|
||||
label?: string;
|
||||
icon: (active: boolean) => React.ReactElement;
|
||||
children: SubRoute[];
|
||||
hidden?: boolean;
|
||||
@@ -36,15 +38,16 @@ export type SingleRoute = {
|
||||
authorized: string[];
|
||||
path: string;
|
||||
name: RouteName;
|
||||
label?: string;
|
||||
navName?: RouteName;
|
||||
icon: (active: boolean) => React.ReactElement;
|
||||
navButton?: (
|
||||
isActive: boolean,
|
||||
toggleSidebar: () => void,
|
||||
route: Route,
|
||||
) => React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
) => typeof React.Component | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
isEntity?: boolean;
|
||||
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
component: typeof React.Component | React.LazyExoticComponent<() => JSX.Element | null>;
|
||||
hidden?: boolean;
|
||||
isCustom?: boolean;
|
||||
children?: undefined;
|
||||
|
||||
@@ -16,6 +16,7 @@ interface Props {
|
||||
isUsingRadius: boolean;
|
||||
isPasspoint?: boolean;
|
||||
canUseRadius: boolean;
|
||||
acceptedEncryptionProtos?: string[];
|
||||
}
|
||||
|
||||
const EncryptionForm = ({
|
||||
@@ -28,6 +29,7 @@ const EncryptionForm = ({
|
||||
isUsingRadius,
|
||||
isPasspoint,
|
||||
canUseRadius,
|
||||
acceptedEncryptionProtos,
|
||||
}: Props) => (
|
||||
<>
|
||||
<Flex mt={4}>
|
||||
@@ -40,7 +42,11 @@ const EncryptionForm = ({
|
||||
name={`${namePrefix}.proto`}
|
||||
label="protocol"
|
||||
definitionKey="interface.ssid.encryption.proto"
|
||||
options={ENCRYPTION_OPTIONS}
|
||||
options={
|
||||
acceptedEncryptionProtos
|
||||
? ENCRYPTION_OPTIONS.filter(({ value }) => acceptedEncryptionProtos.includes(value))
|
||||
: ENCRYPTION_OPTIONS
|
||||
}
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
onChange={onProtoChange}
|
||||
|
||||
@@ -17,12 +17,14 @@ const Encryption = ({
|
||||
namePrefix,
|
||||
radiusPrefix,
|
||||
isPasspoint,
|
||||
acceptedEncryptionProtos,
|
||||
}: {
|
||||
editing: boolean;
|
||||
namePrefix: string;
|
||||
radiusPrefix: string;
|
||||
ssidName: string;
|
||||
isPasspoint?: boolean;
|
||||
acceptedEncryptionProtos?: string[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { value: encryptionValue, onChange: onEncryptionChange } = useFastField({ name: namePrefix });
|
||||
@@ -43,12 +45,6 @@ const Encryption = ({
|
||||
if (ENCRYPTION_PROTOS_REQUIRE_IEEE.includes(e.target.value)) newEncryption.ieee80211w = 'required';
|
||||
onEncryptionChange(newEncryption);
|
||||
if (ENCRYPTION_PROTOS_REQUIRE_RADIUS.includes(e.target.value)) {
|
||||
/*
|
||||
if (isPasspoint) {
|
||||
onRadiusChange(DEFAULT_PASSPOINT_RADIUS);
|
||||
} else {
|
||||
onRadiusChange(INTERFACE_SSID_RADIUS_SCHEMA(t, true).cast());
|
||||
} */
|
||||
onRadiusChange(INTERFACE_SSID_RADIUS_SCHEMA(t, true).cast());
|
||||
} else {
|
||||
onRadiusChange(undefined);
|
||||
@@ -80,6 +76,7 @@ const Encryption = ({
|
||||
isUsingRadius={isUsingRadius}
|
||||
isPasspoint={isPasspoint}
|
||||
canUseRadius={canUseRadius}
|
||||
acceptedEncryptionProtos={acceptedEncryptionProtos}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,9 +12,10 @@ import StringField from 'components/FormFields/StringField';
|
||||
|
||||
const propTypes = {
|
||||
data: PropTypes.instanceOf(Object).isRequired,
|
||||
isUsingRadiusEndpoint: PropTypes.bool,
|
||||
};
|
||||
|
||||
const LockedEncryption = ({ data }) => {
|
||||
const LockedEncryption = ({ data, isUsingRadiusEndpoint }) => {
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
@@ -73,7 +74,7 @@ const LockedEncryption = ({ data }) => {
|
||||
Radius
|
||||
</Heading>
|
||||
</Flex>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" hidden={isUsingRadiusEndpoint}>
|
||||
<DisplayStringField
|
||||
value={data?.radius?.authentication?.host}
|
||||
label="authentication.host"
|
||||
@@ -97,65 +98,69 @@ const LockedEncryption = ({ data }) => {
|
||||
/>
|
||||
<DisplayToggleField value={data?.radius?.authentication?.['mac-filter']} isDisabled />
|
||||
</SimpleGrid>
|
||||
<FormControl isDisabled>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal">
|
||||
Accounting
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
{data?.radius?.accounting && (
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<DisplayStringField
|
||||
value={data?.radius?.accounting?.host}
|
||||
label="accounting.host"
|
||||
isDisabled
|
||||
isRequired
|
||||
/>
|
||||
<DisplayNumberField
|
||||
value={data?.radius?.accounting?.port}
|
||||
label="accounting.port"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideArrows
|
||||
w={24}
|
||||
/>
|
||||
<DisplayStringField
|
||||
value={data?.radius?.accounting?.secret}
|
||||
label="accounting.secret"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideButton
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<>
|
||||
<FormControl>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal">
|
||||
Accounting
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<DisplayStringField
|
||||
value={data?.radius?.accounting?.host}
|
||||
label="accounting.host"
|
||||
isDisabled
|
||||
isRequired
|
||||
/>
|
||||
<DisplayNumberField
|
||||
value={data?.radius?.accounting?.port}
|
||||
label="accounting.port"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideArrows
|
||||
w={24}
|
||||
/>
|
||||
<DisplayStringField
|
||||
value={data?.radius?.accounting?.secret}
|
||||
label="accounting.secret"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideButton
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
<FormControl isDisabled>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal">
|
||||
Accounting
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
{data?.radius?.['dynamic-authorization'] && (
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<DisplayStringField
|
||||
value={data?.radius?.['dynamic-authorization']?.host}
|
||||
label="dynamic-authorization.host"
|
||||
isDisabled
|
||||
isRequired
|
||||
/>
|
||||
<DisplayNumberField
|
||||
value={data?.radius?.['dynamic-authorization']?.port}
|
||||
label="dynamic-authorization.port"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideArrows
|
||||
w={24}
|
||||
/>
|
||||
<DisplayStringField
|
||||
value={data?.radius?.['dynamic-authorization']?.secret}
|
||||
label="dynamic-authorization.secret"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideButton
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<>
|
||||
<FormControl isDisabled hidden={isUsingRadiusEndpoint}>
|
||||
<FormLabel ms="4px" fontSize="md" fontWeight="normal">
|
||||
Dynamic Authorization
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<DisplayStringField
|
||||
value={data?.radius?.['dynamic-authorization']?.host}
|
||||
label="dynamic-authorization.host"
|
||||
isDisabled
|
||||
isRequired
|
||||
/>
|
||||
<DisplayNumberField
|
||||
value={data?.radius?.['dynamic-authorization']?.port}
|
||||
label="dynamic-authorization.port"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideArrows
|
||||
w={24}
|
||||
/>
|
||||
<DisplayStringField
|
||||
value={data?.radius?.['dynamic-authorization']?.secret}
|
||||
label="dynamic-authorization.secret"
|
||||
isDisabled
|
||||
isRequired
|
||||
hideButton
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<DisplayStringField value={data?.radius?.['nas-identifier']} label="nas-identifier" isDisabled />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Flex, Heading, Image, NumberInputField, SimpleGrid } from '@chakra-ui/react';
|
||||
import { Flex, Heading, Image, SimpleGrid } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { INTERFACE_PASSPOINT_ICONS_SCHEMA } from '../../interfacesConstants';
|
||||
import DisplayNumberField from 'components/DisplayFields/DisplayNumberField';
|
||||
@@ -35,7 +35,7 @@ const LockedPasspoint = ({ data }) => {
|
||||
() => (
|
||||
<>
|
||||
<SimpleGrid minChildWidth="180px" gap={4} mb={4}>
|
||||
<NumberInputField name="width" label="width" w="140px" emptyIsUndefined isRequired unit="px" />
|
||||
<NumberField name="width" label="width" w="140px" emptyIsUndefined isRequired unit="px" />
|
||||
<NumberField name="height" label="height" w="140px" isRequired unit="px" />
|
||||
<StringField name="language" label="language" w="100px" isRequired />
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { SimpleGrid, useToast } from '@chakra-ui/react';
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, SimpleGrid, useToast } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LockedAdvanced from './LockedAdvanced';
|
||||
@@ -8,7 +8,9 @@ import LockedPasspoint from './LockedPasspoint';
|
||||
import DisplayMultiSelectField from 'components/DisplayFields/DisplayMultiSelectField';
|
||||
import DisplaySelectField from 'components/DisplayFields/DisplaySelectField';
|
||||
import DisplayStringField from 'components/DisplayFields/DisplayStringField';
|
||||
import { useGetRadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import { useGetResource } from 'hooks/Network/Resources';
|
||||
import useRadiusEndpointAccountModal from 'pages/SystemConfigurationPage/RadiusEndpoints/ViewDetailsModal/useEditModal';
|
||||
|
||||
const propTypes = {
|
||||
variableBlockId: PropTypes.string.isRequired,
|
||||
@@ -17,6 +19,7 @@ const propTypes = {
|
||||
const LockedSsid = ({ variableBlockId }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const modal = useRadiusEndpointAccountModal({ hideEdit: true });
|
||||
const { data: resource } = useGetResource({
|
||||
t,
|
||||
toast,
|
||||
@@ -31,10 +34,26 @@ const LockedSsid = ({ variableBlockId }) => {
|
||||
return null;
|
||||
}, [resource]);
|
||||
|
||||
const getEndpoint = useGetRadiusEndpoint({
|
||||
id: data?.radius?.__radiusEndpoint,
|
||||
});
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal.modal}
|
||||
{getEndpoint.data ? (
|
||||
<Alert status="info" mb={4} onClick={() => modal.openModal(getEndpoint.data)} cursor="pointer" w="max-content">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>Custom radius endpoint: {getEndpoint.data.name}</AlertTitle>
|
||||
<AlertDescription>
|
||||
Click <b>here</b> to view details
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
) : null}
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<DisplayStringField value={data.name} label="name" definitionKey="interface.ssid.name" isRequired />
|
||||
<DisplaySelectField
|
||||
@@ -62,7 +81,7 @@ const LockedSsid = ({ variableBlockId }) => {
|
||||
isRequired
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<LockedEncryption data={data} />
|
||||
<LockedEncryption data={data} isUsingRadiusEndpoint={getEndpoint.data} />
|
||||
<LockedPasspoint data={data} />
|
||||
<LockedAdvanced data={data} />
|
||||
</>
|
||||
|
||||
@@ -14,16 +14,10 @@ interface Props {
|
||||
namePrefix: string;
|
||||
isEnabled: boolean;
|
||||
onToggle: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
lockConsortium?: boolean;
|
||||
}
|
||||
|
||||
const PassPointForm = (
|
||||
{
|
||||
isDisabled,
|
||||
namePrefix,
|
||||
isEnabled,
|
||||
onToggle
|
||||
}: Props
|
||||
) => {
|
||||
const PassPointForm: React.FC<Props> = ({ isDisabled, namePrefix, isEnabled, onToggle, lockConsortium }) => {
|
||||
const name = React.useCallback((suffix: string) => `${namePrefix}.${suffix}`, []);
|
||||
|
||||
const fieldProps = (suffix: string) => ({
|
||||
@@ -157,7 +151,12 @@ const PassPointForm = (
|
||||
<ToggleField {...fieldProps('esr')} falseIsUndefined />
|
||||
<ToggleField {...fieldProps('uesa')} falseIsUndefined />
|
||||
<StringField {...fieldProps('hessid')} emptyIsUndefined />
|
||||
<CreatableSelectField {...fieldProps('roaming-consortium')} emptyIsUndefined placeholder="BAA2D00100" />
|
||||
<CreatableSelectField
|
||||
{...fieldProps('roaming-consortium')}
|
||||
emptyIsUndefined
|
||||
placeholder="BAA2D00100"
|
||||
isDisabled={lockConsortium || isDisabled}
|
||||
/>
|
||||
<ToggleField {...fieldProps('disable-dgaf')} falseIsUndefined />
|
||||
<NumberField {...fieldProps('ipaddr-type-available')} acceptEmptyValue emptyIsUndefined />
|
||||
<SelectField
|
||||
|
||||
@@ -8,9 +8,10 @@ type Props = {
|
||||
isDisabled?: boolean;
|
||||
namePrefix: string;
|
||||
radiusPrefix: string;
|
||||
lockConsortium?: boolean;
|
||||
};
|
||||
|
||||
const PassPointConfig = ({ isDisabled, namePrefix, radiusPrefix }: Props) => {
|
||||
const PassPointConfig = ({ isDisabled, namePrefix, radiusPrefix, lockConsortium }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { value, onChange } = useFastField({ name: namePrefix });
|
||||
const { value: radius } = useFastField({ name: radiusPrefix });
|
||||
@@ -36,7 +37,15 @@ const PassPointConfig = ({ isDisabled, namePrefix, radiusPrefix }: Props) => {
|
||||
[onChange, radius],
|
||||
);
|
||||
|
||||
return <PassPointForm isDisabled={isDisabled} namePrefix={namePrefix} isEnabled={isEnabled} onToggle={onToggle} />;
|
||||
return (
|
||||
<PassPointForm
|
||||
isDisabled={isDisabled}
|
||||
namePrefix={namePrefix}
|
||||
isEnabled={isEnabled}
|
||||
onToggle={onToggle}
|
||||
lockConsortium={lockConsortium}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PassPointConfig);
|
||||
|
||||
@@ -3,14 +3,13 @@ import { Box, Flex, Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
||||
import { getIn, useFormikContext } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { INTERFACE_SSID_SCHEMA } from '../../interfacesConstants';
|
||||
import AdvancedSettings from './AdvancedSettings';
|
||||
import Encryption from './Encryption';
|
||||
import LockedSsid from './LockedSsid';
|
||||
import PassPoint from './PassPoint';
|
||||
import SsidResourcePicker from './SsidResourcePicker';
|
||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import ConfigurationResourcePicker from 'components/CustomFields/ConfigurationResourcePicker';
|
||||
import MultiSelectField from 'components/FormFields/MultiSelectField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
@@ -42,74 +41,71 @@ const SingleSsid = ({ editing, index, namePrefix, remove }) => {
|
||||
<Heading size="md" mr={2} pt={2}>
|
||||
#{index}
|
||||
</Heading>
|
||||
<ConfigurationResourcePicker
|
||||
name={namePrefix}
|
||||
prefix="interface.ssid"
|
||||
isDisabled={!editing}
|
||||
defaultValue={INTERFACE_SSID_SCHEMA}
|
||||
/>
|
||||
<SsidResourcePicker name={namePrefix} isDisabled={!editing} />
|
||||
<Spacer />
|
||||
<DeleteButton isDisabled={!editing} onClick={removeSsid} label={t('configurations.delete_ssid')} />
|
||||
</Flex>
|
||||
<CardBody display="unset">
|
||||
{isUsingCustomRadius ? (
|
||||
<>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mt={2}>
|
||||
<StringField
|
||||
name={`${namePrefix}.name`}
|
||||
label="SSID"
|
||||
definitionKey="interface.ssid.name"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
/>
|
||||
<SelectField
|
||||
name={`${namePrefix}.bss-mode`}
|
||||
label="bss-mode"
|
||||
definitionKey="interface.ssid.bss-mode"
|
||||
isDisabled={!editing}
|
||||
options={[
|
||||
{ value: 'ap', label: 'ap' },
|
||||
{ value: 'sta', label: 'sta' },
|
||||
{ value: 'mesh', label: 'mesh' },
|
||||
{ value: 'wds-ap', label: 'wds-ap' },
|
||||
{ value: 'wds-sta', label: 'wds-sta' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
<MultiSelectField
|
||||
name={`${namePrefix}.wifi-bands`}
|
||||
label="wifi-bands"
|
||||
definitionKey="interface.ssid.wifi-bands"
|
||||
isDisabled={!editing}
|
||||
options={[
|
||||
{ value: '2G', label: '2G' },
|
||||
{ value: '5G', label: '5G' },
|
||||
{ value: '5G-lower', label: '5G-lower' },
|
||||
{ value: '5G-upper', label: '5G-upper' },
|
||||
{ value: '6G', label: '6G' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<Encryption
|
||||
editing={editing}
|
||||
ssidName={namePrefix}
|
||||
namePrefix={`${namePrefix}.encryption`}
|
||||
radiusPrefix={`${namePrefix}.radius`}
|
||||
isPasspoint={isPasspoint}
|
||||
/>
|
||||
<Box my={2}>
|
||||
<PassPoint
|
||||
isDisabled={!editing}
|
||||
namePrefix={`${namePrefix}.pass-point`}
|
||||
<Box>
|
||||
{isUsingCustomRadius ? (
|
||||
<>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px" mt={2}>
|
||||
<StringField
|
||||
name={`${namePrefix}.name`}
|
||||
label="SSID"
|
||||
definitionKey="interface.ssid.name"
|
||||
isDisabled={!editing}
|
||||
isRequired
|
||||
/>
|
||||
<SelectField
|
||||
name={`${namePrefix}.bss-mode`}
|
||||
label="bss-mode"
|
||||
definitionKey="interface.ssid.bss-mode"
|
||||
isDisabled={!editing}
|
||||
options={[
|
||||
{ value: 'ap', label: 'ap' },
|
||||
{ value: 'sta', label: 'sta' },
|
||||
{ value: 'mesh', label: 'mesh' },
|
||||
{ value: 'wds-ap', label: 'wds-ap' },
|
||||
{ value: 'wds-sta', label: 'wds-sta' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
<MultiSelectField
|
||||
name={`${namePrefix}.wifi-bands`}
|
||||
label="wifi-bands"
|
||||
definitionKey="interface.ssid.wifi-bands"
|
||||
isDisabled={!editing}
|
||||
options={[
|
||||
{ value: '2G', label: '2G' },
|
||||
{ value: '5G', label: '5G' },
|
||||
{ value: '5G-lower', label: '5G-lower' },
|
||||
{ value: '5G-upper', label: '5G-upper' },
|
||||
{ value: '6G', label: '6G' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<Encryption
|
||||
editing={editing}
|
||||
ssidName={namePrefix}
|
||||
namePrefix={`${namePrefix}.encryption`}
|
||||
radiusPrefix={`${namePrefix}.radius`}
|
||||
isPasspoint={isPasspoint}
|
||||
/>
|
||||
</Box>
|
||||
<AdvancedSettings editing={editing} namePrefix={namePrefix} />
|
||||
</>
|
||||
) : (
|
||||
<LockedSsid variableBlockId={getIn(values, `${namePrefix}.__variableBlock`)[0]} />
|
||||
)}
|
||||
<Box my={2}>
|
||||
<PassPoint
|
||||
isDisabled={!editing}
|
||||
namePrefix={`${namePrefix}.pass-point`}
|
||||
radiusPrefix={`${namePrefix}.radius`}
|
||||
/>
|
||||
</Box>
|
||||
<AdvancedSettings editing={editing} namePrefix={namePrefix} />
|
||||
</>
|
||||
) : (
|
||||
<LockedSsid variableBlockId={getIn(values, `${namePrefix}.__variableBlock`)[0]} />
|
||||
)}
|
||||
</Box>
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { Select } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { INTERFACE_SSID_SCHEMA } from '../../interfacesConstants';
|
||||
import { useConfigurationContext } from 'contexts/ConfigurationProvider';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
|
||||
const SsidResourcePicker = ({ name, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const context = useConfigurationContext();
|
||||
const field = useFastField<{ __variableBlock?: string[] } | undefined>({ name });
|
||||
|
||||
const availableResources = React.useMemo(() => {
|
||||
if (context.availableResources)
|
||||
return context.availableResources
|
||||
.filter(
|
||||
(resource) =>
|
||||
resource.variables[0]?.prefix === 'interface.ssid.openroaming' ||
|
||||
resource.variables[0]?.prefix === 'interface.ssid',
|
||||
)
|
||||
.map((resource) => ({ value: resource.id, label: resource.name }));
|
||||
return [];
|
||||
}, [context.availableResources?.length]);
|
||||
|
||||
const selectValue = React.useMemo(() => {
|
||||
if (!field.value || !field.value.__variableBlock) return '';
|
||||
return field.value.__variableBlock[0];
|
||||
}, [field.value?.__variableBlock]);
|
||||
|
||||
const onChange = React.useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (e.target.value === '') field.onChange(INTERFACE_SSID_SCHEMA(t, true).cast());
|
||||
else {
|
||||
const newObj = {} as { __variableBlock: string[] };
|
||||
newObj.__variableBlock = [e.target.value];
|
||||
field.onChange(newObj);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Select value={selectValue} isDisabled={isDisabled} maxW={72} onChange={onChange}>
|
||||
<option value="">{t('configurations.no_resource_selected')}</option>
|
||||
{availableResources.map((res) => (
|
||||
<option key={res.value} value={res.value}>
|
||||
{res.label}
|
||||
</option>
|
||||
))}
|
||||
{selectValue !== '' && !availableResources.find(({ value: resource }) => resource === selectValue) && (
|
||||
<option value={selectValue}>{t('configurations.invalid_resource')}</option>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default SsidResourcePicker;
|
||||
@@ -763,7 +763,7 @@ export const SINGLE_INTERFACE_SCHEMA = (
|
||||
.default({ addressing: 'dynamic' })
|
||||
: INTERFACE_IPV4_SCHEMA(t, useDefault),
|
||||
tunnel: INTERFACE_TUNNEL_SCHEMA(t, useDefault).default(undefined),
|
||||
ssids: array().of(INTERFACE_SSID_SCHEMA(t, useDefault)).default([]),
|
||||
ssids: array().of(INTERFACE_SSID_SCHEMA(t, useDefault)).default(undefined),
|
||||
'hostapd-bss-raw': array().of(string()).default(undefined),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bool, object, number, string } from 'yup';
|
||||
import { testAlphanumWithDash } from 'constants/formTests';
|
||||
import { testFqdnHostname } from 'constants/formTests';
|
||||
|
||||
export const DEFAULT_UNIT = {
|
||||
name: 'Unit',
|
||||
@@ -28,7 +28,7 @@ export const UNIT_SCHEMA = (t) =>
|
||||
name: string().required(t('form.required')).default(''),
|
||||
location: string().required(t('form.required')).default(''),
|
||||
hostname: string()
|
||||
.test('test-hostname-network', t('form.invalid_hostname'), testAlphanumWithDash)
|
||||
.test('test-hostname-network', t('form.invalid_fqdn_host'), (v) => v === undefined || testFqdnHostname(v))
|
||||
.default(undefined),
|
||||
timezone: string().default(undefined),
|
||||
'leds-active': bool().default(true),
|
||||
|
||||
@@ -18,13 +18,14 @@ import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import LoadingOverlay from 'components/LoadingOverlay';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import { ConfigurationProvider } from 'contexts/ConfigurationProvider';
|
||||
import { useGetConfiguration, useUpdateConfiguration } from 'hooks/Network/Configurations';
|
||||
|
||||
const propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const tryParse = (value) => {
|
||||
const try_parse = (value) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
@@ -77,7 +78,7 @@ const ConfigurationCard = ({ id }) => {
|
||||
if (conf !== 'third-party') deviceConfig.__selected_subcategories = undefined;
|
||||
const config = { ...sections.data[conf].data, configuration: {} };
|
||||
if (conf === 'interfaces') config.configuration = { interfaces: deviceConfig };
|
||||
else if (conf === 'third-party') config.configuration = { 'third-party': tryParse(deviceConfig) };
|
||||
else if (conf === 'third-party') config.configuration = { 'third-party': try_parse(deviceConfig) };
|
||||
else config.configuration[conf] = deviceConfig;
|
||||
return config;
|
||||
}),
|
||||
@@ -140,8 +141,8 @@ const ConfigurationCard = ({ id }) => {
|
||||
return (
|
||||
<>
|
||||
<Card mb={4}>
|
||||
<CardHeader>
|
||||
<Box pt={1}>
|
||||
<CardHeader mb={0}>
|
||||
<Box>
|
||||
<Heading size="md">{configuration?.name}</Heading>
|
||||
</Box>
|
||||
<Spacer />
|
||||
@@ -182,7 +183,9 @@ const ConfigurationCard = ({ id }) => {
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
<ConfigurationSectionsCard editing={editing} configId={id} setSections={setSections} />
|
||||
<ConfigurationProvider configurationId={id}>
|
||||
<ConfigurationSectionsCard editing={editing} configId={id} setSections={setSections} />
|
||||
</ConfigurationProvider>
|
||||
<ConfirmConfigurationWarnings
|
||||
isOpen={showWarnings}
|
||||
onClose={closeWarnings}
|
||||
|
||||
172
src/pages/OpenRoamingPage/GlobalReachPage/AccountTable.tsx
Normal file
172
src/pages/OpenRoamingPage/GlobalReachPage/AccountTable.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import * as React from 'react';
|
||||
import { State } from 'country-state-city';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ActionCell from './ActionCell';
|
||||
import CreateGlobalReachAccountModal from './CreateGlobalReachAccountModal';
|
||||
import useGlobalAccountModal from './DetailsModal/useEditModal';
|
||||
import { DataGrid } from 'components/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataGrid/useDataGrid';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import COUNTRY_LIST from 'constants/countryList';
|
||||
import { GlobalReachAccount, useGetGlobalReachAccounts } from 'hooks/Network/GlobalReach';
|
||||
|
||||
const dateCell = (date: number) => <FormattedDate date={date} key={uuid()} />;
|
||||
const actionCell = (row: GlobalReachAccount, open: (acc: GlobalReachAccount) => void) => (
|
||||
<ActionCell account={row} openDetailsModal={open} />
|
||||
);
|
||||
const countryCell = (row: GlobalReachAccount) => {
|
||||
const found = COUNTRY_LIST.find((c) => c.value === row.country);
|
||||
|
||||
return found?.label ?? row.country;
|
||||
};
|
||||
const provinceCell = (row: GlobalReachAccount) => {
|
||||
const found = State.getStateByCodeAndCountry(row.province, row.country);
|
||||
|
||||
return found?.name ?? row.province;
|
||||
};
|
||||
|
||||
const GlobalReachAccountTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const detailsModal = useGlobalAccountModal();
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'provisioning.global_reach_roaming.table',
|
||||
defaultSortBy: [{ id: 'name', desc: false }],
|
||||
defaultOrder: [
|
||||
'name',
|
||||
'modified',
|
||||
'country',
|
||||
'province',
|
||||
'city',
|
||||
'organization',
|
||||
'commonName',
|
||||
'description',
|
||||
'actions',
|
||||
],
|
||||
});
|
||||
const getAccounts = useGetGlobalReachAccounts();
|
||||
|
||||
const columns: DataGridColumn<GlobalReachAccount>[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'name',
|
||||
header: t('common.name'),
|
||||
accessorKey: 'name',
|
||||
meta: {
|
||||
customMaxWidth: '200px',
|
||||
customWidth: 'calc(15vh)',
|
||||
customMinWidth: '150px',
|
||||
anchored: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'modified',
|
||||
header: t('common.modified'),
|
||||
|
||||
accessorKey: 'modified',
|
||||
cell: (cell) => dateCell(cell.row.original.modified),
|
||||
meta: {
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'country',
|
||||
header: t('roaming.country'),
|
||||
accessorKey: 'country',
|
||||
cell: (cell) => countryCell(cell.row.original),
|
||||
meta: {
|
||||
customMinWidth: '100px',
|
||||
customWidth: '100px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'province',
|
||||
header: t('roaming.province'),
|
||||
accessorKey: 'province',
|
||||
cell: (cell) => provinceCell(cell.row.original),
|
||||
meta: {
|
||||
customMinWidth: '100px',
|
||||
customWidth: '100px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'city',
|
||||
header: t('roaming.city'),
|
||||
accessorKey: 'city',
|
||||
meta: {
|
||||
customMinWidth: '100px',
|
||||
customWidth: '100px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
header: t('roaming.organization'),
|
||||
accessorKey: 'organization',
|
||||
meta: {
|
||||
customMinWidth: '100px',
|
||||
customWidth: '100px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'commonName',
|
||||
header: t('roaming.common_name'),
|
||||
accessorKey: 'commonName',
|
||||
meta: {
|
||||
customMinWidth: '100px',
|
||||
customWidth: '100px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
header: t('common.description'),
|
||||
accessorKey: 'description',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
accessorKey: 'id',
|
||||
cell: (cell) => actionCell(cell.row.original, detailsModal.openModal),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '80px',
|
||||
alwaysShow: true,
|
||||
columnSelectorOptions: {
|
||||
label: t('common.actions'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{detailsModal.modal}
|
||||
<DataGrid<GlobalReachAccount>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: `${t('roaming.account', { count: getAccounts.data?.length ?? 0 })} ${
|
||||
getAccounts.data?.length ? `(${getAccounts.data.length})` : ''
|
||||
}`,
|
||||
objectListed: t('roaming.account_other'),
|
||||
addButton: <CreateGlobalReachAccountModal />,
|
||||
}}
|
||||
columns={columns}
|
||||
data={getAccounts.data ?? []}
|
||||
isLoading={getAccounts.isFetching}
|
||||
options={{
|
||||
count: getAccounts.data?.length ?? 0,
|
||||
onRowClick: (device) => () => detailsModal.openModal(device),
|
||||
refetch: getAccounts.refetch,
|
||||
minimumHeight: '200px',
|
||||
showAsCard: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalReachAccountTable;
|
||||
93
src/pages/OpenRoamingPage/GlobalReachPage/ActionCell.tsx
Normal file
93
src/pages/OpenRoamingPage/GlobalReachPage/ActionCell.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
HStack,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GlobalReachAccount, useDeleteGlobalReachAccount } from 'hooks/Network/GlobalReach';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
type Props = {
|
||||
account: GlobalReachAccount;
|
||||
openDetailsModal: (account: GlobalReachAccount) => void;
|
||||
};
|
||||
|
||||
const ActionCell = ({ account, openDetailsModal }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const deleteAccount = useDeleteGlobalReachAccount();
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
|
||||
const onDelete = (onClose: () => void) => async () => {
|
||||
await deleteAccount.mutateAsync(account.id, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: t('roaming.account_deleted'),
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack spacing={2}>
|
||||
<Popover placement="start">
|
||||
{({ onClose, isOpen }) => (
|
||||
<>
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="334px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.delete')} {account.name}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>{t('crud.delete_confirm', { obj: t('roaming.account_one') })}</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={onDelete(onClose)} isLoading={deleteAccount.isLoading}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<Tooltip label={t('common.view_details')}>
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
onClick={() => openDetailsModal(account)}
|
||||
size="sm"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
colorScheme="blue"
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionCell;
|
||||
@@ -0,0 +1,185 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Yup from 'yup';
|
||||
import PrivateKeyField from './PrivateKeyField';
|
||||
import StatePicker from './StatePicker';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import COUNTRY_LIST from 'constants/countryList';
|
||||
import { testPemPrivateKey } from 'constants/formTests';
|
||||
import { CreateGlobalReachAccountRequest, useCreateGlobalReachAccount } from 'hooks/Network/GlobalReach';
|
||||
import useFormModal from 'hooks/useFormModal';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
const CreateGlobalReachAccountModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const [formKey, setFormKey] = React.useState(0);
|
||||
const { form, formRef } = useFormRef<CreateGlobalReachAccountRequest>();
|
||||
const modal = useFormModal({
|
||||
isDirty: form.dirty,
|
||||
onCloseSideEffect: () => {
|
||||
setFormKey((k) => k + 1);
|
||||
},
|
||||
});
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
const create = useCreateGlobalReachAccount();
|
||||
|
||||
const onSubmit = async (
|
||||
data: CreateGlobalReachAccountRequest,
|
||||
helpers: FormikHelpers<CreateGlobalReachAccountRequest>,
|
||||
) => {
|
||||
helpers.setSubmitting(true);
|
||||
|
||||
await create.mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: t('roaming.account_created'),
|
||||
});
|
||||
modal.closeCancelAndForm();
|
||||
helpers.resetForm();
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(t('form.required')),
|
||||
description: Yup.string(),
|
||||
notes: Yup.array().of(Yup.string()),
|
||||
privateKey: Yup.string()
|
||||
.test('test-key', t('roaming.invalid_key'), (v) => testPemPrivateKey(v))
|
||||
.required(t('form.required')),
|
||||
country: Yup.string().required(t('form.required')),
|
||||
province: Yup.string().required(t('form.required')),
|
||||
city: Yup.string().required(t('form.required')),
|
||||
organization: Yup.string().required(t('form.required')),
|
||||
commonName: Yup.string().required(t('form.required')),
|
||||
GlobalReachAcctId: Yup.string().required(t('form.required')),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const isFieldDisabled = create.isLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateButton onClick={modal.onOpen} />
|
||||
<Modal
|
||||
isOpen={modal.isOpen}
|
||||
onClose={modal.closeModal}
|
||||
title={`${t('common.create')} ${t('roaming.account_one')}`}
|
||||
topRightButtons={<SaveButton onClick={form.submitForm} isLoading={form.isSubmitting} />}
|
||||
>
|
||||
<Box>
|
||||
<Formik
|
||||
key={formKey}
|
||||
innerRef={formRef}
|
||||
initialValues={
|
||||
{
|
||||
name: '',
|
||||
description: '',
|
||||
notes: [],
|
||||
privateKey: '',
|
||||
country: 'US',
|
||||
province: 'AL',
|
||||
city: '',
|
||||
organization: '',
|
||||
commonName: '',
|
||||
GlobalReachAcctId: '',
|
||||
} as CreateGlobalReachAccountRequest
|
||||
}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Box>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
{t('roaming.account_one')} {t('common.details')}
|
||||
</Heading>
|
||||
<Flex my={2}>
|
||||
<StringField name="name" label={t('common.name')} isRequired isDisabled={isFieldDisabled} w="300px" />
|
||||
<Box ml={2}>
|
||||
<StringField
|
||||
name="commonName"
|
||||
label={`${t('roaming.common_name')}`}
|
||||
isRequired
|
||||
placeholder="example.com"
|
||||
isDisabled={isFieldDisabled}
|
||||
w="300px"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>{' '}
|
||||
<StringField name="Description" isArea h="80px" isDisabled={isFieldDisabled} />
|
||||
<Heading size="md" textDecoration="underline" mt={2}>
|
||||
{t('roaming.global_reach')}
|
||||
</Heading>
|
||||
<Flex my={2}>
|
||||
<StringField
|
||||
name="GlobalReachAcctId"
|
||||
label={t('roaming.global_reach_account_id')}
|
||||
isRequired
|
||||
isDisabled={isFieldDisabled}
|
||||
w="266px"
|
||||
/>
|
||||
<Box ml={2}>
|
||||
<PrivateKeyField isDisabled={isFieldDisabled} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
{t('roaming.location_details_title')}
|
||||
</Heading>{' '}
|
||||
<Flex my={2}>
|
||||
<Box>
|
||||
<SelectField
|
||||
name="country"
|
||||
label={t('roaming.country')}
|
||||
options={COUNTRY_LIST}
|
||||
isRequired
|
||||
w="max-content"
|
||||
/>
|
||||
</Box>
|
||||
<Box ml={2}>
|
||||
<StatePicker isDisabled={isFieldDisabled} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex my={2}>
|
||||
<StringField
|
||||
name="city"
|
||||
label={`${t('roaming.city')}`}
|
||||
isRequired
|
||||
isDisabled={isFieldDisabled}
|
||||
w="266px"
|
||||
/>
|
||||
<Box ml={2}>
|
||||
<StringField
|
||||
name="organization"
|
||||
label={`${t('roaming.organization')}`}
|
||||
isRequired
|
||||
isDisabled={isFieldDisabled}
|
||||
w="300px"
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Formik>
|
||||
</Box>
|
||||
</Modal>
|
||||
<ConfirmCloseAlert isOpen={modal.isConfirmOpen} confirm={modal.closeCancelAndForm} cancel={modal.closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateGlobalReachAccountModal;
|
||||
@@ -0,0 +1,117 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
HStack,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { Recycle, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
GlobalReachCertificate,
|
||||
useDeleteGlobalReachCertificate,
|
||||
useRenewGlobalReachCertificate,
|
||||
} from 'hooks/Network/GlobalReach';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
type Props = {
|
||||
certificate: GlobalReachCertificate;
|
||||
};
|
||||
|
||||
const GlobalReachCertActionCell = ({ certificate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const renewCertificate = useRenewGlobalReachCertificate();
|
||||
const deleteCertificate = useDeleteGlobalReachCertificate();
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
|
||||
const onDelete = (onClose: () => void) => async () => {
|
||||
await deleteCertificate.mutateAsync(
|
||||
{ id: certificate.id, accountId: certificate.accountId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: t('roaming.certificate_deleted'),
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onRenew = async () => {
|
||||
await renewCertificate.mutateAsync(
|
||||
{ id: certificate.id, accountId: certificate.accountId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: 'Recreated certificate!',
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack>
|
||||
<Popover placement="start">
|
||||
{({ onClose, isOpen }) => (
|
||||
<>
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="334px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.delete')} {certificate.name}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>{t('crud.delete_confirm', { obj: t('roaming.certificate_one') })}</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={onDelete(onClose)} isLoading={deleteCertificate.isLoading}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<Tooltip hasArrow label="Recreate" placement="top">
|
||||
<IconButton
|
||||
aria-label="Recreate"
|
||||
colorScheme="blue"
|
||||
icon={<Recycle size={20} />}
|
||||
size="sm"
|
||||
isLoading={renewCertificate.isLoading}
|
||||
onClick={onRenew}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalReachCertActionCell;
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Heading,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import { GlobalReachAccount, useCreateGlobalReachCertificate } from 'hooks/Network/GlobalReach';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
type Props = {
|
||||
account: GlobalReachAccount;
|
||||
};
|
||||
|
||||
const CreateGlobalReachCertificateButton = ({ account }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const popoverProps = useDisclosure();
|
||||
const create = useCreateGlobalReachCertificate();
|
||||
const [name, setName] = React.useState('');
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
|
||||
const handleCreate = () => {
|
||||
create.mutate(
|
||||
{
|
||||
accountId: account.id,
|
||||
name,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: 'Certificate created successfully',
|
||||
});
|
||||
popoverProps.onClose();
|
||||
setName('');
|
||||
},
|
||||
onError: (e) => {
|
||||
apiErrorToast({ e });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setName('');
|
||||
}, [popoverProps.isOpen]);
|
||||
|
||||
return (
|
||||
<Popover {...popoverProps} placement="start">
|
||||
<PopoverTrigger>
|
||||
<Box>
|
||||
<CreateButton onClick={popoverProps.onOpen} />
|
||||
</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="334px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.create')} {t('certificates.certificate')}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Heading size="sm">What should be this certificate's name?</Heading>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={popoverProps.onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
ml="1"
|
||||
onClick={handleCreate}
|
||||
isLoading={create.isLoading}
|
||||
isDisabled={name.length < 3}
|
||||
>
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateGlobalReachCertificateButton;
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import GlobalReachCertActionCell from './ActionCell';
|
||||
import CreateGlobalReachCertificateButton from './AddButton';
|
||||
import CopyCell from './CopyCell';
|
||||
import { DataGrid } from 'components/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataGrid/useDataGrid';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import { GlobalReachAccount, GlobalReachCertificate, useGetGlobalReachCertificates } from 'hooks/Network/GlobalReach';
|
||||
|
||||
const dateCell = (date: number) => <FormattedDate date={date} key={uuid()} />;
|
||||
const copyCell = (value: string) => <CopyCell value={value} key={uuid()} isCompact />;
|
||||
const actionCell = (row: GlobalReachCertificate) => <GlobalReachCertActionCell certificate={row} />;
|
||||
|
||||
type Props = {
|
||||
account: GlobalReachAccount;
|
||||
isSubTable?: boolean;
|
||||
};
|
||||
|
||||
const CertificatesTable = ({ account, isSubTable }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const getCertificates = useGetGlobalReachCertificates(account.id);
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'provisioning.global_reach_roaming_certs.table',
|
||||
defaultSortBy: [{ id: 'name', desc: false }],
|
||||
defaultOrder: [
|
||||
'name',
|
||||
'created',
|
||||
'expiresAt',
|
||||
'csr',
|
||||
'certificate',
|
||||
'certificateChain',
|
||||
'certificateId',
|
||||
'actions',
|
||||
],
|
||||
});
|
||||
|
||||
const columns: DataGridColumn<GlobalReachCertificate>[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'name',
|
||||
header: t('common.name'),
|
||||
accessorKey: 'name',
|
||||
meta: {
|
||||
customMaxWidth: '200px',
|
||||
customWidth: 'calc(15vh)',
|
||||
customMinWidth: '150px',
|
||||
anchored: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
header: t('common.created'),
|
||||
|
||||
accessorKey: 'created',
|
||||
cell: (cell) => dateCell(cell.row.original.created),
|
||||
meta: {
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expires',
|
||||
header: 'Expires',
|
||||
|
||||
accessorKey: 'created',
|
||||
cell: (cell) => dateCell(cell.row.original.expiresAt),
|
||||
meta: {
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'csr',
|
||||
header: 'CSR',
|
||||
accessorKey: 'csr',
|
||||
cell: (cell) => copyCell(cell.row.original.csr),
|
||||
meta: {
|
||||
customMaxWidth: '50px',
|
||||
customWidth: '50px',
|
||||
customMinWidth: '50px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'certificate',
|
||||
header: 'Cert',
|
||||
accessorKey: 'certificate',
|
||||
cell: (cell) => copyCell(cell.row.original.certificate),
|
||||
meta: {
|
||||
customMaxWidth: '60px',
|
||||
customWidth: '60px',
|
||||
customMinWidth: '60px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'certificateChain',
|
||||
header: 'Cert Chain',
|
||||
accessorKey: 'certificateChain',
|
||||
cell: (cell) => copyCell(cell.row.original.certificateChain),
|
||||
meta: {
|
||||
customMaxWidth: '120px',
|
||||
customWidth: '120px',
|
||||
customMinWidth: '120px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
accessorKey: 'id',
|
||||
cell: (cell) => actionCell(cell.row.original),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '10px',
|
||||
alwaysShow: true,
|
||||
columnSelectorOptions: {
|
||||
label: t('common.actions'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
return (
|
||||
<DataGrid<GlobalReachCertificate>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: isSubTable
|
||||
? ''
|
||||
: `${t('roaming.certificate', { count: getCertificates.data?.length ?? 0 })} ${
|
||||
getCertificates.data?.length ? `(${getCertificates.data.length})` : ''
|
||||
}`,
|
||||
objectListed: t('roaming.certificate_other'),
|
||||
addButton: isSubTable ? null : <CreateGlobalReachCertificateButton account={account} />,
|
||||
}}
|
||||
columns={isSubTable ? columns.filter(({ id }) => id !== 'actions') : columns}
|
||||
data={getCertificates.data ?? []}
|
||||
isLoading={getCertificates.isFetching}
|
||||
options={{
|
||||
count: getCertificates.data?.length ?? 0,
|
||||
// onRowClick: (device) => () => detailsModal.openModal(device),
|
||||
refetch: getCertificates.refetch,
|
||||
minimumHeight: '200px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificatesTable;
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { CopyIcon } from '@chakra-ui/icons';
|
||||
import { Button, Center, IconButton, Tooltip, useClipboard } from '@chakra-ui/react';
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
isCompact?: boolean;
|
||||
};
|
||||
|
||||
const CopyCell = ({ value, isCompact }: Props) => {
|
||||
const copy = useClipboard(value);
|
||||
|
||||
if (isCompact) {
|
||||
return (
|
||||
<Tooltip label={copy.hasCopied ? 'Copied!' : 'Copy'} placement="top" closeOnClick={false}>
|
||||
<IconButton
|
||||
aria-label="Copy"
|
||||
size="sm"
|
||||
onClick={copy.onCopy}
|
||||
isDisabled={value.length === 0}
|
||||
colorScheme="teal"
|
||||
icon={<CopyIcon />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Button size="sm" onClick={copy.onCopy} isDisabled={value.length === 0}>
|
||||
{copy.hasCopied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyCell;
|
||||
128
src/pages/OpenRoamingPage/GlobalReachPage/DetailsModal/index.tsx
Normal file
128
src/pages/OpenRoamingPage/GlobalReachPage/DetailsModal/index.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Heading,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Text,
|
||||
UseDisclosureReturn,
|
||||
useClipboard,
|
||||
} from '@chakra-ui/react';
|
||||
import { State } from 'country-state-city';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CertificatesTable from './CertificatesTable';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import COUNTRY_LIST from 'constants/countryList';
|
||||
import { GlobalReachAccount } from 'hooks/Network/GlobalReach';
|
||||
|
||||
const labelProps = {
|
||||
mr: 2,
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
account: GlobalReachAccount;
|
||||
modalProps: UseDisclosureReturn;
|
||||
};
|
||||
|
||||
const DetailsModal = ({ account, modalProps }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const privateKeyCopy = useClipboard(account.privateKey);
|
||||
const csrCopy = useClipboard(account.CSR);
|
||||
|
||||
const state = () => {
|
||||
const found = State.getStateByCodeAndCountry(account.province, account.country);
|
||||
|
||||
return found?.name ?? account.province;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
privateKeyCopy.setValue(account.privateKey);
|
||||
csrCopy.setValue(account.CSR);
|
||||
}, [account.privateKey]);
|
||||
|
||||
return (
|
||||
<Modal {...modalProps} title={account.name}>
|
||||
<Box>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>{t('common.details')}</Tab>
|
||||
<Tab>{t('certificates.title')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
{t('roaming.account_one')} {t('common.details')}
|
||||
</Heading>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" {...labelProps}>
|
||||
{t('common.name')}:
|
||||
</Heading>
|
||||
<Text>{account.name}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" {...labelProps}>
|
||||
{t('roaming.common_name')}:
|
||||
</Heading>
|
||||
<Text>{account.commonName}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center" hidden={account.description.length === 0}>
|
||||
<Heading size="sm" {...labelProps}>
|
||||
{t('common.description')}:
|
||||
</Heading>
|
||||
<Text>{account.description}</Text>
|
||||
</Flex>
|
||||
<Heading size="md" textDecoration="underline" mt={2}>
|
||||
{t('roaming.global_reach')}
|
||||
</Heading>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" {...labelProps}>
|
||||
{t('roaming.global_reach_account_id')}:
|
||||
</Heading>
|
||||
<Text>{account.GlobalReachAcctId}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" {...labelProps}>
|
||||
{t('roaming.private_key')}:
|
||||
</Heading>
|
||||
<Button onClick={privateKeyCopy.onCopy} size="sm" colorScheme="blue">
|
||||
{privateKeyCopy.hasCopied ? 'Copied!' : t('common.copy')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" {...labelProps}>
|
||||
CSR:
|
||||
</Heading>
|
||||
<Button onClick={csrCopy.onCopy} size="sm" colorScheme="blue">
|
||||
{csrCopy.hasCopied ? 'Copied!' : t('common.copy')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
{t('roaming.location_details_title')}
|
||||
</Heading>
|
||||
<Heading size="sm" my={2}>
|
||||
{account.city}, {state()},{' '}
|
||||
{COUNTRY_LIST.find((acc) => acc.value === account.country)?.label ?? account.country}
|
||||
</Heading>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" {...labelProps}>
|
||||
{t('roaming.organization')}:
|
||||
</Heading>
|
||||
<Text>{account.organization}</Text>
|
||||
</Flex>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CertificatesTable account={account} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailsModal;
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import DetailsModal from '.';
|
||||
import { GlobalReachAccount } from 'hooks/Network/GlobalReach';
|
||||
|
||||
const useGlobalAccountModal = () => {
|
||||
const [account, setAccount] = React.useState<GlobalReachAccount | null>(null);
|
||||
const modalProps = useDisclosure();
|
||||
|
||||
const openModal = (newAcc: GlobalReachAccount) => {
|
||||
setAccount(newAcc);
|
||||
modalProps.onOpen();
|
||||
};
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
openModal,
|
||||
modal: account ? <DetailsModal account={account} modalProps={modalProps} /> : null,
|
||||
}),
|
||||
[account, modalProps],
|
||||
);
|
||||
};
|
||||
|
||||
export default useGlobalAccountModal;
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import FileInputButton from 'components/Buttons/FileInputButton';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const PrivateKeyField = ({ isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [refreshId, setRefreshId] = React.useState(uuid());
|
||||
const privateKey = useFastField<string>({ name: 'privateKey' });
|
||||
|
||||
React.useEffect(() => {
|
||||
setRefreshId(uuid());
|
||||
}, [privateKey.value]);
|
||||
|
||||
return (
|
||||
<FormControl id="privateKey" isRequired isInvalid={privateKey.isError} isDisabled={isDisabled}>
|
||||
<FormLabel>{t('roaming.private_key')}</FormLabel>
|
||||
<FileInputButton
|
||||
value={privateKey.value}
|
||||
setValue={privateKey.onChange}
|
||||
refreshId={refreshId}
|
||||
accept=".pem"
|
||||
isStringFile
|
||||
/>
|
||||
<FormErrorMessage>{privateKey.error}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivateKeyField;
|
||||
49
src/pages/OpenRoamingPage/GlobalReachPage/StatePicker.tsx
Normal file
49
src/pages/OpenRoamingPage/GlobalReachPage/StatePicker.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import { State } from 'country-state-city';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const StatePicker = ({ isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const country = useFastField<string>({ name: 'country' });
|
||||
const province = useFastField<string>({ name: 'province' });
|
||||
const [options, setOptions] = React.useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (country.value) {
|
||||
const states = State.getStatesOfCountry(country.value);
|
||||
setOptions(states.map((s) => ({ label: s.name, value: s.isoCode })));
|
||||
if (states[0]) province.onChange(states[0].isoCode);
|
||||
}
|
||||
}, [country.value]);
|
||||
|
||||
if (options.length === 0)
|
||||
return (
|
||||
<StringField
|
||||
name="province"
|
||||
label={`${t('roaming.province')}/${t('roaming.state')}`}
|
||||
isRequired
|
||||
isDisabled={isDisabled}
|
||||
w="300px"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectField
|
||||
name="province"
|
||||
label={`${t('roaming.province')}/${t('roaming.state')}`}
|
||||
isRequired
|
||||
isDisabled={isDisabled}
|
||||
w="max-content"
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatePicker;
|
||||
6
src/pages/OpenRoamingPage/GlobalReachPage/index.tsx
Normal file
6
src/pages/OpenRoamingPage/GlobalReachPage/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import GlobalReachAccountTable from './AccountTable';
|
||||
|
||||
const GlobalReachPage = () => <GlobalReachAccountTable />;
|
||||
|
||||
export default GlobalReachPage;
|
||||
103
src/pages/OpenRoamingPage/GoogleOrionPage/AccountTable.tsx
Normal file
103
src/pages/OpenRoamingPage/GoogleOrionPage/AccountTable.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import GoogleOrionAccountActionCell from './ActionCell';
|
||||
import CreateGoogleOrionAccountModal from './CreateGoogleOrionAccountModal';
|
||||
import useGoogleOrionAccountModal from './DetailsModal/useEditModal';
|
||||
import { DataGrid } from 'components/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataGrid/useDataGrid';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import { GoogleOrionAccount, useGetGoogleOrionAccounts } from 'hooks/Network/GoogleOrion';
|
||||
|
||||
const dateCell = (date: number) => <FormattedDate date={date} key={uuid()} />;
|
||||
const actionCell = (row: GoogleOrionAccount, open: (acc: GoogleOrionAccount) => void) => (
|
||||
<GoogleOrionAccountActionCell account={row} openDetailsModal={open} />
|
||||
);
|
||||
|
||||
const GoogleOrionAccountTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const detailsModal = useGoogleOrionAccountModal();
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'provisioning.google_orion_roaming.table',
|
||||
defaultSortBy: [{ id: 'name', desc: false }],
|
||||
defaultOrder: ['name', 'modified', 'description', 'actions'],
|
||||
});
|
||||
const getAccounts = useGetGoogleOrionAccounts();
|
||||
|
||||
const columns: DataGridColumn<GoogleOrionAccount>[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'name',
|
||||
header: t('common.name'),
|
||||
accessorKey: 'name',
|
||||
meta: {
|
||||
customMaxWidth: '200px',
|
||||
customWidth: 'calc(15vh)',
|
||||
customMinWidth: '150px',
|
||||
anchored: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'modified',
|
||||
header: t('common.modified'),
|
||||
|
||||
accessorKey: 'modified',
|
||||
cell: (cell) => dateCell(cell.row.original.modified),
|
||||
meta: {
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
header: t('common.description'),
|
||||
accessorKey: 'description',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
accessorKey: 'id',
|
||||
cell: (cell) => actionCell(cell.row.original, detailsModal.openModal),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '80px',
|
||||
alwaysShow: true,
|
||||
columnSelectorOptions: {
|
||||
label: t('common.actions'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{detailsModal.modal}
|
||||
<DataGrid<GoogleOrionAccount>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: `${t('roaming.account', { count: getAccounts.data?.length ?? 0 })} ${
|
||||
getAccounts.data?.length ? `(${getAccounts.data.length})` : ''
|
||||
}`,
|
||||
objectListed: t('roaming.account_other'),
|
||||
addButton: <CreateGoogleOrionAccountModal />,
|
||||
}}
|
||||
columns={columns}
|
||||
data={getAccounts.data ?? []}
|
||||
isLoading={getAccounts.isFetching}
|
||||
options={{
|
||||
count: getAccounts.data?.length ?? 0,
|
||||
onRowClick: (device) => () => detailsModal.openModal(device),
|
||||
refetch: getAccounts.refetch,
|
||||
minimumHeight: '200px',
|
||||
showAsCard: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOrionAccountTable;
|
||||
93
src/pages/OpenRoamingPage/GoogleOrionPage/ActionCell.tsx
Normal file
93
src/pages/OpenRoamingPage/GoogleOrionPage/ActionCell.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
HStack,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GoogleOrionAccount, useDeleteGoogleOrionAccount } from 'hooks/Network/GoogleOrion';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
type Props = {
|
||||
account: GoogleOrionAccount;
|
||||
openDetailsModal: (account: GoogleOrionAccount) => void;
|
||||
};
|
||||
|
||||
const GoogleOrionAccountActionCell = ({ account, openDetailsModal }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const deleteAccount = useDeleteGoogleOrionAccount();
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
|
||||
const onDelete = (onClose: () => void) => async () => {
|
||||
await deleteAccount.mutateAsync(account.id, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: t('roaming.account_deleted'),
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack spacing={2}>
|
||||
<Popover placement="start">
|
||||
{({ onClose, isOpen }) => (
|
||||
<>
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="334px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.delete')} {account.name}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>{t('crud.delete_confirm', { obj: t('roaming.account_one') })}</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={onDelete(onClose)} isLoading={deleteAccount.isLoading}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<Tooltip label={t('common.view_details')}>
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
onClick={() => openDetailsModal(account)}
|
||||
size="sm"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
colorScheme="blue"
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOrionAccountActionCell;
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as React from 'react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
ListItem,
|
||||
Tooltip,
|
||||
UnorderedList,
|
||||
} from '@chakra-ui/react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import FileInputButton from 'components/Buttons/FileInputButton';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const GoogleOrionCaCertificateField = ({ name, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [refreshId, setRefreshId] = React.useState(uuid());
|
||||
const field = useFastField<{ value: string; filename: string }[]>({ name: name ?? 'temporaryCerts' });
|
||||
|
||||
const onAdd = (v: string, file?: File) => {
|
||||
if (!file) return;
|
||||
|
||||
field.onChange([...field.value, { value: v, filename: file.name }]);
|
||||
setRefreshId(uuid());
|
||||
};
|
||||
|
||||
const onRemove = (index: number) => {
|
||||
field.onChange(field.value.filter((_: unknown, i: number) => i !== index));
|
||||
setRefreshId(uuid());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl id={name ?? 'temporaryCerts'} isDisabled={isDisabled} w="300px" isRequired isInvalid={field.isError}>
|
||||
<FormLabel>
|
||||
CA Certificates ({field.value?.length})
|
||||
{name ? null : (
|
||||
<Tooltip hasArrow label="Upload every file inside the 'cacerts' directory you have obtained from Google">
|
||||
<InfoIcon ml={2} mb="2px" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
<FileInputButton value="" setValue={onAdd} refreshId={refreshId} accept="" isStringFile />
|
||||
<FormErrorMessage>You need to upload at least one file to CA Certs</FormErrorMessage>
|
||||
</FormControl>
|
||||
<UnorderedList>
|
||||
{field.value.map((v: { filename: string }, i: number) => (
|
||||
<ListItem key={uuid()}>
|
||||
{v.filename}
|
||||
<Tooltip label={t('common.remove')}>
|
||||
<IconButton
|
||||
aria-label={t('common.remove')}
|
||||
icon={<Trash size={20} />}
|
||||
size="sm"
|
||||
ml={2}
|
||||
colorScheme="red"
|
||||
onClick={() => onRemove(i)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOrionCaCertificateField;
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import FileInputButton from 'components/Buttons/FileInputButton';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const GoogleOrionCertificateField = ({ isDisabled, name }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [refreshId, setRefreshId] = React.useState(uuid());
|
||||
const certificate = useFastField<string>({ name: name ?? 'certificate' });
|
||||
|
||||
React.useEffect(() => {
|
||||
setRefreshId(uuid());
|
||||
}, [certificate.value]);
|
||||
|
||||
return (
|
||||
<FormControl id="privateKey" isRequired isInvalid={certificate.isError} isDisabled={isDisabled}>
|
||||
<FormLabel>{t('certificates.certificate')}</FormLabel>
|
||||
<FileInputButton
|
||||
value={certificate.value}
|
||||
setValue={certificate.onChange}
|
||||
refreshId={refreshId}
|
||||
accept=".pem"
|
||||
isStringFile
|
||||
/>
|
||||
<FormErrorMessage>{certificate.error}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOrionCertificateField;
|
||||
@@ -0,0 +1,138 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Yup from 'yup';
|
||||
import GoogleOrionCaCertificateField from './CaCertificateField';
|
||||
import GoogleOrionCertificateField from './CertificateField';
|
||||
import GoogleOrionPrivateKeyField from './PrivateKeyField';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { testPemCertificate, testPemPrivateKey } from 'constants/formTests';
|
||||
import { CreateGoogleOrionAccountRequest, useCreateGoogleOrionAccount } from 'hooks/Network/GoogleOrion';
|
||||
import useFormModal from 'hooks/useFormModal';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
const CreateGoogleOrionAccountModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const [formKey, setFormKey] = React.useState(0);
|
||||
const { form, formRef } = useFormRef<
|
||||
CreateGoogleOrionAccountRequest & { temporaryCerts: { value: string; filename: string }[] }
|
||||
>();
|
||||
const modal = useFormModal({
|
||||
isDirty: form.dirty,
|
||||
onCloseSideEffect: () => {
|
||||
setFormKey((k) => k + 1);
|
||||
},
|
||||
});
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
const create = useCreateGoogleOrionAccount();
|
||||
|
||||
const onSubmit = async (
|
||||
data: CreateGoogleOrionAccountRequest & { temporaryCerts: { value: string; filename: string }[] },
|
||||
helpers: FormikHelpers<CreateGoogleOrionAccountRequest & { temporaryCerts: { value: string; filename: string }[] }>,
|
||||
) => {
|
||||
helpers.setSubmitting(true);
|
||||
|
||||
const finalData = data;
|
||||
finalData.cacerts = data.temporaryCerts.map((v) => v.value);
|
||||
|
||||
await create.mutateAsync(finalData, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: t('roaming.account_created'),
|
||||
});
|
||||
modal.closeCancelAndForm();
|
||||
helpers.resetForm();
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(t('form.required')),
|
||||
description: Yup.string(),
|
||||
notes: Yup.array().of(Yup.string()),
|
||||
privateKey: Yup.string()
|
||||
.test('test-key', t('roaming.invalid_key'), (v) => testPemPrivateKey(v, true))
|
||||
.required(t('form.required')),
|
||||
certificate: Yup.string()
|
||||
.test('test-certificate', t('roaming.invalid_certificate'), (v) => testPemCertificate(v, true))
|
||||
.required(t('form.required')),
|
||||
temporaryCerts: Yup.array().of(Yup.object()).min(1).required(t('form.required')),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const isFieldDisabled = create.isLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateButton onClick={modal.onOpen} />
|
||||
<Modal
|
||||
isOpen={modal.isOpen}
|
||||
onClose={modal.closeModal}
|
||||
title={`${t('common.create')} ${t('roaming.account_one')}`}
|
||||
topRightButtons={
|
||||
<SaveButton onClick={form.submitForm} isLoading={form.isSubmitting} isDisabled={!form.isValid} />
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Formik
|
||||
key={formKey}
|
||||
innerRef={formRef}
|
||||
initialValues={
|
||||
{
|
||||
name: '',
|
||||
description: '',
|
||||
notes: [],
|
||||
privateKey: '',
|
||||
certificate: '',
|
||||
cacerts: [],
|
||||
temporaryCerts: [],
|
||||
} as CreateGoogleOrionAccountRequest & { temporaryCerts: { value: string; filename: string }[] }
|
||||
}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Box>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
{t('roaming.account_one')} {t('common.details')}
|
||||
</Heading>
|
||||
<Flex my={2}>
|
||||
<StringField name="name" label={t('common.name')} isRequired isDisabled={isFieldDisabled} w="300px" />
|
||||
</Flex>
|
||||
<StringField name="Description" isArea h="80px" isDisabled={isFieldDisabled} />
|
||||
<Heading size="md" textDecoration="underline" mt={2}>
|
||||
Google Orion
|
||||
</Heading>
|
||||
<Flex my={2}>
|
||||
<Box w="300px">
|
||||
<GoogleOrionCertificateField isDisabled={isFieldDisabled} />
|
||||
</Box>
|
||||
<Box w="300px" ml={2}>
|
||||
<GoogleOrionPrivateKeyField isDisabled={isFieldDisabled} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<GoogleOrionCaCertificateField isDisabled={isFieldDisabled} />
|
||||
</Box>
|
||||
</Formik>
|
||||
</Box>
|
||||
</Modal>
|
||||
<ConfirmCloseAlert isOpen={modal.isConfirmOpen} confirm={modal.closeCancelAndForm} cancel={modal.closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateGoogleOrionAccountModal;
|
||||
161
src/pages/OpenRoamingPage/GoogleOrionPage/DetailsModal/index.tsx
Normal file
161
src/pages/OpenRoamingPage/GoogleOrionPage/DetailsModal/index.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, ListItem, Text, UnorderedList, UseDisclosureReturn, useBoolean } from '@chakra-ui/react';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ToggleEditButton from 'components/Buttons/ToggleEditButton';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import {
|
||||
GoogleOrionAccount,
|
||||
UpdateGoogleOrionAccountRequest,
|
||||
useUpdateGoogleOrionAccount,
|
||||
} from 'hooks/Network/GoogleOrion';
|
||||
import useFormModal from 'hooks/useFormModal';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
import CopyCell from 'pages/OpenRoamingPage/GlobalReachPage/DetailsModal/CopyCell';
|
||||
|
||||
type Props = {
|
||||
modalProps: UseDisclosureReturn;
|
||||
account: GoogleOrionAccount;
|
||||
};
|
||||
const GoogleOrionAccountEditModal = ({ modalProps, account }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [isEditing, setIsEditing] = useBoolean();
|
||||
const [formKey, setFormKey] = React.useState(uuid());
|
||||
const { form, formRef } = useFormRef<UpdateGoogleOrionAccountRequest>();
|
||||
const modal = useFormModal({
|
||||
isDirty: form.dirty || isEditing,
|
||||
onCloseSideEffect: () => {
|
||||
setFormKey(uuid());
|
||||
setIsEditing.off();
|
||||
modalProps.onClose();
|
||||
},
|
||||
});
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
const update = useUpdateGoogleOrionAccount();
|
||||
|
||||
const onSubmit = async (
|
||||
data: UpdateGoogleOrionAccountRequest,
|
||||
helpers: FormikHelpers<UpdateGoogleOrionAccountRequest>,
|
||||
) => {
|
||||
helpers.setSubmitting(true);
|
||||
|
||||
await update.mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: t('roaming.account_created'),
|
||||
});
|
||||
modal.closeCancelAndForm();
|
||||
helpers.resetForm();
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(t('form.required')),
|
||||
description: Yup.string(),
|
||||
notes: Yup.array().of(Yup.string()),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const isFieldDisabled = update.isLoading || !isEditing;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!modalProps.isOpen) return;
|
||||
|
||||
modal.onOpen();
|
||||
}, [modalProps.isOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isEditing]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={modal.isOpen}
|
||||
onClose={modal.closeModal}
|
||||
title={account.name}
|
||||
topRightButtons={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
<ToggleEditButton toggleEdit={setIsEditing.toggle} isEditing={isEditing} isDirty={form.dirty} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Formik
|
||||
key={formKey}
|
||||
innerRef={formRef}
|
||||
initialValues={{
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
description: account.description,
|
||||
// notes: account.notes,
|
||||
}}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Box>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
{t('roaming.account_one')} {t('common.details')}
|
||||
</Heading>
|
||||
<Flex my={2}>
|
||||
<StringField name="name" label={t('common.name')} isRequired isDisabled={isFieldDisabled} w="300px" />
|
||||
</Flex>
|
||||
<StringField name="description" isArea h="80px" isDisabled={isFieldDisabled} />
|
||||
<Heading size="md" textDecoration="underline" mt={2}>
|
||||
Google Orion
|
||||
</Heading>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Certificate:{' '}
|
||||
</Heading>
|
||||
<CopyCell value={account.certificate} />
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Private Key:{' '}
|
||||
</Heading>
|
||||
<CopyCell value={account.privateKey} />
|
||||
</Flex>
|
||||
<Heading size="sm">CA Certificates ({account.cacerts.length}):</Heading>
|
||||
<UnorderedList>
|
||||
{account.cacerts.map((v, i) => (
|
||||
<ListItem key={uuid()} display="flex" alignItems="center">
|
||||
<Text mr={2} my={2}>
|
||||
Certificate #{i}:
|
||||
</Text>
|
||||
<CopyCell key={uuid()} value={v} />
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</Box>
|
||||
</Formik>
|
||||
</Box>
|
||||
</Modal>
|
||||
<ConfirmCloseAlert isOpen={modal.isConfirmOpen} confirm={modal.closeCancelAndForm} cancel={modal.closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOrionAccountEditModal;
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import DetailsModal from '.';
|
||||
import { GoogleOrionAccount } from 'hooks/Network/GoogleOrion';
|
||||
|
||||
const useGoogleOrionAccountModal = () => {
|
||||
const [account, setAccount] = React.useState<GoogleOrionAccount>();
|
||||
const modalProps = useDisclosure();
|
||||
|
||||
const openModal = (newAcc: GoogleOrionAccount) => {
|
||||
setAccount(newAcc);
|
||||
modalProps.onOpen();
|
||||
};
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
openModal,
|
||||
modal: account ? <DetailsModal account={account} modalProps={modalProps} /> : null,
|
||||
}),
|
||||
[account, modalProps],
|
||||
);
|
||||
};
|
||||
|
||||
export default useGoogleOrionAccountModal;
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import FileInputButton from 'components/Buttons/FileInputButton';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const GoogleOrionPrivateKeyField = ({ name, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [refreshId, setRefreshId] = React.useState(uuid());
|
||||
const privateKey = useFastField<string>({ name: name ?? 'privateKey' });
|
||||
|
||||
React.useEffect(() => {
|
||||
setRefreshId(uuid());
|
||||
}, [privateKey.value]);
|
||||
|
||||
return (
|
||||
<FormControl id="privateKey" isRequired isInvalid={privateKey.isError} isDisabled={isDisabled}>
|
||||
<FormLabel>{t('roaming.private_key')}</FormLabel>
|
||||
<FileInputButton
|
||||
value={privateKey.value}
|
||||
setValue={privateKey.onChange}
|
||||
refreshId={refreshId}
|
||||
accept=".pem"
|
||||
isStringFile
|
||||
/>
|
||||
<FormErrorMessage>{privateKey.error}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOrionPrivateKeyField;
|
||||
6
src/pages/OpenRoamingPage/GoogleOrionPage/index.tsx
Normal file
6
src/pages/OpenRoamingPage/GoogleOrionPage/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import GoogleOrionAccountTable from './AccountTable';
|
||||
|
||||
const GoogleOrionPage = () => <GoogleOrionAccountTable />;
|
||||
|
||||
export default GoogleOrionPage;
|
||||
41
src/pages/OpenRoamingPage/index.tsx
Normal file
41
src/pages/OpenRoamingPage/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useColorMode } from '@chakra-ui/react';
|
||||
import GlobalReachAccountTable from './GlobalReachPage/AccountTable';
|
||||
import GoogleOrionPage from './GoogleOrionPage';
|
||||
|
||||
const OpenRoamingPage = () => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const isLight = colorMode === 'light';
|
||||
|
||||
const tabStyle = {
|
||||
textColor: isLight ? 'var(--chakra-colors-blue-600)' : 'var(--chakra-colors-blue-300)',
|
||||
fontWeight: 'semibold',
|
||||
borderWidth: '0px',
|
||||
marginBottom: '-1px',
|
||||
borderBottom: '2px solid',
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab _selected={tabStyle}>Global Reach</Tab>
|
||||
<Tab _selected={tabStyle}>Google Orion</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel px={0}>
|
||||
<Box w="100%">
|
||||
<GlobalReachAccountTable />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel px={0}>
|
||||
<Box w="100%">
|
||||
<GoogleOrionPage />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenRoamingPage;
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Box, Center, Flex, Heading, Link, Spacer, Spinner, useToast } from '@chakra-ui/react';
|
||||
import { Box, Center, Flex, HStack, Heading, Link, Spacer, Spinner } from '@chakra-ui/react';
|
||||
import { Form, Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -18,11 +18,11 @@ import { useUpdateAccount } from 'hooks/Network/Account';
|
||||
import useApiRequirements from 'hooks/useApiRequirements';
|
||||
import useFormModal from 'hooks/useFormModal';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
const FormSchema = (t: (str: string) => string, { passRegex }: { passRegex: string }) =>
|
||||
Yup.object().shape({
|
||||
firstName: Yup.string().required(t('form.required')),
|
||||
lastName: Yup.string().required(t('form.required')),
|
||||
name: Yup.string().required(t('form.required')),
|
||||
newPassword: Yup.string()
|
||||
.notRequired()
|
||||
.test('password', t('form.invalid_password'), (v) => testRegex(v, passRegex)),
|
||||
@@ -36,7 +36,7 @@ const FormSchema = (t: (str: string) => string, { passRegex }: { passRegex: stri
|
||||
|
||||
const GeneralInformationProfile = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
const { passwordPattern, passwordPolicyLink } = useApiRequirements();
|
||||
const { user } = useAuth();
|
||||
const updateUser = useUpdateAccount({});
|
||||
@@ -70,13 +70,15 @@ const GeneralInformationProfile = () => {
|
||||
<CardHeader>
|
||||
<Heading size="md">{t('profile.your_profile')}</Heading>
|
||||
<Spacer />
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} ml={2} />
|
||||
<HStack>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} />
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody display="block">
|
||||
{!user ? (
|
||||
@@ -86,8 +88,7 @@ const GeneralInformationProfile = () => {
|
||||
) : (
|
||||
<Formik<{
|
||||
description: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
name: string;
|
||||
newPassword?: string;
|
||||
}>
|
||||
key={formKey}
|
||||
@@ -95,12 +96,10 @@ const GeneralInformationProfile = () => {
|
||||
{
|
||||
email: user?.email,
|
||||
description: user?.description ?? '',
|
||||
firstName: user?.name.split(' ')[0] ?? '',
|
||||
lastName: user?.name.split(' ')[1] ?? '',
|
||||
name: user?.name ?? '',
|
||||
} as {
|
||||
description: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
name: string;
|
||||
newPassword?: string;
|
||||
}
|
||||
}
|
||||
@@ -108,35 +107,35 @@ const GeneralInformationProfile = () => {
|
||||
formRef as React.Ref<
|
||||
FormikProps<{
|
||||
description: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
name: string;
|
||||
newPassword?: string;
|
||||
}>
|
||||
>
|
||||
}
|
||||
validationSchema={FormSchema(t, { passRegex: passwordPattern })}
|
||||
onSubmit={async ({ description, firstName, lastName, newPassword }, { setSubmitting }) => {
|
||||
onSubmit={async ({ description, name, newPassword }, { setSubmitting }) => {
|
||||
await updateUser.mutateAsync(
|
||||
{
|
||||
id: user?.id,
|
||||
description,
|
||||
name: `${firstName} ${lastName}`,
|
||||
name,
|
||||
currentPassword: newPassword,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSubmitting(false);
|
||||
closeCancelAndForm();
|
||||
toast({
|
||||
successToast({
|
||||
id: 'account-update-success',
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_update_obj', {
|
||||
obj: t('profile.your_profile'),
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
apiErrorToast({
|
||||
id: 'account-update-error',
|
||||
e,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -145,29 +144,16 @@ const GeneralInformationProfile = () => {
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<StringField name="email" label={t('common.email')} isDisabled />
|
||||
<Flex my={4}>
|
||||
<StringField
|
||||
name="firstName"
|
||||
label={t('contacts.first_name')}
|
||||
isDisabled={isSubmitting || !isEditing}
|
||||
isRequired
|
||||
/>
|
||||
<Flex>
|
||||
<StringField name="email" label={t('common.email')} isDisabled />
|
||||
<Box w={8} />
|
||||
<StringField
|
||||
name="lastName"
|
||||
label={t('contacts.last_name')}
|
||||
name="name"
|
||||
label={t('common.name')}
|
||||
isDisabled={isSubmitting || !isEditing}
|
||||
isRequired
|
||||
/>
|
||||
</Flex>
|
||||
<StringField
|
||||
h="100px"
|
||||
name="description"
|
||||
label={t('profile.about_me')}
|
||||
isDisabled={isSubmitting || !isEditing}
|
||||
isArea
|
||||
/>
|
||||
<Flex my={4}>
|
||||
<StringField
|
||||
name="newPassword"
|
||||
@@ -185,6 +171,13 @@ const GeneralInformationProfile = () => {
|
||||
hideButton
|
||||
/>
|
||||
</Flex>
|
||||
<StringField
|
||||
h="100px"
|
||||
name="description"
|
||||
label={t('profile.about_me')}
|
||||
isDisabled={isSubmitting || !isEditing}
|
||||
isArea
|
||||
/>
|
||||
<Box w="100%" mt={4} textAlign="right">
|
||||
<Link href={passwordPolicyLink} isExternal>
|
||||
{t('login.password_policy')}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Yup from 'yup';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ToggleField from 'components/FormFields/ToggleField';
|
||||
import { GlobalReachAccount } from 'hooks/Network/GlobalReach';
|
||||
import { GoogleOrionAccount } from 'hooks/Network/GoogleOrion';
|
||||
import {
|
||||
RADIUS_ENDPOINT_POOL_STRATEGIES,
|
||||
RADIUS_ENDPOINT_TYPES,
|
||||
useGetRadiusEndpoints,
|
||||
} from 'hooks/Network/RadiusEndpoints';
|
||||
|
||||
const testString = (v: string | undefined) => {
|
||||
try {
|
||||
if (!v) return false;
|
||||
const split = v.split('.');
|
||||
|
||||
if (split.length !== 4) {
|
||||
return false;
|
||||
}
|
||||
if (split[0] !== '0' || split[1] !== '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const num1 = parseInt(split[2] ?? '0', 10);
|
||||
const num2 = parseInt(split[3] ?? '0', 10);
|
||||
|
||||
if (!num1 || num1 < 1 || num1 > 2) {
|
||||
return false;
|
||||
}
|
||||
if (!num2 || num2 < 1 || num2 > 254) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
formRef: React.Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
finishStep: (v: Record<string, unknown>) => void;
|
||||
orionAccounts: GoogleOrionAccount[];
|
||||
globalReachAccounts: GlobalReachAccount[];
|
||||
};
|
||||
|
||||
const CreateRadiusEndpointDetailsStep = ({ formRef, finishStep, orionAccounts, globalReachAccounts }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const getAccounts = useGetRadiusEndpoints();
|
||||
|
||||
const testIndexIsUnused = React.useCallback(
|
||||
(v: string | undefined) => {
|
||||
if (!v) return false;
|
||||
if (!getAccounts.data) return true;
|
||||
|
||||
return !getAccounts.data.find((a) => a.Index === v);
|
||||
},
|
||||
[getAccounts.data],
|
||||
);
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object().shape({
|
||||
name: Yup.string()
|
||||
.min(3, 'Must be between 3 and 32 characters')
|
||||
.max(32, 'Must be between 3 and 32 characters')
|
||||
.required(t('form.required'))
|
||||
.default(''),
|
||||
description: Yup.string().default(''),
|
||||
Type: Yup.string()
|
||||
.oneOf(RADIUS_ENDPOINT_TYPES as unknown as string[])
|
||||
.required(t('form.required'))
|
||||
.default('generic'),
|
||||
PoolStrategy: Yup.string()
|
||||
.oneOf(RADIUS_ENDPOINT_POOL_STRATEGIES as unknown as string[])
|
||||
.required(t('form.required'))
|
||||
.default('random'),
|
||||
UseGWProxy: Yup.boolean().default(true),
|
||||
Index: Yup.string()
|
||||
.test('index-value', 'Must be between 0.0.1.1 and 0.0.2.254', testString)
|
||||
.test('unique-index', 'Index must be unique and not used by other endpoints', testIndexIsUnused)
|
||||
.required(t('form.required'))
|
||||
.default('0.0.1.1'),
|
||||
NasIdentifier: Yup.string().default(''),
|
||||
AccountingInterval: Yup.number().min(0).max(65535).default(60),
|
||||
}),
|
||||
[t, testIndexIsUnused],
|
||||
);
|
||||
|
||||
type FormValues = Yup.InferType<typeof FormSchema>;
|
||||
|
||||
const initialValues: FormValues = FormSchema.cast({});
|
||||
|
||||
const typeOptions = React.useMemo(() => {
|
||||
const options = [
|
||||
{ value: 'generic', label: 'Generic' },
|
||||
{ value: 'radsec', label: 'RadSec' },
|
||||
];
|
||||
|
||||
if (orionAccounts.length > 0) {
|
||||
options.push({ value: 'orion', label: 'Google Orion' });
|
||||
}
|
||||
|
||||
if (globalReachAccounts.length > 0) {
|
||||
options.push({ value: 'globalreach', label: 'Global Reach' });
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [t, orionAccounts, globalReachAccounts]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef as (instance: FormikProps<FormValues> | null) => void}
|
||||
initialValues={initialValues}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={(values: FormValues) => {
|
||||
finishStep(values);
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
{t('common.details')}
|
||||
</Heading>
|
||||
<Flex mb={2}>
|
||||
<StringField name="name" label={t('common.name')} isRequired maxW="400px" />
|
||||
</Flex>
|
||||
<Box mb={2}>
|
||||
<StringField name="description" label={t('common.description')} h="80px" isArea />
|
||||
</Box>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
Endpoint
|
||||
</Heading>
|
||||
<SelectField name="Type" label="Endpoint Type" w="max-content" options={typeOptions} isRequired />
|
||||
<Flex my={2}>
|
||||
<Box>
|
||||
<StringField name="Index" label="IP Index" isRequired />
|
||||
</Box>
|
||||
<Box mx={4}>
|
||||
<SelectField
|
||||
name="PoolStrategy"
|
||||
label="Pool Strategy"
|
||||
w="140px"
|
||||
options={[
|
||||
{ value: 'random', label: 'Random' },
|
||||
{ value: 'round_robin', label: 'Round-Robin' },
|
||||
{ value: 'weighted', label: 'Weighted' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
</Box>
|
||||
<ToggleField name="UseGWProxy" label="Use Gateway Proxy" />
|
||||
</Flex>
|
||||
<Flex my={2}>
|
||||
<StringField name="NasIdentifier" label="NAS Identifier" w="300px" />
|
||||
<Box mx={4}>
|
||||
<NumberField name="AccountingInterval" label="Accounting Interval" w="120px" isRequired unit="s" />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRadiusEndpointDetailsStep;
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Radio, RadioGroup, Stack } from '@chakra-ui/react';
|
||||
import { GlobalReachCertificate } from 'hooks/Network/GlobalReach';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
certificates: GlobalReachCertificate[];
|
||||
name: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const GlobalReachAccountField = ({ certificates, name, isDisabled }: Props) => {
|
||||
const field = useFastField<{ UseOpenRoamingAccount: string; Weight: number }[]>({ name });
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<RadioGroup
|
||||
isDisabled={isDisabled}
|
||||
value={field.value[0]?.UseOpenRoamingAccount}
|
||||
onChange={(v) =>
|
||||
field.onChange([
|
||||
{
|
||||
UseOpenRoamingAccount: v,
|
||||
Weight: 0,
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
{certificates.map((certificate) => (
|
||||
<Radio value={certificate.id} key={certificate.id}>
|
||||
{certificate.name}
|
||||
</Radio>
|
||||
))}
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalReachAccountField;
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Center, Heading, Select, Spinner } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Yup from 'yup';
|
||||
import GlobalReachAccountField from './GlobalReachAccountField';
|
||||
import { GlobalReachAccount, useGetGlobalReachCertificates } from 'hooks/Network/GlobalReach';
|
||||
|
||||
type Props = {
|
||||
formRef: React.Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
finishStep: (v: Record<string, unknown>) => void;
|
||||
accounts: GlobalReachAccount[];
|
||||
};
|
||||
|
||||
const CreateRadiusEndpointGlobalReachStep = ({ formRef, finishStep, accounts }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedAccount, setSelectedAccount] = React.useState<GlobalReachAccount>(accounts[0] as GlobalReachAccount);
|
||||
const getCertificates = useGetGlobalReachCertificates(selectedAccount.id);
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object()
|
||||
.shape({
|
||||
Accounts: Yup.array()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
UseOpenRoamingAccount: Yup.string().required(t('form.required')).default(''),
|
||||
Weight: Yup.number().required(t('form.required')).min(0).max(500).default(0),
|
||||
}),
|
||||
)
|
||||
.min(1, 'Must select at least one account')
|
||||
.required(t('form.required'))
|
||||
.default([]),
|
||||
})
|
||||
.default([]),
|
||||
[t],
|
||||
);
|
||||
|
||||
type FormValues = Yup.InferType<typeof FormSchema>;
|
||||
|
||||
const initialValues: FormValues = FormSchema.cast({});
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef as (instance: FormikProps<FormValues> | null) => void}
|
||||
initialValues={initialValues}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={async (values: FormValues) => {
|
||||
await finishStep({
|
||||
RadsecServers: values.Accounts,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Box>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
What Global Reach account would like to use?
|
||||
</Heading>
|
||||
<Select
|
||||
mb={2}
|
||||
value={selectedAccount.id}
|
||||
onChange={(e) => {
|
||||
const found = accounts.find((account) => account.id === e.target.value);
|
||||
if (found) {
|
||||
setSelectedAccount(found);
|
||||
}
|
||||
}}
|
||||
w="max-content"
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
Please choose one or more certificates to use:
|
||||
</Heading>
|
||||
<Box>
|
||||
{getCertificates.data ? (
|
||||
<GlobalReachAccountField certificates={getCertificates.data} name="Accounts" isDisabled={isSubmitting} />
|
||||
) : (
|
||||
<Center my={8}>
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRadiusEndpointGlobalReachStep;
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Radio, RadioGroup, Stack } from '@chakra-ui/react';
|
||||
import { GoogleOrionAccount } from 'hooks/Network/GoogleOrion';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
accounts: GoogleOrionAccount[];
|
||||
name: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const OrionAccountField = ({ accounts, name, isDisabled }: Props) => {
|
||||
const field = useFastField<{ UseOpenRoamingAccount: string; Weight: number }[]>({ name });
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<RadioGroup
|
||||
isDisabled={isDisabled}
|
||||
value={field.value[0]?.UseOpenRoamingAccount}
|
||||
onChange={(v) =>
|
||||
field.onChange([
|
||||
{
|
||||
UseOpenRoamingAccount: v,
|
||||
Weight: 0,
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
{accounts.map((account) => (
|
||||
<Radio value={account.id} key={account.id}>
|
||||
{account.name} {account.description ? ` - ${account.description}` : ''}
|
||||
</Radio>
|
||||
))}
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrionAccountField;
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Heading } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Yup from 'yup';
|
||||
import OrionAccountField from './OrionAccountField';
|
||||
import { GoogleOrionAccount } from 'hooks/Network/GoogleOrion';
|
||||
|
||||
type Props = {
|
||||
formRef: React.Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
finishStep: (v: Record<string, unknown>) => void;
|
||||
accounts: GoogleOrionAccount[];
|
||||
};
|
||||
|
||||
const CreateRadiusEndpointOrionStep = ({ formRef, finishStep, accounts }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object()
|
||||
.shape({
|
||||
Accounts: Yup.array()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
UseOpenRoamingAccount: Yup.string().required(t('form.required')).default(''),
|
||||
Weight: Yup.number().required(t('form.required')).min(0).max(500).default(0),
|
||||
}),
|
||||
)
|
||||
.min(1, 'Must select at least one account')
|
||||
.required(t('form.required'))
|
||||
.default([]),
|
||||
})
|
||||
.default([]),
|
||||
[t],
|
||||
);
|
||||
|
||||
type FormValues = Yup.InferType<typeof FormSchema>;
|
||||
|
||||
const initialValues: FormValues = FormSchema.cast({});
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef as (instance: FormikProps<FormValues> | null) => void}
|
||||
initialValues={initialValues}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={async (values: FormValues) => {
|
||||
await finishStep({
|
||||
RadsecServers: values.Accounts,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Box>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
Please choose one or more Google Orion accounts you would like to use:
|
||||
</Heading>
|
||||
<OrionAccountField accounts={accounts} name="Accounts" isDisabled={isSubmitting} />
|
||||
</Box>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRadiusEndpointOrionStep;
|
||||
@@ -0,0 +1,149 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Center } from '@chakra-ui/react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { Formik } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import RadiusServerForm from './RadiusServerForm';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import { testIpv4, testIpv6 } from 'constants/formTests';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
|
||||
type Props = {
|
||||
setValue: (field: string, value: object, shouldValidate?: boolean | undefined) => void;
|
||||
value: object[];
|
||||
};
|
||||
|
||||
const RadiusEndpointServerForm = ({ setValue, value }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { form, formRef } = useFormRef();
|
||||
const [key, setFormKey] = React.useState(uuid());
|
||||
const onAdd = (v: object) => {
|
||||
setValue('RadiusServers', [...value, v]);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormKey(uuid());
|
||||
};
|
||||
|
||||
const RadiusServerSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object().shape({
|
||||
Hostname: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.matches(
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$/,
|
||||
'Invalid hostname, please check the format (ex.: example.com)',
|
||||
),
|
||||
IP: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.test(
|
||||
'test-ipv4-ipv6',
|
||||
'Invalid IP address, needs to be either IPv4 or IPv6',
|
||||
(v) => testIpv4(v) || testIpv6(v),
|
||||
),
|
||||
Port: Yup.number().required(t('form.required')).min(1).max(65535).default(1812),
|
||||
Secret: Yup.string().required(t('form.required')),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const RadiusEndpointServerSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object()
|
||||
.shape({
|
||||
Authentication: Yup.array().of(RadiusServerSchema).min(1).required(t('form.required')),
|
||||
Accounting: Yup.array().of(RadiusServerSchema).min(1).required(t('form.required')),
|
||||
CoA: Yup.array().of(RadiusServerSchema).min(1).required(t('form.required')),
|
||||
AccountingInterval: Yup.number().required(t('form.required')).min(10).max(500).default(60),
|
||||
})
|
||||
.default({
|
||||
Authentication: [
|
||||
{
|
||||
Hostname: '',
|
||||
IP: '',
|
||||
Port: 1812,
|
||||
Secret: '',
|
||||
},
|
||||
],
|
||||
Accounting: [
|
||||
{
|
||||
Hostname: '',
|
||||
IP: '',
|
||||
Port: 1813,
|
||||
Secret: '',
|
||||
},
|
||||
],
|
||||
CoA: [
|
||||
{
|
||||
Hostname: '',
|
||||
IP: '',
|
||||
Port: 1814,
|
||||
Secret: '',
|
||||
},
|
||||
],
|
||||
AccountingInterval: 60,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
key={key}
|
||||
innerRef={formRef}
|
||||
initialValues={{
|
||||
Authentication: [
|
||||
{
|
||||
Hostname: '',
|
||||
IP: '',
|
||||
Port: 1812,
|
||||
Secret: '',
|
||||
},
|
||||
],
|
||||
Accounting: [
|
||||
{
|
||||
Hostname: '',
|
||||
IP: '',
|
||||
Port: 1813,
|
||||
Secret: '',
|
||||
},
|
||||
],
|
||||
CoA: [
|
||||
{
|
||||
Hostname: '',
|
||||
IP: '',
|
||||
Port: 1814,
|
||||
Secret: '',
|
||||
},
|
||||
],
|
||||
AccountingInterval: 60,
|
||||
}}
|
||||
validateOnMount
|
||||
validationSchema={RadiusEndpointServerSchema}
|
||||
onSubmit={async (values) => {
|
||||
onAdd(values);
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<NumberField name="AccountingInterval" label="Accounting Interval" isRequired w="120px" />
|
||||
<RadiusServerForm label="Authentication" namePrefix="Authentication" />
|
||||
<RadiusServerForm label="Accounting" namePrefix="Accounting" />
|
||||
<RadiusServerForm label="Change of Authorization" namePrefix="CoA" />
|
||||
<Center my={8}>
|
||||
<Button
|
||||
onClick={form.submitForm}
|
||||
isDisabled={!form.isValid}
|
||||
colorScheme="blue"
|
||||
rightIcon={<Plus size={20} />}
|
||||
>
|
||||
Add Server
|
||||
</Button>
|
||||
</Center>
|
||||
</Box>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadiusEndpointServerForm;
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Flex, Heading } from '@chakra-ui/react';
|
||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
type Props = {
|
||||
namePrefix: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const RadiusServerForm = ({ namePrefix, label }: Props) => {
|
||||
const field = useFastField<{ Hostname: string; IP: string; Port: number; Secret: string }[]>({ name: namePrefix });
|
||||
|
||||
const name = (postfix: string, index: number) => `${namePrefix}[${index}].${postfix}]`;
|
||||
|
||||
const onAdd = () => {
|
||||
const newValue = [...field.value];
|
||||
newValue.push({ Hostname: '', IP: '', Port: 1815, Secret: '' });
|
||||
field.onChange(newValue);
|
||||
};
|
||||
const onRemove = (index: number) => {
|
||||
const newValue = [...field.value];
|
||||
newValue.splice(index, 1);
|
||||
field.onChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex mt={4} mb={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
{label} ({field.value.length})
|
||||
</Heading>
|
||||
<Button onClick={onAdd} colorScheme="blue" size="sm">
|
||||
Add New Entry
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box>
|
||||
{field.value.map((_: unknown, i: number) => (
|
||||
<Box key={i} borderWidth={1} borderRadius="15px" p={4} mb={4}>
|
||||
<Flex alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
#{i}
|
||||
</Heading>
|
||||
<DeleteButton onClick={() => onRemove(i)} isDisabled={field.value.length === 1} />
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Box w="300px">
|
||||
<StringField name={name('Hostname', i)} label="Hostname" isRequired />
|
||||
</Box>
|
||||
<Box w="240px" mx={4}>
|
||||
<StringField name={name('IP', i)} label="IP" isRequired />
|
||||
</Box>
|
||||
<Box>
|
||||
<NumberField name={name('Port', i)} label="Port" w="120px" isRequired />
|
||||
</Box>
|
||||
</Flex>
|
||||
<StringField name={name('Secret', i)} label="Secret" hideButton w="300px" isRequired />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RadiusServerForm);
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import RadiusEndpointServerForm from './RadiusEndpointServerForm';
|
||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||
|
||||
type Props = {
|
||||
formRef: React.Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
finishStep: (v: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
const CreateRadiusEndpointRadiusStep = ({ formRef, finishStep }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object()
|
||||
.shape({
|
||||
RadiusServers: Yup.array()
|
||||
.of(Yup.object())
|
||||
.min(1, 'Must select at least one account')
|
||||
.required(t('form.required'))
|
||||
.default([]),
|
||||
})
|
||||
.default({
|
||||
RadiusServers: [],
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
type FormValues = Yup.InferType<typeof FormSchema>;
|
||||
|
||||
const initialValues: FormValues = FormSchema.cast({});
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef as (instance: FormikProps<FormValues> | null) => void}
|
||||
initialValues={initialValues}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={async (values: FormValues) => {
|
||||
await finishStep({
|
||||
RadiusServers: values.RadiusServers,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{({ setFieldValue, values }) => (
|
||||
<Box>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
Please input the information of one or more generic radius servers:
|
||||
</Heading>
|
||||
<RadiusEndpointServerForm setValue={setFieldValue} value={values.RadiusServers} />
|
||||
<Heading mt={8} mb={4} size="md" textDecoration="underline">
|
||||
Servers ({values.RadiusServers.length})
|
||||
</Heading>
|
||||
<UnorderedList>
|
||||
{values.RadiusServers.map((v: { Authentication: { Hostname: string }[] }) => (
|
||||
<ListItem key={uuid()} display="flex" alignItems="center">
|
||||
<Heading size="sm">{v.Authentication[0]?.Hostname}</Heading>
|
||||
<DeleteButton
|
||||
ml={4}
|
||||
onClick={() => {
|
||||
setFieldValue(
|
||||
'RadiusServers',
|
||||
values.RadiusServers.filter(
|
||||
(s: { Authentication: { Hostname: string }[] }) =>
|
||||
s.Authentication[0]?.Hostname !== v.Authentication[0]?.Hostname,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</Box>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRadiusEndpointRadiusStep;
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import RadiusEndpointServerForm from './RadsecEndpointServerForm';
|
||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||
|
||||
type Props = {
|
||||
formRef: React.Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
finishStep: (v: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
const CreateRadiusEndpointRadsecStep = ({ formRef, finishStep }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object()
|
||||
.shape({
|
||||
RadsecServers: Yup.array()
|
||||
.of(Yup.object())
|
||||
.min(1, 'Must select at least one account')
|
||||
.required(t('form.required'))
|
||||
.default([]),
|
||||
})
|
||||
.default({
|
||||
RadsecServers: [],
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
type FormValues = Yup.InferType<typeof FormSchema>;
|
||||
|
||||
const initialValues: FormValues = FormSchema.cast({});
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef as (instance: FormikProps<FormValues> | null) => void}
|
||||
initialValues={initialValues}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={async (values: FormValues) => {
|
||||
await finishStep({
|
||||
RadsecServers: values.RadsecServers,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{({ setFieldValue, values }) => (
|
||||
<Box>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
Please input the information of one or more Radsec Servers:
|
||||
</Heading>
|
||||
<RadiusEndpointServerForm setValue={setFieldValue} value={values.RadsecServers} />
|
||||
<Heading mt={8} mb={4} size="md" textDecoration="underline">
|
||||
Radsec Servers ({values.RadsecServers.length})
|
||||
</Heading>
|
||||
<UnorderedList>
|
||||
{values.RadsecServers.map((v: { Hostname: string }) => (
|
||||
<ListItem key={uuid()} display="flex" alignItems="center">
|
||||
<Heading size="sm">{v.Hostname}</Heading>
|
||||
<DeleteButton
|
||||
ml={4}
|
||||
onClick={() => {
|
||||
setFieldValue(
|
||||
'RadsecServers',
|
||||
values.RadsecServers.filter((s: { Hostname: string }) => s.Hostname !== v.Hostname),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</Box>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRadiusEndpointRadsecStep;
|
||||
@@ -0,0 +1,135 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Center, Flex, Heading } from '@chakra-ui/react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { Formik } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import NumberField from 'components/FormFields/NumberField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ToggleField from 'components/FormFields/ToggleField';
|
||||
import { testPemCertificate, testPemPrivateKey } from 'constants/formTests';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import GoogleOrionCaCertificateField from 'pages/OpenRoamingPage/GoogleOrionPage/CaCertificateField';
|
||||
import GoogleOrionCertificateField from 'pages/OpenRoamingPage/GoogleOrionPage/CertificateField';
|
||||
import GoogleOrionPrivateKeyField from 'pages/OpenRoamingPage/GoogleOrionPage/PrivateKeyField';
|
||||
|
||||
type Props = {
|
||||
setValue: (field: string, value: object, shouldValidate?: boolean | undefined) => void;
|
||||
value: object[];
|
||||
};
|
||||
|
||||
const RadsecEndpointServerForm = ({ setValue, value }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { form, formRef } = useFormRef();
|
||||
const [key, setFormKey] = React.useState(uuid());
|
||||
const onAdd = (v: object) => {
|
||||
setValue('RadsecServers', [...value, v]);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormKey(uuid());
|
||||
};
|
||||
|
||||
const RadiusEndpointServerSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object()
|
||||
.shape({
|
||||
Weight: Yup.number().required(t('form.required')).min(1).max(100).default(1),
|
||||
Hostname: Yup.string().required(t('form.required')),
|
||||
IP: Yup.string(),
|
||||
Port: Yup.number().required(t('form.required')).min(1).max(65535).default(2083),
|
||||
Secret: Yup.string().required(t('form.required')),
|
||||
PrivateKey: Yup.string()
|
||||
.test('test-key', t('roaming.invalid_key'), (v) => testPemPrivateKey(v, true))
|
||||
.required(t('form.required'))
|
||||
.default(''),
|
||||
Certificate: Yup.string()
|
||||
.test('test-certificate', t('roaming.invalid_certificate'), (v) => testPemCertificate(v, true))
|
||||
.required(t('form.required'))
|
||||
.default(''),
|
||||
CaCerts: Yup.array().of(Yup.object()).min(1).required(t('form.required')).default([]),
|
||||
AllowSelfSigned: Yup.boolean().required(t('form.required')).default(false),
|
||||
})
|
||||
.default({
|
||||
Weight: 1,
|
||||
Hostname: '',
|
||||
IP: '',
|
||||
Port: 2083,
|
||||
Secret: 'radsec',
|
||||
Certificate: '',
|
||||
PrivateKey: '',
|
||||
CaCerts: [],
|
||||
AllowSelfSigned: false,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
key={key}
|
||||
innerRef={formRef}
|
||||
initialValues={RadiusEndpointServerSchema.cast({})}
|
||||
validateOnMount
|
||||
validationSchema={RadiusEndpointServerSchema}
|
||||
onSubmit={async (values) => {
|
||||
const cleanedCaCerts = values.CaCerts.map((cert: { value: string }) => cert.value);
|
||||
onAdd({
|
||||
...values,
|
||||
CaCerts: cleanedCaCerts,
|
||||
});
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Heading mb={4} size="sm">
|
||||
Radsec Server
|
||||
</Heading>
|
||||
<Flex mb={4}>
|
||||
<Box w="300px">
|
||||
<StringField name="Hostname" label="Hostname" isRequired />
|
||||
</Box>
|
||||
<Box w="240px" mx={4}>
|
||||
<StringField name="IP" label="IP" />
|
||||
</Box>
|
||||
<Box>
|
||||
<NumberField name="Port" label="Port" w="120px" isRequired />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mb={4}>
|
||||
<Box>
|
||||
<StringField name="Secret" label="Secret" hideButton w="300px" isRequired />
|
||||
</Box>
|
||||
<Box ml={4}>
|
||||
<ToggleField name="AllowSelfSigned" label="Allow Self Signed" />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Heading mb={4} size="sm">
|
||||
Certificates
|
||||
</Heading>
|
||||
<Flex mb={2}>
|
||||
<Box w="300px">
|
||||
<GoogleOrionCertificateField name="Certificate" />
|
||||
</Box>
|
||||
<Box w="300px" ml={2}>
|
||||
{' '}
|
||||
<GoogleOrionPrivateKeyField name="PrivateKey" />
|
||||
</Box>
|
||||
</Flex>
|
||||
<GoogleOrionCaCertificateField name="CaCerts" />
|
||||
<Center mt={4} mb={8}>
|
||||
<Button
|
||||
onClick={form.submitForm}
|
||||
isDisabled={!form.isValid}
|
||||
colorScheme="blue"
|
||||
rightIcon={<Plus size={20} />}
|
||||
>
|
||||
Add Server
|
||||
</Button>
|
||||
</Center>
|
||||
</Box>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadsecEndpointServerForm;
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as React from 'react';
|
||||
import { Box, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { ArrowLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateRadiusEndpointDetailsStep from './DetailsStep';
|
||||
import CreateRadiusEndpointGlobalReachStep from './GlobalReach/GlobalReachStep';
|
||||
import CreateRadiusEndpointOrionStep from './GoogleOrion/OrionStep';
|
||||
import CreateRadiusEndpointRadiusStep from './Radius/RadiusStep';
|
||||
import CreateRadiusEndpointRadsecStep from './Radsec/RadiusStep';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import StepButton from 'components/Buttons/StepButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { GlobalReachAccount } from 'hooks/Network/GlobalReach';
|
||||
import { GoogleOrionAccount } from 'hooks/Network/GoogleOrion';
|
||||
import { RadiusEndpoint, useCreateRadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import useFormModal from 'hooks/useFormModal';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
const DEFAULT_DATA = {
|
||||
data: {},
|
||||
step: 0,
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
orionAccounts: GoogleOrionAccount[];
|
||||
globalReachAccounts: GlobalReachAccount[];
|
||||
};
|
||||
|
||||
const CreateRadiusEndpointModal = ({ orionAccounts, globalReachAccounts }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [data, setData] = React.useState<{
|
||||
data: Partial<RadiusEndpoint>;
|
||||
step: 0 | 1;
|
||||
}>(DEFAULT_DATA);
|
||||
const { form, formRef } = useFormRef();
|
||||
const onReset = () => {
|
||||
setData({ ...DEFAULT_DATA });
|
||||
};
|
||||
const modal = useFormModal({
|
||||
isDirty: form.dirty || Object.keys(data).length > 0,
|
||||
onCloseSideEffect() {
|
||||
onReset();
|
||||
},
|
||||
});
|
||||
const create = useCreateRadiusEndpoint();
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
|
||||
const handleNextStep = React.useCallback(
|
||||
async (newData: Partial<RadiusEndpoint>) => {
|
||||
if (data.step === 0) {
|
||||
setData({
|
||||
data: {
|
||||
...data.data,
|
||||
...newData,
|
||||
},
|
||||
step: 1,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.step === 1) {
|
||||
await create.mutateAsync(
|
||||
// @ts-ignore
|
||||
{ ...data.data, ...newData },
|
||||
{
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: 'Radius Endpoint created',
|
||||
});
|
||||
modal.closeCancelAndForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
[data],
|
||||
);
|
||||
|
||||
const body = () => {
|
||||
if (data.step === 0) {
|
||||
return (
|
||||
<CreateRadiusEndpointDetailsStep
|
||||
formRef={formRef}
|
||||
finishStep={handleNextStep}
|
||||
orionAccounts={orionAccounts}
|
||||
globalReachAccounts={globalReachAccounts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const type = data.data.Type ?? 'generic';
|
||||
|
||||
if (type === 'orion') {
|
||||
return <CreateRadiusEndpointOrionStep accounts={orionAccounts} formRef={formRef} finishStep={handleNextStep} />;
|
||||
}
|
||||
if (type === 'globalreach') {
|
||||
return (
|
||||
<CreateRadiusEndpointGlobalReachStep
|
||||
accounts={globalReachAccounts}
|
||||
formRef={formRef}
|
||||
finishStep={handleNextStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (type === 'generic') {
|
||||
return <CreateRadiusEndpointRadiusStep formRef={formRef} finishStep={handleNextStep} />;
|
||||
}
|
||||
|
||||
return <CreateRadiusEndpointRadsecStep formRef={formRef} finishStep={handleNextStep} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateButton onClick={modal.onOpen} />
|
||||
<Modal
|
||||
title="Create Radius Endpoint"
|
||||
isOpen={modal.isOpen}
|
||||
onClose={modal.closeModal}
|
||||
topRightButtons={
|
||||
<>
|
||||
<Tooltip label={t('common.reset')}>
|
||||
<IconButton
|
||||
aria-label={t('common.reset')}
|
||||
onClick={onReset}
|
||||
icon={<ArrowLeft size={20} />}
|
||||
isDisabled={data.step === 0}
|
||||
/>
|
||||
</Tooltip>
|
||||
<StepButton
|
||||
onNext={form.submitForm}
|
||||
currentStep={data.step}
|
||||
lastStep={1}
|
||||
isLoading={create.isLoading}
|
||||
isDisabled={!form.isValid}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box>{body()}</Box>
|
||||
</Modal>
|
||||
<ConfirmCloseAlert isOpen={modal.isConfirmOpen} confirm={modal.closeCancelAndForm} cancel={modal.closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateRadiusEndpointModal;
|
||||
@@ -0,0 +1,146 @@
|
||||
/* eslint-disable max-len */
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Heading,
|
||||
IconButton,
|
||||
Tag,
|
||||
TagLeftIcon,
|
||||
Text,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { CloudArrowUp, Warning } from '@phosphor-icons/react';
|
||||
import RefreshButton from 'components/Buttons/RefreshButton';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { useGetRadiusEndpointLastGwUpdate, useUpdateRadiusEndpointsOnGateway } from 'hooks/Network/RadiusEndpoints';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
const LastRadiusEndpointUpdateButton = () => {
|
||||
const getLastUpdate = useGetRadiusEndpointLastGwUpdate();
|
||||
const triggerUpdate = useUpdateRadiusEndpointsOnGateway();
|
||||
const { apiErrorToast, successToast } = useNotification();
|
||||
const modalProps = useDisclosure();
|
||||
|
||||
const lastUpdateInfo = React.useMemo(() => {
|
||||
if (getLastUpdate.data) {
|
||||
const lastUpdate =
|
||||
getLastUpdate.data.lastUpdate === 0 ? 'Never' : <FormattedDate date={getLastUpdate.data.lastUpdate} />;
|
||||
const lastConfigurationChange =
|
||||
getLastUpdate.data.lastConfigurationChange === 0 ? (
|
||||
'Never'
|
||||
) : (
|
||||
<FormattedDate date={getLastUpdate.data.lastConfigurationChange} />
|
||||
);
|
||||
|
||||
const isUpToDate =
|
||||
getLastUpdate.data.lastUpdate !== 0 &&
|
||||
getLastUpdate.data.lastConfigurationChange === getLastUpdate.data.lastUpdate;
|
||||
|
||||
return {
|
||||
lastUpdate,
|
||||
lastConfigurationChange,
|
||||
isUpToDate,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lastUpdate: '-',
|
||||
lastConfigurationChange: '-',
|
||||
isUpToDate: true,
|
||||
};
|
||||
}, [getLastUpdate.data]);
|
||||
|
||||
const onTrigger = async () => {
|
||||
await triggerUpdate.mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: 'Initiated update of all Radius Endpoints on the gateway',
|
||||
});
|
||||
modalProps.onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
apiErrorToast({ e });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
getLastUpdate.refetch();
|
||||
modalProps.onOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
label={
|
||||
lastUpdateInfo.isUpToDate
|
||||
? 'The RADIUS configuration of your controller matches your RADIUS endpoints'
|
||||
: 'The RADIUS configuration of your controller does not match your RADIUS endpoints. This means your RADIUS configurations on your controller might not work as expected'
|
||||
}
|
||||
>
|
||||
<Tag colorScheme={lastUpdateInfo.isUpToDate ? 'green' : 'yellow'} size="lg">
|
||||
<TagLeftIcon as={Warning} hidden={lastUpdateInfo.isUpToDate} />
|
||||
<Text mr={2}>Last Update:</Text>
|
||||
{lastUpdateInfo.lastUpdate}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip label="Update Controller" closeOnClick={false}>
|
||||
<IconButton aria-label="Update" onClick={onOpen} colorScheme="purple" icon={<CloudArrowUp size={20} />} />
|
||||
</Tooltip>
|
||||
<Modal
|
||||
{...modalProps}
|
||||
title="Update Endpoints"
|
||||
options={
|
||||
{
|
||||
// modalSize: 'sm',
|
||||
}
|
||||
}
|
||||
topRightButtons={<RefreshButton onClick={getLastUpdate.refetch} isFetching={getLastUpdate.isFetching} />}
|
||||
>
|
||||
<Box>
|
||||
{!lastUpdateInfo.isUpToDate ? (
|
||||
<Alert status="warning" mb={4}>
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertDescription>
|
||||
The RADIUS configuration of your controller does not match your RADIUS endpoints. This means your
|
||||
RADIUS configurations on your controller might not work as expected
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Heading size="sm">Last Provisioning change: {lastUpdateInfo.lastConfigurationChange}</Heading>
|
||||
<Heading size="sm">Last Controller update: {lastUpdateInfo.lastUpdate}</Heading>
|
||||
<Alert status="error" mt={4}>
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
Updating the Controller with the latest RADIUS endpoint values may cause some RADIUS disruption for 1-2
|
||||
minutes
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
<Center mt={4}>
|
||||
<Button onClick={modalProps.onClose} ml={-2} mr={2}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button ml={2} colorScheme="red" onClick={onTrigger} isLoading={triggerUpdate.isLoading}>
|
||||
Proceed
|
||||
</Button>
|
||||
</Center>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LastRadiusEndpointUpdateButton;
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
|
||||
const prettyTypes = {
|
||||
orion: 'Google Orion',
|
||||
globalreach: 'Global Reach',
|
||||
generic: 'Generic',
|
||||
radsec: 'RadSec',
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
endpoint?: RadiusEndpoint;
|
||||
};
|
||||
|
||||
const RadiusEndpointSummary = ({ endpoint }: Props) =>
|
||||
!endpoint ? null : (
|
||||
<Box borderWidth={1} borderRadius="15px" p={4} my={2}>
|
||||
<Flex alignItems="center">
|
||||
<Heading w="100px" size="sm">
|
||||
Name:
|
||||
</Heading>
|
||||
<Box>{endpoint.name}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<Heading w="100px" size="sm">
|
||||
Description:
|
||||
</Heading>
|
||||
<Box>{endpoint.description}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<Heading w="100px" size="sm">
|
||||
Type:
|
||||
</Heading>
|
||||
<Box>{prettyTypes[endpoint.Type]}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default RadiusEndpointSummary;
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
HStack,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RadiusEndpoint, useDeleteRadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
|
||||
type Props = {
|
||||
endpoint: RadiusEndpoint;
|
||||
onEdit: (endpoint: RadiusEndpoint) => void;
|
||||
};
|
||||
|
||||
const RadiusEndpointActions = ({ endpoint, onEdit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const deleteEndpoint = useDeleteRadiusEndpoint();
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
|
||||
const onDelete = (onClose: () => void) => async () => {
|
||||
await deleteEndpoint.mutateAsync(endpoint.id, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: 'Endpoint deleted',
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack spacing={2}>
|
||||
<Popover placement="start">
|
||||
{({ onClose, isOpen }) => (
|
||||
<>
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="350px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.delete')} {endpoint.name}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>{t('crud.delete_confirm', { obj: 'radius endpoint' })}</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={onDelete(onClose)} isLoading={deleteEndpoint.isLoading}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<Tooltip label={t('common.view_details')}>
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
onClick={() => onEdit(endpoint)}
|
||||
size="sm"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
colorScheme="blue"
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadiusEndpointActions;
|
||||
@@ -0,0 +1,82 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, Text } from '@chakra-ui/react';
|
||||
import GlobalReachEndpointDetails from './GlobalReachEndpointDetails';
|
||||
import GoogleOrionEndpointDetails from './GoogleOrionEndpointDetails';
|
||||
import RadiusEndpointDetails from './RadiusEndpointDetails';
|
||||
import RadsecEndpointDetails from './RadsecEndpointDetails';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import { uppercaseFirstLetter } from 'utils/stringHelper';
|
||||
|
||||
const firstColProps = {
|
||||
w: '160px',
|
||||
mr: 4,
|
||||
} as const;
|
||||
const secondColProps = {} as const;
|
||||
|
||||
const prettyTypes = {
|
||||
orion: 'Google Orion',
|
||||
globalreach: 'Global Reach',
|
||||
generic: 'Generic',
|
||||
radsec: 'RadSec',
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
endpoint: RadiusEndpoint;
|
||||
};
|
||||
|
||||
const EndpointDisplay = ({ endpoint }: Props) => {
|
||||
const furtherDetails = () => {
|
||||
if (endpoint.Type === 'orion') return <GoogleOrionEndpointDetails endpoint={endpoint} />;
|
||||
if (endpoint.Type === 'generic') return <RadiusEndpointDetails endpoint={endpoint} />;
|
||||
if (endpoint.Type === 'radsec') return <RadsecEndpointDetails endpoint={endpoint} />;
|
||||
if (endpoint.Type === 'globalreach') return <GlobalReachEndpointDetails endpoint={endpoint} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Heading mb={4} size="md" textDecoration="underline">
|
||||
Endpoint
|
||||
</Heading>
|
||||
<Flex>
|
||||
<Heading {...firstColProps} size="sm">
|
||||
Type:
|
||||
</Heading>
|
||||
<Text {...secondColProps}>{prettyTypes[endpoint.Type]}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Heading {...firstColProps} size="sm">
|
||||
IP Index:
|
||||
</Heading>
|
||||
<Text {...secondColProps}>{endpoint.Index}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Heading {...firstColProps} size="sm">
|
||||
Pool Strategy:
|
||||
</Heading>
|
||||
<Text {...secondColProps}>{uppercaseFirstLetter(endpoint.PoolStrategy)}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Heading {...firstColProps} size="sm">
|
||||
Gateway Proxy:
|
||||
</Heading>
|
||||
<Text {...secondColProps}>{endpoint.UseGWProxy ? 'On' : 'Off'}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Heading {...firstColProps} size="sm">
|
||||
NAS Identifier:
|
||||
</Heading>
|
||||
<Text {...secondColProps}>{endpoint.NasIdentifier}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Heading {...firstColProps} size="sm">
|
||||
Accounting Interval:
|
||||
</Heading>
|
||||
<Text {...secondColProps}>{endpoint.AccountingInterval}s</Text>
|
||||
</Flex>
|
||||
{furtherDetails()}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndpointDisplay;
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, AlertDescription, AlertIcon, Box, Flex, Heading, Text } from '@chakra-ui/react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import { useGetSelectedGlobalReachCertificates } from 'hooks/Network/GlobalReach';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import CopyCell from 'pages/OpenRoamingPage/GlobalReachPage/DetailsModal/CopyCell';
|
||||
|
||||
const copyCell = (value: string) => <CopyCell value={value} key={uuid()} isCompact />;
|
||||
|
||||
type Props = {
|
||||
endpoint: RadiusEndpoint;
|
||||
};
|
||||
|
||||
const GlobalReachEndpointDetails = ({ endpoint }: Props) => {
|
||||
const getCertificates = useGetSelectedGlobalReachCertificates({
|
||||
certIds: endpoint.RadsecServers.map((v) => v.UseOpenRoamingAccount),
|
||||
});
|
||||
|
||||
const certificate = getCertificates.data?.[0];
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
Global Reach Certificate
|
||||
</Heading>{' '}
|
||||
{certificate ? (
|
||||
<Box>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Name:{' '}
|
||||
</Heading>
|
||||
<Text>{certificate.name}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Created:{' '}
|
||||
</Heading>
|
||||
<FormattedDate date={certificate.created} />
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Expiry:{' '}
|
||||
</Heading>
|
||||
<FormattedDate date={certificate.expiresAt} />
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Certificate:{' '}
|
||||
</Heading>
|
||||
{copyCell(certificate.certificate)}
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Cert. Chain:{' '}
|
||||
</Heading>
|
||||
{copyCell(certificate.certificateChain)}
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
CSR:{' '}
|
||||
</Heading>
|
||||
{copyCell(certificate.csr)}
|
||||
</Flex>
|
||||
</Box>
|
||||
) : (
|
||||
<Alert status="warning" mt={4}>
|
||||
<AlertIcon /> <AlertDescription>Cannot retrieve certificate information for now</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalReachEndpointDetails;
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useGetGoogleOrionAccounts } from 'hooks/Network/GoogleOrion';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import CopyCell from 'pages/OpenRoamingPage/GlobalReachPage/DetailsModal/CopyCell';
|
||||
|
||||
type Props = {
|
||||
endpoint: RadiusEndpoint;
|
||||
};
|
||||
|
||||
const GoogleOrionEndpointDetails = ({ endpoint }: Props) => {
|
||||
const getAccounts = useGetGoogleOrionAccounts();
|
||||
|
||||
const account = getAccounts.data?.find(
|
||||
({ id }) => endpoint.RadsecServers.find((server) => server.UseOpenRoamingAccount === id) !== undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
Google Orion Account
|
||||
</Heading>
|
||||
{account ? (
|
||||
<Box mt={2}>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Certificate:{' '}
|
||||
</Heading>
|
||||
<CopyCell value={account.certificate} />
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Private Key:{' '}
|
||||
</Heading>
|
||||
<CopyCell value={account.privateKey} />
|
||||
</Flex>
|
||||
<Heading size="sm">CA Certificates ({account.cacerts.length}):</Heading>
|
||||
<UnorderedList>
|
||||
{account.cacerts.map((v, i) => (
|
||||
<ListItem key={uuid()} display="flex" alignItems="center">
|
||||
<Text mr={2} my={2}>
|
||||
Certificate #{i}:
|
||||
</Text>
|
||||
<CopyCell key={uuid()} value={v} />
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</Box>
|
||||
) : (
|
||||
'No account found'
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOrionEndpointDetails;
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { RadiusEndpoint, RadiusServer } from 'hooks/Network/RadiusEndpoints';
|
||||
|
||||
const SectionDisplay = ({ Hostname, IP, Port, Secret }: RadiusServer) => (
|
||||
<Box>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Hostname:
|
||||
</Heading>
|
||||
<Text>{Hostname}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
IP:
|
||||
</Heading>
|
||||
<Text>{IP}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Port:
|
||||
</Heading>
|
||||
<Text>{Port}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Secret:
|
||||
</Heading>
|
||||
<Text>{Secret}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
endpoint: RadiusEndpoint;
|
||||
};
|
||||
|
||||
const RadiusEndpointDetails = ({ endpoint }: Props) => (
|
||||
<Box mt={2}>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
Servers
|
||||
</Heading>
|
||||
<Accordion allowToggle defaultIndex={[0]} mt={2}>
|
||||
{endpoint.RadiusServers.map((server) => (
|
||||
<AccordionItem key={uuid()}>
|
||||
<AccordionButton>
|
||||
<Heading size="sm">{server.Authentication.map((data) => data.Hostname).join(', ')}</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Accounting Interval:{' '}
|
||||
</Heading>
|
||||
<Text>{server.AccountingInterval}s</Text>
|
||||
</Flex>
|
||||
<Heading size="md" my={2} textDecoration="underline">
|
||||
Authentication
|
||||
</Heading>
|
||||
{server.Authentication.map((data) => (
|
||||
<SectionDisplay {...data} />
|
||||
))}
|
||||
<Heading size="md" my={2} textDecoration="underline">
|
||||
Accounting
|
||||
</Heading>
|
||||
{server.Accounting.map((data) => (
|
||||
<SectionDisplay {...data} />
|
||||
))}
|
||||
<Heading size="md" my={2} textDecoration="underline">
|
||||
Change of Authorization (CoA)
|
||||
</Heading>
|
||||
{server.CoA.map((data) => (
|
||||
<SectionDisplay {...data} />
|
||||
))}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default RadiusEndpointDetails;
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
ListItem,
|
||||
Text,
|
||||
UnorderedList,
|
||||
} from '@chakra-ui/react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import CopyCell from 'pages/OpenRoamingPage/GlobalReachPage/DetailsModal/CopyCell';
|
||||
|
||||
type Props = {
|
||||
endpoint: RadiusEndpoint;
|
||||
};
|
||||
|
||||
const RadsecEndpointDetails = ({ endpoint }: Props) => (
|
||||
<Box mt={2}>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
RadSec Servers
|
||||
</Heading>
|
||||
<Accordion allowToggle defaultIndex={[0]} mt={2}>
|
||||
{endpoint.RadsecServers.map((server) => (
|
||||
<AccordionItem key={uuid()}>
|
||||
<AccordionButton>
|
||||
<Heading size="sm">{server.Hostname}</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
IP Address:{' '}
|
||||
</Heading>
|
||||
<Text>{server.IP}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Port:{' '}
|
||||
</Heading>
|
||||
<Text>{server.Port}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Secret:{' '}
|
||||
</Heading>
|
||||
<Text>{server.Secret}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Allow Self-Signed:{' '}
|
||||
</Heading>
|
||||
<Text>{server.AllowSelfSigned ? 'Yes' : 'No'}</Text>
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Certificate:{' '}
|
||||
</Heading>
|
||||
<CopyCell value={server.Certificate} />
|
||||
</Flex>
|
||||
<Flex my={2} alignItems="center">
|
||||
<Heading size="sm" mr={2}>
|
||||
Private Key:{' '}
|
||||
</Heading>
|
||||
<CopyCell value={server.PrivateKey} />
|
||||
</Flex>
|
||||
<Heading size="sm">CA Certificates ({server.CaCerts.length}):</Heading>
|
||||
<UnorderedList>
|
||||
{server.CaCerts.map((v, i) => (
|
||||
<ListItem key={uuid()} display="flex" alignItems="center">
|
||||
<Text mr={2} my={2}>
|
||||
Certificate #{i}:
|
||||
</Text>
|
||||
<CopyCell key={uuid()} value={v} />
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default RadsecEndpointDetails;
|
||||
@@ -0,0 +1,142 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, UseDisclosureReturn, useBoolean } from '@chakra-ui/react';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import EndpointDisplay from './EndpointDisplay';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ToggleEditButton from 'components/Buttons/ToggleEditButton';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { RadiusEndpoint, useUpdateRadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
import useFormModal from 'hooks/useFormModal';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
import { AtLeast } from 'models/General';
|
||||
|
||||
type Props = {
|
||||
endpoint: RadiusEndpoint;
|
||||
modalProps: UseDisclosureReturn;
|
||||
hideEdit?: boolean;
|
||||
};
|
||||
|
||||
const ViewDetailsModal = ({ endpoint, modalProps, hideEdit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [isEditing, setIsEditing] = useBoolean();
|
||||
const [formKey, setFormKey] = React.useState(uuid());
|
||||
const { form, formRef } = useFormRef<AtLeast<RadiusEndpoint, 'id'>>();
|
||||
const modal = useFormModal({
|
||||
isDirty: form.dirty || isEditing,
|
||||
onCloseSideEffect: () => {
|
||||
setFormKey(uuid());
|
||||
setIsEditing.off();
|
||||
modalProps.onClose();
|
||||
},
|
||||
});
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
const update = useUpdateRadiusEndpoint();
|
||||
|
||||
const onSubmit = async (
|
||||
data: AtLeast<RadiusEndpoint, 'id'>,
|
||||
helpers: FormikHelpers<AtLeast<RadiusEndpoint, 'id'>>,
|
||||
) => {
|
||||
helpers.setSubmitting(true);
|
||||
|
||||
await update.mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
description: t('roaming.account_created'),
|
||||
});
|
||||
modal.closeCancelAndForm();
|
||||
helpers.resetForm();
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
apiErrorToast({ e: error });
|
||||
helpers.setSubmitting(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const FormSchema = React.useMemo(
|
||||
() =>
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(t('form.required')),
|
||||
description: Yup.string(),
|
||||
notes: Yup.array().of(Yup.string()),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const isFieldDisabled = update.isLoading || !isEditing;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!modalProps.isOpen) return;
|
||||
|
||||
modal.onOpen();
|
||||
}, [modalProps.isOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isEditing]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={modal.isOpen}
|
||||
onClose={modal.closeModal}
|
||||
title={endpoint.name}
|
||||
topRightButtons={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
{!hideEdit ? (
|
||||
<ToggleEditButton toggleEdit={setIsEditing.toggle} isEditing={isEditing} isDirty={form.dirty} />
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Formik
|
||||
key={formKey}
|
||||
innerRef={formRef}
|
||||
initialValues={{
|
||||
id: endpoint.id,
|
||||
name: endpoint.name,
|
||||
description: endpoint.description,
|
||||
}}
|
||||
validateOnMount
|
||||
validationSchema={FormSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Box>
|
||||
<Heading size="md" textDecoration="underline">
|
||||
{t('roaming.account_one')} {t('common.details')}
|
||||
</Heading>
|
||||
<Flex my={2}>
|
||||
<StringField name="name" label={t('common.name')} isRequired isDisabled={isFieldDisabled} w="300px" />
|
||||
</Flex>
|
||||
<StringField
|
||||
name="description"
|
||||
label={t('common.description')}
|
||||
isArea
|
||||
h="80px"
|
||||
isDisabled={isFieldDisabled}
|
||||
/>
|
||||
</Box>
|
||||
</Formik>
|
||||
<EndpointDisplay endpoint={endpoint} />
|
||||
</Box>
|
||||
</Modal>
|
||||
<ConfirmCloseAlert isOpen={modal.isConfirmOpen} confirm={modal.closeCancelAndForm} cancel={modal.closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewDetailsModal;
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import DetailsModal from '.';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
|
||||
type Props = {
|
||||
hideEdit?: boolean;
|
||||
};
|
||||
|
||||
const useRadiusEndpointAccountModal = ({ hideEdit }: Props) => {
|
||||
const [endpoint, setEndpoint] = React.useState<RadiusEndpoint>();
|
||||
const modalProps = useDisclosure();
|
||||
|
||||
const openModal = (newEndpoint: RadiusEndpoint) => {
|
||||
setEndpoint(newEndpoint);
|
||||
modalProps.onOpen();
|
||||
};
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
openModal,
|
||||
modal: endpoint ? <DetailsModal endpoint={endpoint} modalProps={modalProps} hideEdit={hideEdit} /> : null,
|
||||
}),
|
||||
[endpoint, modalProps],
|
||||
);
|
||||
};
|
||||
|
||||
export default useRadiusEndpointAccountModal;
|
||||
156
src/pages/SystemConfigurationPage/RadiusEndpoints/index.tsx
Normal file
156
src/pages/SystemConfigurationPage/RadiusEndpoints/index.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import * as React from 'react';
|
||||
import { Tag } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateRadiusEndpointModal from './CreateModal';
|
||||
import LastRadiusEndpointUpdateButton from './LastUpdateButton';
|
||||
import RadiusEndpointActions from './TableActions';
|
||||
import { useRadiusEndpointsTable } from './useRadiusEndpointsTable';
|
||||
import useRadiusEndpointAccountModal from './ViewDetailsModal/useEditModal';
|
||||
import { DataGrid } from 'components/DataGrid';
|
||||
import { DataGridColumn } from 'components/DataGrid/useDataGrid';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import { useGetGlobalReachAccounts } from 'hooks/Network/GlobalReach';
|
||||
import { useGetGoogleOrionAccounts } from 'hooks/Network/GoogleOrion';
|
||||
import { RadiusEndpoint } from 'hooks/Network/RadiusEndpoints';
|
||||
|
||||
const dateCell = (date: number) => <FormattedDate date={date} />;
|
||||
const actionCell = (row: RadiusEndpoint, open: (acc: RadiusEndpoint) => void) => (
|
||||
<RadiusEndpointActions endpoint={row} onEdit={open} />
|
||||
);
|
||||
const typeCell = (row: RadiusEndpoint) => {
|
||||
let colorScheme = 'blue';
|
||||
if (row.Type === 'orion') colorScheme = 'purple';
|
||||
if (row.Type === 'globalreach') colorScheme = 'green';
|
||||
if (row.Type === 'radsec') colorScheme = 'teal';
|
||||
|
||||
return (
|
||||
<Tag colorScheme={colorScheme} size="md">
|
||||
{row.Type}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const RadiusEndpointsManagement = () => {
|
||||
const { t } = useTranslation();
|
||||
const table = useRadiusEndpointsTable({
|
||||
tableSettingsId: 'system.radiusEndpoints.table',
|
||||
});
|
||||
const getOrionAccounts = useGetGoogleOrionAccounts();
|
||||
const getGlobalReachAccounts = useGetGlobalReachAccounts();
|
||||
const modal = useRadiusEndpointAccountModal({});
|
||||
|
||||
const columns: DataGridColumn<RadiusEndpoint>[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'name',
|
||||
header: t('common.name'),
|
||||
accessorKey: 'name',
|
||||
meta: {
|
||||
alwaysShow: true,
|
||||
anchored: true,
|
||||
customWidth: '120px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Type',
|
||||
header: t('common.type'),
|
||||
accessorKey: 'Type',
|
||||
cell: (cell) => typeCell(cell.row.original),
|
||||
meta: {
|
||||
customWidth: '120px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Index',
|
||||
header: 'Index',
|
||||
accessorKey: 'Index',
|
||||
meta: {
|
||||
customWidth: '80px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'PoolStrategy',
|
||||
header: t('openroaming.pool_strategy'),
|
||||
accessorKey: 'PoolStrategy',
|
||||
meta: {
|
||||
customWidth: '120px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'created',
|
||||
header: t('common.created'),
|
||||
|
||||
accessorKey: 'created',
|
||||
cell: (cell) => dateCell(cell.row.original.created),
|
||||
meta: {
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'modified',
|
||||
header: t('common.modified'),
|
||||
|
||||
accessorKey: 'modified',
|
||||
cell: (cell) => dateCell(cell.row.original.modified),
|
||||
meta: {
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
header: t('common.description'),
|
||||
accessorKey: 'description',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
accessorKey: 'id',
|
||||
cell: (cell) => actionCell(cell.row.original, modal.openModal),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
customWidth: '80px',
|
||||
alwaysShow: true,
|
||||
columnSelectorOptions: {
|
||||
label: t('common.actions'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal.modal}
|
||||
<DataGrid<RadiusEndpoint>
|
||||
controller={table.controller}
|
||||
header={{
|
||||
title: t('openroaming.radius_endpoint_other'),
|
||||
objectListed: t('openroaming.radius_endpoint_other'),
|
||||
addButton: (
|
||||
<CreateRadiusEndpointModal
|
||||
// @ts-ignore
|
||||
orionAccounts={getOrionAccounts.data ?? []}
|
||||
// @ts-ignore
|
||||
globalReachAccounts={getGlobalReachAccounts.data ?? []}
|
||||
/>
|
||||
),
|
||||
otherButtons: <LastRadiusEndpointUpdateButton />,
|
||||
}}
|
||||
columns={columns}
|
||||
data={table.getRadiusEndpoints.data ?? []}
|
||||
isLoading={table.getRadiusEndpoints.isFetching}
|
||||
options={{
|
||||
refetch: table.getRadiusEndpoints.refetch,
|
||||
showAsCard: true,
|
||||
onRowClick: (row) => () => modal.openModal(row),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadiusEndpointsManagement;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useDataGrid } from 'components/DataGrid/useDataGrid';
|
||||
import { useGetRadiusEndpoints } from 'hooks/Network/RadiusEndpoints';
|
||||
|
||||
export type UseRadiusEndpointsTableProps = {
|
||||
tableSettingsId: string;
|
||||
};
|
||||
export const useRadiusEndpointsTable = ({ tableSettingsId }: UseRadiusEndpointsTableProps) => {
|
||||
const controller = useDataGrid({
|
||||
tableSettingsId,
|
||||
defaultSortBy: [{ id: 'name', desc: false }],
|
||||
defaultOrder: ['name'],
|
||||
});
|
||||
const getRadiusEndpoints = useGetRadiusEndpoints();
|
||||
const editModalProps = useDisclosure();
|
||||
|
||||
return {
|
||||
controller,
|
||||
getRadiusEndpoints,
|
||||
editModalProps,
|
||||
};
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '../../components/Modals/Modal';
|
||||
import { Modal } from '../../../components/Modals/Modal';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import { useCreateSystemSecret } from 'hooks/Network/Secrets';
|
||||
52
src/pages/SystemConfigurationPage/SystemSecrets/index.tsx
Normal file
52
src/pages/SystemConfigurationPage/SystemSecrets/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
BackgroundProps,
|
||||
EffectProps,
|
||||
Heading,
|
||||
InteractivityProps,
|
||||
LayoutProps,
|
||||
PositionProps,
|
||||
SpaceProps,
|
||||
Spacer,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemSecretCreateButton from './CreateButton';
|
||||
import SystemSecretsTable from './Table';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
interface SystemConfigurationPageProps
|
||||
extends LayoutProps,
|
||||
SpaceProps,
|
||||
BackgroundProps,
|
||||
InteractivityProps,
|
||||
PositionProps,
|
||||
EffectProps {}
|
||||
|
||||
const SystemSecrets = ({ ...props }: SystemConfigurationPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
|
||||
if (!user || user.userRole !== 'root') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto">
|
||||
{t('system.secrets')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<SystemSecretCreateButton />
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<SystemSecretsTable />
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSecrets;
|
||||
@@ -1,51 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
BackgroundProps,
|
||||
EffectProps,
|
||||
Heading,
|
||||
InteractivityProps,
|
||||
LayoutProps,
|
||||
PositionProps,
|
||||
SpaceProps,
|
||||
Spacer,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemSecretCreateButton from './CreateButton';
|
||||
import SystemSecretsTable from './Table';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import RadiusEndpointsManagement from './RadiusEndpoints';
|
||||
import SystemSecrets from './SystemSecrets';
|
||||
|
||||
interface SystemConfigurationPageProps
|
||||
extends LayoutProps,
|
||||
SpaceProps,
|
||||
BackgroundProps,
|
||||
InteractivityProps,
|
||||
PositionProps,
|
||||
EffectProps {}
|
||||
|
||||
const SystemConfigurationPage = ({ ...props }: SystemConfigurationPageProps) => {
|
||||
const SystemConfigurationPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
|
||||
if (!user || user.userRole !== 'root') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto">
|
||||
{t('system.secrets')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<SystemSecretCreateButton />
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<SystemSecretsTable />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>{t('openroaming.radius_endpoint_other')}</Tab>
|
||||
<Tab>{t('system.secrets')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel px={0}>
|
||||
<RadiusEndpointsManagement />
|
||||
</TabPanel>
|
||||
<TabPanel px={0}>
|
||||
<SystemSecrets />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user