mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Update site fonts (#208)
This commit is contained in:
399
package-lock.json
generated
399
package-lock.json
generated
@@ -44,6 +44,7 @@
|
||||
"react-redux": "^8.0.2",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-select": "^5.7.3",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-table": "^7.7.0",
|
||||
"redux": "^4.2.0",
|
||||
@@ -2327,6 +2328,49 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||
"integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.16.7",
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/hash": "^0.9.1",
|
||||
"@emotion/memoize": "^0.8.1",
|
||||
"@emotion/serialize": "^1.1.2",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"convert-source-map": "^1.5.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"find-root": "^1.1.0",
|
||||
"source-map": "^0.5.7",
|
||||
"stylis": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
|
||||
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin/node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/cache": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
|
||||
"integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.8.1",
|
||||
"@emotion/sheet": "^1.2.2",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"@emotion/weak-memoize": "^0.3.1",
|
||||
"stylis": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
@@ -2341,9 +2385,59 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/memoize": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
|
||||
"integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
|
||||
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
|
||||
},
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz",
|
||||
"integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/serialize": "^1.1.2",
|
||||
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"@emotion/weak-memoize": "^0.3.1",
|
||||
"hoist-non-react-statics": "^3.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/serialize": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz",
|
||||
"integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==",
|
||||
"dependencies": {
|
||||
"@emotion/hash": "^0.9.1",
|
||||
"@emotion/memoize": "^0.8.1",
|
||||
"@emotion/unitless": "^0.8.1",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/serialize/node_modules/@emotion/hash": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
|
||||
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
|
||||
},
|
||||
"node_modules/@emotion/serialize/node_modules/@emotion/unitless": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
|
||||
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
|
||||
},
|
||||
"node_modules/@emotion/sheet": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
|
||||
"integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
|
||||
},
|
||||
"node_modules/@emotion/stylis": {
|
||||
"version": "0.8.5",
|
||||
@@ -2355,6 +2449,24 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||
},
|
||||
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
|
||||
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/utils": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
|
||||
"integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
|
||||
},
|
||||
"node_modules/@emotion/weak-memoize": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
|
||||
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
|
||||
@@ -2418,6 +2530,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.0.tgz",
|
||||
"integrity": "sha512-vX1WVAdPjZg9DkDkC+zEx/tKtnST6/qcNpwcjeBgco3XRNHz5PUA+ivi/yr6G3o0kMR60uKBJcfOdfzOFI7PMQ=="
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.3.0.tgz",
|
||||
"integrity": "sha512-qIAwejE3r6NeA107u4ELDKkH8+VtgRKdXqtSPaKflL2S2V+doyN+Wt9s5oHKXPDo4E8TaVXaHT3+6BbagH31xw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.2.tgz",
|
||||
@@ -3967,6 +4092,14 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
|
||||
"integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@@ -6712,6 +6845,15 @@
|
||||
"utila": "~0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||
@@ -8026,6 +8168,11 @@
|
||||
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@@ -10692,6 +10839,11 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
@@ -13906,6 +14058,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-select": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.3.tgz",
|
||||
"integrity": "sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.0",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.8.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"use-isomorphic-layout-effect": "^1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "15.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
|
||||
@@ -13941,6 +14113,21 @@
|
||||
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -15168,9 +15355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
|
||||
"integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
||||
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
@@ -15975,6 +16162,19 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
|
||||
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
@@ -18425,6 +18625,48 @@
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz",
|
||||
"integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ=="
|
||||
},
|
||||
"@emotion/babel-plugin": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||
"integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.16.7",
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/hash": "^0.9.1",
|
||||
"@emotion/memoize": "^0.8.1",
|
||||
"@emotion/serialize": "^1.1.2",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"convert-source-map": "^1.5.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"find-root": "^1.1.0",
|
||||
"source-map": "^0.5.7",
|
||||
"stylis": "4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/hash": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
|
||||
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@emotion/cache": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
|
||||
"integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
|
||||
"requires": {
|
||||
"@emotion/memoize": "^0.8.1",
|
||||
"@emotion/sheet": "^1.2.2",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"@emotion/weak-memoize": "^0.3.1",
|
||||
"stylis": "4.2.0"
|
||||
}
|
||||
},
|
||||
"@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
@@ -18439,9 +18681,53 @@
|
||||
}
|
||||
},
|
||||
"@emotion/memoize": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
|
||||
"integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
|
||||
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
|
||||
},
|
||||
"@emotion/react": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz",
|
||||
"integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/serialize": "^1.1.2",
|
||||
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"@emotion/weak-memoize": "^0.3.1",
|
||||
"hoist-non-react-statics": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"@emotion/serialize": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz",
|
||||
"integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==",
|
||||
"requires": {
|
||||
"@emotion/hash": "^0.9.1",
|
||||
"@emotion/memoize": "^0.8.1",
|
||||
"@emotion/unitless": "^0.8.1",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"csstype": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/hash": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
|
||||
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
|
||||
},
|
||||
"@emotion/unitless": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
|
||||
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@emotion/sheet": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
|
||||
"integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
|
||||
},
|
||||
"@emotion/stylis": {
|
||||
"version": "0.8.5",
|
||||
@@ -18453,6 +18739,22 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||
},
|
||||
"@emotion/use-insertion-effect-with-fallbacks": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
|
||||
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@emotion/utils": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
|
||||
"integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
|
||||
},
|
||||
"@emotion/weak-memoize": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
|
||||
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
|
||||
@@ -18497,6 +18799,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@floating-ui/core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.0.tgz",
|
||||
"integrity": "sha512-vX1WVAdPjZg9DkDkC+zEx/tKtnST6/qcNpwcjeBgco3XRNHz5PUA+ivi/yr6G3o0kMR60uKBJcfOdfzOFI7PMQ=="
|
||||
},
|
||||
"@floating-ui/dom": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.3.0.tgz",
|
||||
"integrity": "sha512-qIAwejE3r6NeA107u4ELDKkH8+VtgRKdXqtSPaKflL2S2V+doyN+Wt9s5oHKXPDo4E8TaVXaHT3+6BbagH31xw==",
|
||||
"requires": {
|
||||
"@floating-ui/core": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"@headlessui/react": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.2.tgz",
|
||||
@@ -19689,6 +20004,14 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
|
||||
"integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@@ -21720,6 +22043,15 @@
|
||||
"utila": "~0.4"
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||
@@ -22698,6 +23030,11 @@
|
||||
"pkg-dir": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
|
||||
},
|
||||
"find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@@ -24637,6 +24974,11 @@
|
||||
"fs-monkey": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
@@ -26755,6 +27097,22 @@
|
||||
"workbox-webpack-plugin": "^6.4.1"
|
||||
}
|
||||
},
|
||||
"react-select": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.3.tgz",
|
||||
"integrity": "sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.0",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.8.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"use-isomorphic-layout-effect": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"react-syntax-highlighter": {
|
||||
"version": "15.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
|
||||
@@ -26780,6 +27138,17 @@
|
||||
"integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -27694,9 +28063,9 @@
|
||||
}
|
||||
},
|
||||
"stylis": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
|
||||
"integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
||||
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
@@ -28302,6 +28671,12 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"use-isomorphic-layout-effect": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
|
||||
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
|
||||
"requires": {}
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"react-redux": "^8.0.2",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-select": "^5.7.3",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-table": "^7.7.0",
|
||||
"redux": "^4.2.0",
|
||||
|
||||
@@ -450,7 +450,7 @@ const AccessControlEdit = () => {
|
||||
{policy && (
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Breadcrumb
|
||||
style={{marginBottom: "30px"}}
|
||||
style={{marginBottom: "25px"}}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Access Control</a>,
|
||||
@@ -488,6 +488,7 @@ const AccessControlEdit = () => {
|
||||
margin: "0px",
|
||||
marginBottom: "10px",
|
||||
cursor: "pointer",
|
||||
fontWeight:"500"
|
||||
}}
|
||||
onDoubleClick={() => toggleEditName(true)}
|
||||
>
|
||||
@@ -502,7 +503,7 @@ const AccessControlEdit = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Rule name
|
||||
@@ -553,7 +554,7 @@ const AccessControlEdit = () => {
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24, fontWeight: "600"}}
|
||||
style={{marginTop: 24, fontWeight: "500"}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
@@ -587,7 +588,7 @@ const AccessControlEdit = () => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
@@ -596,7 +597,7 @@ const AccessControlEdit = () => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
@@ -615,7 +616,7 @@ const AccessControlEdit = () => {
|
||||
name="tagSourceGroups"
|
||||
label="Source groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
style={{fontWeight: "600"}}
|
||||
style={{fontWeight: "500"}}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
@@ -768,7 +769,7 @@ const AccessControlEdit = () => {
|
||||
name="tagDestinationGroups"
|
||||
label="Destination groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
style={{fontWeight: "600"}}
|
||||
style={{fontWeight: "500"}}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
@@ -798,7 +799,7 @@ const AccessControlEdit = () => {
|
||||
<Form.Item
|
||||
name="protocol"
|
||||
label="Protocol"
|
||||
style={{fontWeight: "600"}}
|
||||
style={{fontWeight: "500"}}
|
||||
className="tag-box"
|
||||
>
|
||||
<Select
|
||||
@@ -814,7 +815,7 @@ const AccessControlEdit = () => {
|
||||
<Form.Item
|
||||
name="ports"
|
||||
label="Ports"
|
||||
style={{fontWeight: "600"}}
|
||||
style={{fontWeight: "500"}}
|
||||
rules={[
|
||||
{
|
||||
message:
|
||||
|
||||
@@ -458,8 +458,9 @@ const AccessControlNew = () => {
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "22px",
|
||||
fontSize: "18px",
|
||||
margin: "0px",
|
||||
fontWeight:"500"
|
||||
}}
|
||||
>
|
||||
Create Rule
|
||||
@@ -480,7 +481,7 @@ const AccessControlNew = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Rule name
|
||||
@@ -531,7 +532,7 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{ marginTop: 24, fontWeight: "600" }}
|
||||
style={{ marginTop: 24, fontWeight: "500" }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
@@ -553,7 +554,7 @@ const AccessControlNew = () => {
|
||||
name="tagSourceGroups"
|
||||
label="Source"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ fontWeight: "600" }}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
@@ -706,7 +707,7 @@ const AccessControlNew = () => {
|
||||
name="tagDestinationGroups"
|
||||
label="Destination"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ fontWeight: "600" }}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
@@ -736,14 +737,14 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="protocol"
|
||||
label="Protocol"
|
||||
style={{ fontWeight: "600" }}
|
||||
style={{ fontWeight: "500" }}
|
||||
className="tag-box"
|
||||
>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-10px",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
@@ -767,7 +768,7 @@ const AccessControlNew = () => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "600",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Ports
|
||||
@@ -776,7 +777,7 @@ const AccessControlNew = () => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-5px",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
@@ -786,7 +787,7 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="ports"
|
||||
label=""
|
||||
style={{ fontWeight: "600" }}
|
||||
style={{ fontWeight: "500" }}
|
||||
rules={[
|
||||
{
|
||||
message: "Directional traffic requires at least one port",
|
||||
@@ -845,7 +846,7 @@ const AccessControlNew = () => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
@@ -854,7 +855,7 @@ const AccessControlNew = () => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
@@ -878,7 +879,7 @@ const AccessControlNew = () => {
|
||||
href="https://docs.netbird.io/how-to/manage-network-access"
|
||||
>
|
||||
{" "}
|
||||
access controls
|
||||
Access Controls
|
||||
</a>
|
||||
</Text>
|
||||
</Col>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Switch,
|
||||
Breadcrumb,
|
||||
Table,
|
||||
SelectProps,
|
||||
Modal,
|
||||
} from "antd";
|
||||
import { Container } from "./Container";
|
||||
@@ -38,7 +39,7 @@ import { timeAgo } from "../utils/common";
|
||||
import { actions as routeActions } from "../store/route";
|
||||
import RouteAddNew from "./RouteAddNew";
|
||||
import { Route } from "../store/route/types";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
@@ -50,9 +51,7 @@ const PeerUpdate = () => {
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const { Column } = Table;
|
||||
const { confirm } = Modal;
|
||||
const {
|
||||
optionRender,
|
||||
} = useGetGroupTagHelpers()
|
||||
const { optionRender } = useGetGroupTagHelpers();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
@@ -71,19 +70,21 @@ const PeerUpdate = () => {
|
||||
const deletedRoute = useSelector(
|
||||
(state: RootState) => state.route.deletedRoute
|
||||
);
|
||||
const setupNewRouteVisible = useSelector(
|
||||
(state: RootState) => state.route.setupNewRouteVisible
|
||||
);
|
||||
const setupNewRouteVisible = useSelector(
|
||||
(state: RootState) => state.route.setupNewRouteVisible
|
||||
);
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
|
||||
const [peerGroups, setPeerGroups] = useState([] as GroupPeer[]);
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const [editName, setEditName] = useState(false);
|
||||
const options: SelectProps["options"] = [];
|
||||
const [estimatedName, setEstimatedName] = useState("");
|
||||
const [callingPeerAPI, setCallingPeerAPI] = useState(false);
|
||||
const [callingGroupAPI, setCallingGroupAPI] = useState(false);
|
||||
const [isSubmitRunning, setSubmitRunning] = useState(false);
|
||||
const [peerRoutes, setPeerRoutes] = useState([]);
|
||||
const [notPeerRoutes, setNotPeerRoutes] = useState([]);
|
||||
const [peerGroupsToSave, setPeerGroupsToSave] = useState({
|
||||
ID: "",
|
||||
groupsNoId: [],
|
||||
@@ -94,7 +95,6 @@ const PeerUpdate = () => {
|
||||
const routes = useSelector((state: RootState) => state.route.data);
|
||||
const [form] = Form.useForm();
|
||||
const styleNotification = { marginTop: 85 };
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
@@ -146,7 +146,11 @@ const PeerUpdate = () => {
|
||||
(route) => route.peer === peer.id
|
||||
);
|
||||
setPeerRoutes(filterPeerRoutes);
|
||||
}, [routes]);
|
||||
const filterNotPeerRoutes: any = routes.filter(
|
||||
(route) => route.peer !== peer.id
|
||||
);
|
||||
setNotPeerRoutes(filterNotPeerRoutes);
|
||||
}, [routes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!peer) return;
|
||||
@@ -419,7 +423,7 @@ const PeerUpdate = () => {
|
||||
const showConfirmDelete = (routeId: string, name: string) => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: 'Delete network route "' + name + '"',
|
||||
title: <span className="font-500">Delete network route {name}</span>,
|
||||
width: 600,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
@@ -545,7 +549,7 @@ const PeerUpdate = () => {
|
||||
{peer && (
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "30px" }}
|
||||
style={{ marginBottom: "25px" }}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Peers</a>,
|
||||
@@ -574,8 +578,8 @@ const PeerUpdate = () => {
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontWeight: "600",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
fontSize: "22px",
|
||||
}}
|
||||
onClick={() => toggleEditName(true, peer.name)}
|
||||
>
|
||||
@@ -644,7 +648,7 @@ const PeerUpdate = () => {
|
||||
<span
|
||||
style={{
|
||||
marginRight: "5px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
NetBird IP
|
||||
@@ -668,7 +672,7 @@ const PeerUpdate = () => {
|
||||
<Form.Item
|
||||
name="dns_label"
|
||||
label="Domain name"
|
||||
style={{ fontWeight: "bold" }}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
@@ -683,7 +687,7 @@ const PeerUpdate = () => {
|
||||
<Form.Item
|
||||
name="last_seen"
|
||||
label="Last seen"
|
||||
style={{ fontWeight: "bold" }}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
@@ -700,28 +704,31 @@ const PeerUpdate = () => {
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="login_expiration_enabled"
|
||||
style={{ fontWeight: "bold" }}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
gap: "15px",
|
||||
margin: "25px 0",
|
||||
lineHeight: "16px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
checked={formPeer.login_expiration_enabled}
|
||||
onChange={onLoginExpirationChange}
|
||||
disabled={!formPeer.user_id}
|
||||
size="small"
|
||||
/>
|
||||
<div style={{ margin: "30px 0" }}>
|
||||
<strong>Login expiration</strong>
|
||||
<div>
|
||||
<span className="font-500">Login expiration</span>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontWeight: "400",
|
||||
margin: "0",
|
||||
}}
|
||||
>
|
||||
Login expiration SSO login peers require
|
||||
@@ -736,6 +743,7 @@ const PeerUpdate = () => {
|
||||
name="groupsNames"
|
||||
label="Select peer groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
@@ -757,6 +765,7 @@ const PeerUpdate = () => {
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
gap: "10px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Button onClick={onCancel} disabled={savedGroups.loading}>
|
||||
@@ -793,7 +802,7 @@ const PeerUpdate = () => {
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network routes
|
||||
@@ -835,7 +844,6 @@ const PeerUpdate = () => {
|
||||
showHeader={false}
|
||||
scroll={{ x: 800 }}
|
||||
pagination={false}
|
||||
// loading={tableSpin(loading)}
|
||||
dataSource={peerRoutes}
|
||||
>
|
||||
<Column title="Name" dataIndex="network_id" />
|
||||
@@ -848,6 +856,7 @@ const PeerUpdate = () => {
|
||||
<>
|
||||
<Switch
|
||||
defaultChecked={record.enabled}
|
||||
size="small"
|
||||
onChange={(checked) =>
|
||||
onRouteEnableChange(checked, record)
|
||||
}
|
||||
@@ -917,7 +926,7 @@ const PeerUpdate = () => {
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
margin: "0",
|
||||
}}
|
||||
>
|
||||
@@ -1004,7 +1013,9 @@ const PeerUpdate = () => {
|
||||
</Card>
|
||||
</Container>
|
||||
)}
|
||||
{setupNewRouteVisible && <RouteAddNew peer={peer} />}
|
||||
{setupNewRouteVisible && (
|
||||
<RouteAddNew selectedPeer={peer} notPeerRoutes={notPeerRoutes} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
FlagFilled,
|
||||
QuestionCircleFilled,
|
||||
} from "@ant-design/icons";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import { Route, RouteToSave } from "../store/route/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
@@ -44,19 +45,18 @@ const { Panel } = Collapse;
|
||||
interface FormRoute extends Route {}
|
||||
|
||||
const RouteAddNew = (selectedPeer: any) => {
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
// const { optionRender, blueTagRender, grayTagRender } =
|
||||
// useGetGroupTagHelpers();
|
||||
|
||||
const { Option } = Select;
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
@@ -74,6 +74,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const options: SelectProps["options"] = [];
|
||||
const testOptions: SelectProps["options"] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute);
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
@@ -84,6 +85,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG);
|
||||
const defaultStatusMSG = "Status";
|
||||
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG);
|
||||
const [enableNetwork, setEnableNetwork] = useState(false);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false);
|
||||
|
||||
@@ -117,10 +119,16 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
useEffect(() => {
|
||||
if (!route) return;
|
||||
|
||||
if (selectedPeer && selectedPeer.peer) {
|
||||
if (selectedPeer && selectedPeer.selectedPeer) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(selectedPeer.peer.name, selectedPeer.peer.ip),
|
||||
value: peerToPeerIP(selectedPeer.peer.name, selectedPeer.peer.ip),
|
||||
label: peerToPeerIP(
|
||||
selectedPeer.selectedPeer.name,
|
||||
selectedPeer.selectedPeer.ip
|
||||
),
|
||||
value: peerToPeerIP(
|
||||
selectedPeer.selectedPeer.name,
|
||||
selectedPeer.selectedPeer.ip
|
||||
),
|
||||
disabled: false,
|
||||
});
|
||||
const udpateRoute = { ...route, peer: options[0].value } as FormRoute;
|
||||
@@ -142,9 +150,22 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
} else {
|
||||
setNewRoute(false);
|
||||
}
|
||||
|
||||
// let options = [];
|
||||
}, [route]);
|
||||
|
||||
if (!selectedPeer.peer) {
|
||||
selectedPeer &&
|
||||
selectedPeer.notPeerRoutes &&
|
||||
selectedPeer.notPeerRoutes.forEach((element: any, index: number) => {
|
||||
testOptions?.push({
|
||||
label: element.network_id + " - " + element.network,
|
||||
value: element.network_id + "+" + index,
|
||||
network: element.network,
|
||||
disabled: false,
|
||||
key: index,
|
||||
});
|
||||
});
|
||||
if (!selectedPeer.selectedPeer) {
|
||||
peers.forEach((p) => {
|
||||
let os: string;
|
||||
os = p.os;
|
||||
@@ -338,6 +359,37 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onNetworkChange = (selectedOption: any) => {
|
||||
if (selectedOption === null) {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: "",
|
||||
network_id: "",
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(false)
|
||||
} else if (!!selectedOption.__isNew__) {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: "",
|
||||
network_id: selectedOption.value.split("+")[0],
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(false);
|
||||
} else {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: selectedOption.network,
|
||||
network_id: selectedOption.value.split("+")[0],
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{route && (
|
||||
@@ -378,19 +430,20 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
margin: "0px",
|
||||
fontWeight: 500,
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
Add Route
|
||||
</Paragraph>
|
||||
|
||||
{!!selectedPeer.peer && (
|
||||
{!!selectedPeer.selectedPeer && (
|
||||
<div style={{ lineHeight: "20px" }}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
@@ -399,7 +452,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
@@ -416,7 +469,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
disabled={!!selectedPeer.peer}
|
||||
disabled={!!selectedPeer.selectedPeer}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -434,52 +487,87 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
{formRoute.id ? formRoute.network_id : "New Route"}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a unique cryptographic key that is assigned to
|
||||
each device
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="network_id"
|
||||
label=""
|
||||
|
||||
style={{marginBottom:"10px"}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add an identifier for this access route",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “e.g. aws-eu-central-1-vpc”"
|
||||
ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
maxLength={40}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
<div style={{ marginBottom: "15px" }}>
|
||||
{!!selectedPeer.selectedPeer && (
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a unique cryptographic key that is assigned
|
||||
to each device
|
||||
</Paragraph>
|
||||
<CreatableSelect
|
||||
isClearable
|
||||
className="ant-select-selector-custom"
|
||||
options={testOptions}
|
||||
onChange={onNetworkChange}
|
||||
placeholder="Select an existing network or add a new one"
|
||||
classNamePrefix="react-select"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!!!selectedPeer.selectedPeer && (
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a unique cryptographic key that is assigned
|
||||
to each device
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="network_id"
|
||||
label=""
|
||||
style={{ marginBottom: "10px" }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add an identifier for this access route",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “e.g. aws-eu-central-1-vpc”"
|
||||
ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
maxLength={40}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div
|
||||
@@ -503,7 +591,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{ marginTop: 24 }}
|
||||
style={{ marginTop: 24, fontWeight: 500 }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
@@ -523,7 +611,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
{/* {!!!selectedPeer.peer && (
|
||||
{/* {!!!selectedPeer.selectedPeer && (
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
@@ -542,7 +630,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
@@ -567,7 +655,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Range
|
||||
@@ -576,7 +664,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
@@ -589,20 +677,21 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “172.16.0.0/16”"
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
disabled={(!setupNewRouteHA && !newRoute) || enableNetwork}
|
||||
autoComplete="off"
|
||||
minLength={9}
|
||||
maxLength={43}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{!!!selectedPeer.peer && (
|
||||
|
||||
{!!!selectedPeer.selectedPeer && (
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
@@ -611,7 +700,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
@@ -625,7 +714,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
disabled={!!selectedPeer.peer}
|
||||
disabled={!!selectedPeer.selectedPeer}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -635,7 +724,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Distribution groups
|
||||
@@ -644,7 +733,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
@@ -689,7 +778,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
@@ -698,7 +787,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
@@ -734,7 +823,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
}
|
||||
className="system-info-panel"
|
||||
>
|
||||
<Row gutter={16} style={{padding:"15px 0 0"}}>
|
||||
<Row gutter={16} style={{ padding: "15px 0 0" }}>
|
||||
<Col span={22}>
|
||||
<Form.Item name="masquerade" label="">
|
||||
<div
|
||||
@@ -754,7 +843,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Masquerade
|
||||
@@ -763,7 +852,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
@@ -781,7 +870,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Metric
|
||||
@@ -790,7 +879,7 @@ const RouteAddNew = (selectedPeer: any) => {
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Container } from "./Container";
|
||||
import Paragraph from "antd/es/typography/Paragraph";
|
||||
import { EditOutlined, LockOutlined } from "@ant-design/icons";
|
||||
import { actions as personalAccessTokenActions } from "../store/personal-access-token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const { Option } = Select;
|
||||
const { Text } = Typography;
|
||||
@@ -44,10 +44,7 @@ const customExpiresFormat = (value: Date): string | null => {
|
||||
};
|
||||
|
||||
const SetupKeyNew = () => {
|
||||
const {
|
||||
optionRender,
|
||||
blueTagRender
|
||||
} = useGetGroupTagHelpers()
|
||||
const { optionRender, blueTagRender } = useGetGroupTagHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -62,14 +59,14 @@ const SetupKeyNew = () => {
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey);
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
return () => {
|
||||
setVisibleNewSetupKey(false)
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
return () => {
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editName) return;
|
||||
|
||||
@@ -269,7 +266,7 @@ const SetupKeyNew = () => {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "30px" }}
|
||||
style={{ marginBottom: "25px" }}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Setup Keys</a>,
|
||||
@@ -279,12 +276,11 @@ const SetupKeyNew = () => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
bordered={true}
|
||||
title={setupKey.name}
|
||||
style={{ marginBottom: "7px" }}
|
||||
>
|
||||
<Card bordered={true} style={{ marginBottom: "7px", border: "none" }}>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<h3 style={{ fontSize: "22px", fontWeight: "500",marginBottom:"30px" }}>
|
||||
{setupKey.name}
|
||||
</h3>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
@@ -309,7 +305,7 @@ const SetupKeyNew = () => {
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
@@ -340,14 +336,14 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
></Paragraph>
|
||||
{formSetupKey.type === "one-off" ? "One-off" : "Reusable"},
|
||||
@@ -384,7 +380,7 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Auto-assigned groups
|
||||
@@ -414,7 +410,7 @@ const SetupKeyNew = () => {
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Paragraph style={{ margin: 0, fontWeight: "bold" }}>
|
||||
<Paragraph style={{ margin: 0, fontWeight: "500" }}>
|
||||
Expires
|
||||
</Paragraph>
|
||||
<Row>
|
||||
@@ -437,7 +433,7 @@ const SetupKeyNew = () => {
|
||||
href="https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
>
|
||||
{" "}
|
||||
setup keys
|
||||
Setup Keys
|
||||
</a>
|
||||
</Text>
|
||||
</Row>
|
||||
|
||||
@@ -369,8 +369,9 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "22px",
|
||||
fontSize: "18px",
|
||||
margin: "0px",
|
||||
fontWeight:"500"
|
||||
}}
|
||||
>
|
||||
{showPlainToken
|
||||
@@ -382,7 +383,6 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
paddingBottom: "15px",
|
||||
}}
|
||||
>
|
||||
{showPlainToken
|
||||
@@ -402,8 +402,11 @@ const SetupKeyNew = () => {
|
||||
>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "bold" }}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
<Paragraph style={{ fontWeight: "500" }}>Name</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ marginTop: "-15px", marginBottom: "-5px" }}
|
||||
>
|
||||
Set an easily identifiable name for your key
|
||||
</Paragraph>
|
||||
</Col>
|
||||
@@ -427,7 +430,7 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Reusable
|
||||
@@ -461,7 +464,7 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Usage limit
|
||||
@@ -499,7 +502,7 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Expires in
|
||||
@@ -535,6 +538,7 @@ const SetupKeyNew = () => {
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Auto-assigned groups
|
||||
|
||||
@@ -192,25 +192,29 @@ const UserEdit = () => {
|
||||
|
||||
const showConfirmDelete = (token: TokenDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete token \"" + token.name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to delete this token?</Paragraph>
|
||||
</Space>,
|
||||
onOk() {
|
||||
dispatch(personalAccessTokenActions.deletePersonalAccessToken.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: {
|
||||
user_id: user.id,
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
} as SpecificPAT
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: <span className="font-500">Delete token {token.name}</span>,
|
||||
width: 600,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to delete this token?</Paragraph>
|
||||
</Space>
|
||||
),
|
||||
onOk() {
|
||||
dispatch(
|
||||
personalAccessTokenActions.deletePersonalAccessToken.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: {
|
||||
user_id: user.id,
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
} as SpecificPAT,
|
||||
})
|
||||
);
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,10 +287,10 @@ const UserEdit = () => {
|
||||
|
||||
<Card
|
||||
bordered={true}
|
||||
title={user.name}
|
||||
loading={loading}
|
||||
style={{ marginBottom: "7px" }}
|
||||
>
|
||||
<h3 style={{fontSize:"22px",fontWeight:"500",marginBottom:"25px"}}>{user.name}</h3>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
@@ -314,12 +318,12 @@ const UserEdit = () => {
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<Text style={{}}>Email</Text>}
|
||||
style={{ marginRight: "70px", fontWeight: "bold" }}
|
||||
style={{ marginRight: "70px", fontWeight: "500" }}
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{ color: "#8c8c8c"}}
|
||||
style={{ color: "#8c8c8c" }}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -328,16 +332,19 @@ const UserEdit = () => {
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label={<Text style={{}}>Role</Text>}
|
||||
style={{ marginRight: "50px", fontWeight: "bold" }}
|
||||
label={<Text style={{fontWeight:"500"}}>Role</Text>}
|
||||
style={{ marginRight: "50px", fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
|
||||
disabled={user.is_current}
|
||||
>
|
||||
<Option value="admin"><Text type={"secondary"}>admin</Text></Option>
|
||||
<Option value="user"><Text type={"secondary"}>user</Text></Option>
|
||||
<Option value="admin">
|
||||
<Text type={"secondary"}>admin</Text>
|
||||
</Option>
|
||||
<Option value="user">
|
||||
<Text type={"secondary"}>user</Text>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -356,7 +363,11 @@ const UserEdit = () => {
|
||||
>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label={<Text style={{}}>Auto-assigned groups</Text>}
|
||||
label={
|
||||
<Text style={{ fontWeight: "500" }}>
|
||||
Auto-assigned groups
|
||||
</Text>
|
||||
}
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ marginRight: "70px" }}
|
||||
@@ -389,7 +400,7 @@ const UserEdit = () => {
|
||||
valuePropName="checked"
|
||||
name="is_blocked"
|
||||
label="Block user"
|
||||
style={{ marginRight: "50px", fontWeight: "bold" }}
|
||||
style={{ marginRight: "50px", fontWeight: "500" }}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
@@ -421,8 +432,8 @@ const UserEdit = () => {
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Access tokens
|
||||
|
||||
@@ -116,7 +116,7 @@ const AddPATPopup = () => {
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px"}}>
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "18px",fontWeight:"500"}}>
|
||||
{showPlainToken ? "Token created successfully!" : "Create token"}
|
||||
</Paragraph>
|
||||
{!showPlainToken && <Paragraph type={"secondary"}
|
||||
@@ -142,7 +142,7 @@ const AddPATPopup = () => {
|
||||
<Col span={24}>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set an easily identifiable name for your token</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
@@ -162,7 +162,7 @@ const AddPATPopup = () => {
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24} style={{textAlign: "left", marginTop: "10px"}}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Expires in</Paragraph>
|
||||
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Expires in</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Number of days this token will be valid for</Paragraph>
|
||||
<Form.Item
|
||||
name="expires_in"
|
||||
|
||||
@@ -1,292 +1,362 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {Container} from "../Container";
|
||||
import {CloseOutlined} from "@ant-design/icons";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {useGetTokenSilently} from "../../utils/token";
|
||||
import {actions as userActions} from "../../store/user";
|
||||
import {actions as groupActions} from "../../store/group";
|
||||
import {User, UserToSave} from "../../store/user/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import { Container } from "../Container";
|
||||
import { CloseOutlined } from "@ant-design/icons";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { useGetTokenSilently } from "../../utils/token";
|
||||
import { actions as userActions } from "../../store/user";
|
||||
import { actions as groupActions } from "../../store/group";
|
||||
import { User, UserToSave } from "../../store/user/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const AddServiceUserPopup = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const addServiceUserModalOpen = useSelector((state: RootState) => state.user.addServiceUserPopupVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const addServiceUserModalOpen = useSelector(
|
||||
(state: RootState) => state.user.addServiceUserPopupVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => values.autoGroupsNames && values.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = values.autoGroupsNames?.filter((s: string) => !allGroupsNames.includes(s)) || []
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: true
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
form.resetFields();
|
||||
dispatch(userActions.getServiceUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter(
|
||||
(g) =>
|
||||
values.autoGroupsNames && values.autoGroupsNames.includes(g.name)
|
||||
)
|
||||
.map((g) => g.id || "") || [];
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.name);
|
||||
const groupsToCreate =
|
||||
values.autoGroupsNames?.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
) || [];
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: true,
|
||||
} as UserToSave;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return;
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
form.resetFields();
|
||||
dispatch(
|
||||
userActions.getServiceUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
}, [])
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const { label, value, closable, onClose } = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={addServiceUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
onClick={handleFormSubmit}>Create user</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px"}}>
|
||||
{"Add service user"}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}>
|
||||
{"Service users are non-login users that are not associated with any specific person."}
|
||||
</Paragraph>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
["role"]: "user"
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a name to easily identify the user</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Ansible user"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Role</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Set a role for the user to assign access permissions</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please select a role for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Select style={{width: "120px"}}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
|
||||
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
|
||||
{/* <Form.Item*/}
|
||||
{/* name="autoGroupsNames"*/}
|
||||
{/* label="Auto-assigned groups"*/}
|
||||
{/* tooltip="Every peer enrolled with this user will be automatically added to these groups"*/}
|
||||
{/* rules={[{validator: selectValidator}]}*/}
|
||||
{/* >*/}
|
||||
{/* <Select mode="tags"*/}
|
||||
{/* style={{width: '100%'}}*/}
|
||||
{/* placeholder="Associate groups with the user"*/}
|
||||
{/* tagRender={tagRender}*/}
|
||||
{/* onChange={handleChangeTags}*/}
|
||||
{/* dropdownRender={dropDownRender}*/}
|
||||
{/* >*/}
|
||||
{/* {*/}
|
||||
{/* tagGroups.map(m =>*/}
|
||||
{/* <Option key={m}>{optionRender(m)}</Option>*/}
|
||||
{/* )*/}
|
||||
{/* }*/}
|
||||
{/* </Select>*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = [];
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v);
|
||||
}
|
||||
});
|
||||
setSelectedTagGroups(validatedValues);
|
||||
};
|
||||
|
||||
export default AddServiceUserPopup
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Row style={{ padding: "0 8px 4px" }}>
|
||||
<Col flex="auto">
|
||||
<span style={{ color: "#9CA3AF" }}>
|
||||
Add new group by pressing "Enter"
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = "";
|
||||
const g = groups.find((_g) => _g.name === label);
|
||||
if (g)
|
||||
peersCount = ` - ${g.peers_count || 0} ${
|
||||
!g.peers_count || parseInt(g.peers_count) !== 1 ? "peers" : "peer"
|
||||
} `;
|
||||
return (
|
||||
<>
|
||||
<Tag color="blue" style={{ marginRight: 3 }}>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(
|
||||
groups?.filter((g) => g.name != "All").map((g) => g.name) || []
|
||||
);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={addServiceUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Create user
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
textAlign: "start",
|
||||
marginLeft: "-15px",
|
||||
marginRight: "-15px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{"Add service user"}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}
|
||||
>
|
||||
{
|
||||
"Service users are non-login users that are not associated with any specific person."
|
||||
}
|
||||
</Paragraph>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
["role"]: "user",
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a name to easily identify the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a new name for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Ansible user"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Role
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ fontSize: "14px", marginTop: "-15px" }}
|
||||
>
|
||||
Set a role for the user to assign access permissions
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select a role for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select style={{ width: "120px" }}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
|
||||
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
|
||||
{/* <Form.Item*/}
|
||||
{/* name="autoGroupsNames"*/}
|
||||
{/* label="Auto-assigned groups"*/}
|
||||
{/* tooltip="Every peer enrolled with this user will be automatically added to these groups"*/}
|
||||
{/* rules={[{validator: selectValidator}]}*/}
|
||||
{/* >*/}
|
||||
{/* <Select mode="tags"*/}
|
||||
{/* style={{width: '100%'}}*/}
|
||||
{/* placeholder="Associate groups with the user"*/}
|
||||
{/* tagRender={tagRender}*/}
|
||||
{/* onChange={handleChangeTags}*/}
|
||||
{/* dropdownRender={dropDownRender}*/}
|
||||
{/* >*/}
|
||||
{/* {*/}
|
||||
{/* tagGroups.map(m =>*/}
|
||||
{/* <Option key={m}>{optionRender(m)}</Option>*/}
|
||||
{/* )*/}
|
||||
{/* }*/}
|
||||
{/* </Select>*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddServiceUserPopup;
|
||||
|
||||
@@ -1,284 +1,366 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {Container} from "../Container";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {useGetTokenSilently} from "../../utils/token";
|
||||
import {actions as userActions} from "../../store/user";
|
||||
import {actions as groupActions} from "../../store/group";
|
||||
import {User, UserToSave} from "../../store/user/types";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {useGetGroupTagHelpers} from "../../utils/groups";
|
||||
import { Container } from "../Container";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { useGetTokenSilently } from "../../utils/token";
|
||||
import { actions as userActions } from "../../store/user";
|
||||
import { actions as groupActions } from "../../store/group";
|
||||
import { User, UserToSave } from "../../store/user/types";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
import { QuestionCircleFilled } from "@ant-design/icons";
|
||||
import { useGetGroupTagHelpers } from "../../utils/groups";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const InviteUserPopup = () => {
|
||||
const {
|
||||
optionRender,
|
||||
blueTagRender
|
||||
} = useGetGroupTagHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { optionRender, blueTagRender } = useGetGroupTagHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const inviteUserModalOpen = useSelector((state: RootState) => state.user.inviteUserPopupVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const inviteUserModalOpen = useSelector(
|
||||
(state: RootState) => state.user.inviteUserPopupVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter(
|
||||
(g) =>
|
||||
values.autoGroupsNames && values.autoGroupsNames.includes(g.name)
|
||||
)
|
||||
.map((g) => g.id || "") || [];
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.name);
|
||||
const groupsToCreate =
|
||||
values.autoGroupsNames?.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
) || [];
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
email: values.email,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: false,
|
||||
} as UserToSave;
|
||||
};
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => values.autoGroupsNames && values.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = values.autoGroupsNames?.filter((s: string) => !allGroupsNames.includes(s)) || []
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
email: values.email,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: false
|
||||
} as UserToSave
|
||||
}
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return;
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
form.resetFields();
|
||||
dispatch(userActions.getRegularUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupActions.getGroups.request({
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
}, [])
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
form.resetFields();
|
||||
dispatch(
|
||||
userActions.getRegularUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={inviteUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
onClick={handleFormSubmit}>Invite</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
|
||||
{"Invite user"}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}>
|
||||
{"Invite a user to your network and set their permissions."}
|
||||
</Paragraph>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
["role"]: "user"
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a name to easily identify the user</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a name for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Max Schmidt"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "bold", marginTop: "0px"}}>Email</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Provide the email address of the user</Paragraph>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a valid email address for this user',
|
||||
whitespace: false,
|
||||
pattern: new RegExp(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i)
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "max.schmidt@gmail.com"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "bold", marginTop: "0px"}}>Role</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a role for the user to assign access permissions</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please select a role for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Select style={{width: "120px"}}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}
|
||||
href="https://docs.netbird.io/how-to/access-netbird-public-api">Learn more about user</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
}
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export default InviteUserPopup
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = [];
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v);
|
||||
}
|
||||
});
|
||||
setSelectedTagGroups(validatedValues);
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Row style={{ padding: "0 8px 4px" }}>
|
||||
<Col flex="auto">
|
||||
<span style={{ color: "#9CA3AF" }}>
|
||||
Add new group by pressing "Enter"
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(
|
||||
groups?.filter((g) => g.name != "All").map((g) => g.name) || []
|
||||
);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={inviteUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Invite
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
textAlign: "start",
|
||||
marginLeft: "-15px",
|
||||
marginRight: "-15px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{"Invite user"}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}
|
||||
>
|
||||
{"Invite a user to your network and set their permissions."}
|
||||
</Paragraph>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
["role"]: "user",
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a name to easily identify the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a name for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Max Schmidt"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Email
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Provide the email address of the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a valid email address for this user",
|
||||
whitespace: false,
|
||||
pattern: new RegExp(
|
||||
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
|
||||
),
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "max.schmidt@gmail.com"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Role
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a role for the user to assign access permissions
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select a role for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select style={{ width: "120px" }}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Auto-assigned groups
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Add groups, that will be assigned to peers added by this user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Button
|
||||
icon={<QuestionCircleFilled />}
|
||||
type="link"
|
||||
target="_blank"
|
||||
disabled={true}
|
||||
style={{ marginTop: "20px", marginBottom: "20px" }}
|
||||
href="https://docs.netbird.io/how-to/access-netbird-public-api"
|
||||
>
|
||||
Learn more about user
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteUserPopup;
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;500&display=swap');
|
||||
@import 'antd/dist/reset.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
* {
|
||||
font-family: 'Roboto', sans-serif !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
background-color: #f0f2f5;
|
||||
@@ -27,11 +34,12 @@ body {
|
||||
|
||||
.ant-menu-horizontal>.ant-menu-item a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ant-menu-horizontal>.ant-menu-item-selected a {
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
@@ -223,4 +231,39 @@ td.non-highlighted-table-column {
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.font-500 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
font-weight: 500 !important;
|
||||
font-size: 22px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.react-select__indicator-separator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.react-select__control,
|
||||
.react-select__value-container,
|
||||
.react-select__input-container {
|
||||
min-height: 32px !important;
|
||||
padding: 0 5px !important;
|
||||
max-height: 32px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.react-select__value-container {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.react-select__indicator {
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
export interface Route {
|
||||
id?: string
|
||||
id?: string | null
|
||||
description: string
|
||||
enabled: boolean
|
||||
peer: string
|
||||
|
||||
@@ -257,8 +257,8 @@ export const AccessControl = () => {
|
||||
setPolicyToAction(record as PolicyDataTable);
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: 'Delete rule "' + record.name + '"',
|
||||
width: 600,
|
||||
title: <span className="font-500">Delete rule {record.name}</span>,
|
||||
width: 500,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
<Paragraph>
|
||||
@@ -320,7 +320,7 @@ export const AccessControl = () => {
|
||||
) as Policy[];
|
||||
if (optionAllEnable == "enabled") {
|
||||
f = filter(f, (f: Policy) => f.enabled);
|
||||
} else if (optionAllEnable == "disabled") {
|
||||
} else if (optionAllEnable == "disabled") {
|
||||
f = filter(f, (f: Policy) => !f.enabled);
|
||||
}
|
||||
return f;
|
||||
@@ -438,7 +438,7 @@ export const AccessControl = () => {
|
||||
} `;
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag color="blue" style={{ marginRight: 3}}>
|
||||
<Tag color="blue" style={{ marginRight: 3 }}>
|
||||
{_g.name}
|
||||
</Tag>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
@@ -549,8 +549,8 @@ export const AccessControl = () => {
|
||||
<Container className="container-main">
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Access Control</Title>
|
||||
<Paragraph>
|
||||
<Title className="page-heading">Access Control</Title>
|
||||
<Paragraph type="secondary">
|
||||
Access rules help you manage access permissions in your
|
||||
organisation.
|
||||
</Paragraph>
|
||||
@@ -602,7 +602,7 @@ export const AccessControl = () => {
|
||||
disabled={savedPolicy.loading}
|
||||
onClick={onClickAddNewPolicy}
|
||||
>
|
||||
Add rule
|
||||
Add Rule
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -667,7 +667,7 @@ export const AccessControl = () => {
|
||||
}
|
||||
className="tooltip-label"
|
||||
>
|
||||
<Text strong>{text}</Text>
|
||||
<Text className="font-500">{text}</Text>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -742,8 +742,8 @@ export const AccessControl = () => {
|
||||
render={(text, record: PolicyDataTable, index) => {
|
||||
return (
|
||||
<Tag
|
||||
className="menlo-font"
|
||||
style={{
|
||||
className="menlo-font"
|
||||
style={{
|
||||
marginRight: "3",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
|
||||
@@ -1,311 +1,462 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as eventActions} from '../store/event';
|
||||
import {Container} from "../components/Container";
|
||||
import {Alert, Button, Card, Col, Input, Row, Select, Space, Table, Typography,} from "antd";
|
||||
import {Event} from "../store/event/types";
|
||||
import {filter} from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as eventActions } from "../store/event";
|
||||
import { Container } from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { Event } from "../store/event/types";
|
||||
import { filter } from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {capitalize, formatDateTime} from "../utils/common";
|
||||
import {User} from "../store/user/types";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import {QuestionCircleFilled} from "@ant-design/icons";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useOidcUser } from "@axa-fr/react-oidc";
|
||||
import { capitalize, formatDateTime } from "../utils/common";
|
||||
import { User } from "../store/user/types";
|
||||
import { usePageSizeHelpers } from "../utils/pageSize";
|
||||
import { QuestionCircleFilled } from "@ant-design/icons";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Column } = Table;
|
||||
|
||||
interface EventDataTable extends Event {
|
||||
}
|
||||
interface EventDataTable extends Event {}
|
||||
|
||||
export const Activity = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const {oidcUser} = useOidcUser();
|
||||
const dispatch = useDispatch()
|
||||
const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const { oidcUser } = useOidcUser();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const events = useSelector((state: RootState) => state.event.data);
|
||||
const failed = useSelector((state: RootState) => state.event.failed);
|
||||
const loading = useSelector((state: RootState) => state.event.loading);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
const events = useSelector((state: RootState) => state.event.data);
|
||||
const failed = useSelector((state: RootState) => state.event.failed);
|
||||
const loading = useSelector((state: RootState) => state.event.loading);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
|
||||
const [textToSearch, setTextToSearch] = useState("");
|
||||
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
|
||||
|
||||
const transformDataTable = (d: Event[]): EventDataTable[] => {
|
||||
return d.map((p) => ({ key: p.id, ...p } as EventDataTable));
|
||||
};
|
||||
|
||||
const transformDataTable = (d: Event[]): EventDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as EventDataTable))
|
||||
}
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
eventActions.getEvents.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(events));
|
||||
}, [events]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(eventActions.getEvents.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(events))
|
||||
}, [events])
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()));
|
||||
}, [textToSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
const filterDataTable = (): Event[] => {
|
||||
const t = textToSearch.toLowerCase().trim();
|
||||
let usrsMatch: User[] = filter(
|
||||
users,
|
||||
(u: User) =>
|
||||
u.name?.toLowerCase().includes(t) || u.email?.toLowerCase().includes(t)
|
||||
) as User[];
|
||||
let f: Event[] = filter(
|
||||
events,
|
||||
(f: Event) =>
|
||||
(f.activity || f.id).toLowerCase().includes(t) ||
|
||||
t === "" ||
|
||||
usrsMatch.find((u) => u.id === f.initiator_id)
|
||||
) as Event[];
|
||||
return f;
|
||||
};
|
||||
|
||||
const filterDataTable = (): Event[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let usrsMatch: User[] = filter(users, (u: User) => (u.name)?.toLowerCase().includes(t) || (u.email)?.toLowerCase().includes(t)) as User[]
|
||||
let f: Event[] = filter(events, (f: Event) =>
|
||||
((f.activity || f.id).toLowerCase().includes(t) || t === "" || usrsMatch.find(u => u.id === f.initiator_id))
|
||||
) as Event[]
|
||||
return f
|
||||
}
|
||||
const onChangeTextToSearch = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setTextToSearch(e.target.value);
|
||||
};
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const getActivityRow = (objectType: string, name:string,text:string) => {
|
||||
return <Row> <Text>{objectType} <Text type="secondary">{name}</Text> {text}</Text> </Row>
|
||||
}
|
||||
|
||||
const renderActivity = (event: EventDataTable) => {
|
||||
let body = <Text>{event.activity}</Text>
|
||||
switch (event.activity_code) {
|
||||
case "peer.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to peer")
|
||||
case "peer.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from peer")
|
||||
case "user.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to user")
|
||||
case "user.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from user")
|
||||
case "setupkey.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to setup key")
|
||||
case "setupkey.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed setup key")
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to disabled management DNS setting")
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from disabled management DNS setting")
|
||||
case "personal.access.token.create":
|
||||
return getActivityRow("Personal access token", event.meta.name,"added to user")
|
||||
case "personal.access.token.delete":
|
||||
return getActivityRow("Personal access token", event.meta.name,"removed from user")
|
||||
}
|
||||
return body
|
||||
}
|
||||
const renderInitiator = (event: EventDataTable) => {
|
||||
let body = <></>
|
||||
const user = users?.find(u => u.id === event.initiator_id)
|
||||
switch (event.activity_code) {
|
||||
case "setupkey.peer.add":
|
||||
const key = setupKeys?.find(k => k.id === event.initiator_id)
|
||||
if (key) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{key.name}</Text> </Row>
|
||||
<Row> <Text type="secondary">Setup Key</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (user) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{user.name ? user.name : user.id}</Text> </Row>
|
||||
<Row> <Text type="secondary">{user.email ? user.email : user.is_service_user ? "Service User" : "User"}</Text> </Row>
|
||||
</span>
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
const renderMultiRowSpan = (primaryRowText:string,secondaryRowText:string) => {
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{primaryRowText}</Text> </Row>
|
||||
<Row> <Text type="secondary">{secondaryRowText}</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
|
||||
const renderTarget = (event: EventDataTable) => {
|
||||
if (event.activity_code === "account.create" || event.activity_code === "user.join") {
|
||||
return "-"
|
||||
}
|
||||
const user = users?.find(u => u.id === event.target_id)
|
||||
switch (event.activity_code) {
|
||||
case "account.create":
|
||||
case "user.join":
|
||||
return "-"
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Rule")
|
||||
case "policy.add":
|
||||
case "policy.delete":
|
||||
case "policy.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Policy")
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
let cType:string
|
||||
cType = capitalize(event.meta.type)
|
||||
return renderMultiRowSpan(event.meta.name,cType+" setup key "+event.meta.key)
|
||||
case "group.add":
|
||||
case "group.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Group")
|
||||
case "nameserver.group.add":
|
||||
case "nameserver.group.update":
|
||||
case "nameserver.group.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Nameserver group")
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
case "peer.ssh.enable":
|
||||
case "peer.ssh.disable":
|
||||
case "peer.rename":
|
||||
case "peer.login.expiration.disable":
|
||||
case "peer.login.expiration.enable":
|
||||
return renderMultiRowSpan(event.meta.fqdn,event.meta.ip)
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
case "route.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Route for range " + event.meta.network_range)
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
case "user.role.update":
|
||||
if (user) {
|
||||
return renderMultiRowSpan((user.name ? user.name : user.id),user.email ? user.email : user.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(event.meta.user_name, event.meta.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
return "-"
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey,"Setup Key")
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
case "account.setting.peer.login.expiration.enable":
|
||||
case "account.setting.peer.login.expiration.disable":
|
||||
case "account.setting.peer.login.expiration.update":
|
||||
return renderMultiRowSpan("","System setting")
|
||||
case "personal.access.token.create":
|
||||
case "personal.access.token.delete":
|
||||
if(user) {
|
||||
return renderMultiRowSpan((user.name ? user.name : user.id), user.email ? user.email : user.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(event.meta.user_name,event.meta.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
return "-"
|
||||
case "service.user.create":
|
||||
case "service.user.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Service User")
|
||||
case "user.invite":
|
||||
case "user.block":
|
||||
case "user.unblock":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
break
|
||||
default:
|
||||
console.error("unknown event - missing handling", event.activity_code)
|
||||
}
|
||||
|
||||
return event.target_id
|
||||
}
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable();
|
||||
setDataTable(transformDataTable(data));
|
||||
};
|
||||
|
||||
const getActivityRow = (objectType: string, name: string, text: string) => {
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Activity</Title>
|
||||
<Paragraph>Here you can see all the account and network activity events</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>
|
||||
{objectType} <Text type="secondary">{name}</Text> {text}
|
||||
</Text>{" "}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/how-to/monitor-system-and-network-activity">Learn more about activity tracking</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
|
||||
type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} activity events`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
size="small"
|
||||
>
|
||||
<Column title="Timestamp" dataIndex="timestamp"
|
||||
render={(text, record, index) => {
|
||||
return formatDateTime(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Activity" dataIndex="activity"
|
||||
render={(text, record, index) => {
|
||||
return renderActivity(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Initiated By" dataIndex="initiator_id"
|
||||
render={(text, record, index) => {
|
||||
return renderInitiator(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Target" dataIndex="target_id"
|
||||
render={(text, record, index) => {
|
||||
return renderTarget(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
const renderActivity = (event: EventDataTable) => {
|
||||
let body = <Text>{event.activity}</Text>;
|
||||
switch (event.activity_code) {
|
||||
case "peer.group.add":
|
||||
return getActivityRow("Group", event.meta.group, "added to peer");
|
||||
case "peer.group.delete":
|
||||
return getActivityRow("Group", event.meta.group, "removed from peer");
|
||||
case "user.group.add":
|
||||
return getActivityRow("Group", event.meta.group, "added to user");
|
||||
case "user.group.delete":
|
||||
return getActivityRow("Group", event.meta.group, "removed from user");
|
||||
case "setupkey.group.add":
|
||||
return getActivityRow("Group", event.meta.group, "added to setup key");
|
||||
case "setupkey.group.delete":
|
||||
return getActivityRow("Group", event.meta.group, "removed setup key");
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return getActivityRow(
|
||||
"Group",
|
||||
event.meta.group,
|
||||
"added to disabled management DNS setting"
|
||||
);
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return getActivityRow(
|
||||
"Group",
|
||||
event.meta.group,
|
||||
"removed from disabled management DNS setting"
|
||||
);
|
||||
case "personal.access.token.create":
|
||||
return getActivityRow(
|
||||
"Personal access token",
|
||||
event.meta.name,
|
||||
"added to user"
|
||||
);
|
||||
case "personal.access.token.delete":
|
||||
return getActivityRow(
|
||||
"Personal access token",
|
||||
event.meta.name,
|
||||
"removed from user"
|
||||
);
|
||||
}
|
||||
return body;
|
||||
};
|
||||
const renderInitiator = (event: EventDataTable) => {
|
||||
let body = <></>;
|
||||
const user = users?.find((u) => u.id === event.initiator_id);
|
||||
switch (event.activity_code) {
|
||||
case "setupkey.peer.add":
|
||||
const key = setupKeys?.find((k) => k.id === event.initiator_id);
|
||||
if (key) {
|
||||
body = (
|
||||
<span
|
||||
style={{
|
||||
height: "auto",
|
||||
whiteSpace: "normal",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>{key.name}</Text>{" "}
|
||||
</Row>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text type="secondary">Setup Key</Text>{" "}
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (user) {
|
||||
body = (
|
||||
<span
|
||||
style={{
|
||||
height: "auto",
|
||||
whiteSpace: "normal",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>{user.name ? user.name : user.id}</Text>{" "}
|
||||
</Row>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text type="secondary">
|
||||
{user.email
|
||||
? user.email
|
||||
: user.is_service_user
|
||||
? "Service User"
|
||||
: "User"}
|
||||
</Text>{" "}
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const renderMultiRowSpan = (
|
||||
primaryRowText: string,
|
||||
secondaryRowText: string
|
||||
) => {
|
||||
return (
|
||||
<span style={{ height: "auto", whiteSpace: "normal", textAlign: "left" }}>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>{primaryRowText}</Text>{" "}
|
||||
</Row>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text type="secondary">{secondaryRowText}</Text>{" "}
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTarget = (event: EventDataTable) => {
|
||||
if (
|
||||
event.activity_code === "account.create" ||
|
||||
event.activity_code === "user.join"
|
||||
) {
|
||||
return "-";
|
||||
}
|
||||
const user = users?.find((u) => u.id === event.target_id);
|
||||
switch (event.activity_code) {
|
||||
case "account.create":
|
||||
case "user.join":
|
||||
return "-";
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Rule");
|
||||
case "policy.add":
|
||||
case "policy.delete":
|
||||
case "policy.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Policy");
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
let cType: string;
|
||||
cType = capitalize(event.meta.type);
|
||||
return renderMultiRowSpan(
|
||||
event.meta.name,
|
||||
cType + " setup key " + event.meta.key
|
||||
);
|
||||
case "group.add":
|
||||
case "group.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Group");
|
||||
case "nameserver.group.add":
|
||||
case "nameserver.group.update":
|
||||
case "nameserver.group.delete":
|
||||
return renderMultiRowSpan(event.meta.name, "Nameserver group");
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
case "peer.ssh.enable":
|
||||
case "peer.ssh.disable":
|
||||
case "peer.rename":
|
||||
case "peer.login.expiration.disable":
|
||||
case "peer.login.expiration.enable":
|
||||
return renderMultiRowSpan(event.meta.fqdn, event.meta.ip);
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
case "route.update":
|
||||
return renderMultiRowSpan(
|
||||
event.meta.name,
|
||||
"Route for range " + event.meta.network_range
|
||||
);
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
case "user.role.update":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(
|
||||
user.name ? user.name : user.id,
|
||||
user.email
|
||||
? user.email
|
||||
: user.is_service_user
|
||||
? "Service User"
|
||||
: "User"
|
||||
);
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.user_name,
|
||||
event.meta.is_service_user ? "Service User" : "User"
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey, "Setup Key");
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn, event.meta.peer_ip);
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
case "account.setting.peer.login.expiration.enable":
|
||||
case "account.setting.peer.login.expiration.disable":
|
||||
case "account.setting.peer.login.expiration.update":
|
||||
return renderMultiRowSpan("", "System setting");
|
||||
case "personal.access.token.create":
|
||||
case "personal.access.token.delete":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(
|
||||
user.name ? user.name : user.id,
|
||||
user.email
|
||||
? user.email
|
||||
: user.is_service_user
|
||||
? "Service User"
|
||||
: "User"
|
||||
);
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.user_name,
|
||||
event.meta.is_service_user ? "Service User" : "User"
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
case "service.user.create":
|
||||
case "service.user.delete":
|
||||
return renderMultiRowSpan(event.meta.name, "Service User");
|
||||
case "user.invite":
|
||||
case "user.block":
|
||||
case "user.unblock":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(
|
||||
user.name ? user.name : user.id,
|
||||
user.email ? user.email : "User"
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("unknown event - missing handling", event.activity_code);
|
||||
}
|
||||
|
||||
return event.target_id;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title className="page-heading">Activity</Title>
|
||||
<Paragraph type="secondary">
|
||||
Here you can see all the account and network activity events
|
||||
</Paragraph>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
style={{ display: "flex" }}
|
||||
>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input
|
||||
allowClear
|
||||
value={textToSearch}
|
||||
onPressEnter={searchDataTable}
|
||||
placeholder="Search..."
|
||||
onChange={onChangeTextToSearch}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
options={pageSizeOptions}
|
||||
onChange={onChangePageSize}
|
||||
className="select-rows-per-page-en"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button
|
||||
icon={<QuestionCircleFilled />}
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://docs.netbird.io/how-to/monitor-system-and-network-activity"
|
||||
>
|
||||
Learn more about activity tracking
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed && (
|
||||
<Alert
|
||||
message={failed.message}
|
||||
description={failed.data ? failed.data.message : " "}
|
||||
type="error"
|
||||
showIcon
|
||||
closable
|
||||
/>
|
||||
)}
|
||||
<Card bodyStyle={{ padding: 0 }}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total, range) =>
|
||||
`Showing ${range[0]} to ${range[1]} of ${total} activity events`,
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{ x: true }}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
size="small"
|
||||
>
|
||||
<Column
|
||||
title="Timestamp"
|
||||
dataIndex="timestamp"
|
||||
render={(text, record, index) => {
|
||||
return formatDateTime(text);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Activity"
|
||||
dataIndex="activity"
|
||||
render={(text, record, index) => {
|
||||
return renderActivity(record as EventDataTable);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Initiated By"
|
||||
dataIndex="initiator_id"
|
||||
render={(text, record, index) => {
|
||||
return renderInitiator(record as EventDataTable);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Target"
|
||||
dataIndex="target_id"
|
||||
render={(text, record, index) => {
|
||||
return renderTarget(record as EventDataTable);
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Activity;
|
||||
|
||||
@@ -400,7 +400,7 @@ export const Peers = () => {
|
||||
let name = peerToAction ? peerToAction.name : "";
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: 'Delete peer "' + name + '"',
|
||||
title: <span className="font-500">Delete peer {name}</span>,
|
||||
width: 600,
|
||||
content: contentModule,
|
||||
onOk() {
|
||||
@@ -420,7 +420,9 @@ export const Peers = () => {
|
||||
const showConfirmEnableSSH = (record: PeerDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: 'Enable SSH Server for "' + record.name + '"?',
|
||||
title: (
|
||||
<span className="font-500">Enable SSH Server for {record.name} ?</span>
|
||||
),
|
||||
width: 600,
|
||||
content:
|
||||
"Experimental feature. Enabling this option allows remote SSH access to this machine from other connected network participants.",
|
||||
@@ -598,7 +600,7 @@ export const Peers = () => {
|
||||
>
|
||||
<span style={{ textAlign: "left" }}>
|
||||
<Row>
|
||||
<Text strong>{peer.name}</Text>
|
||||
<Text className="font-500">{peer.name}</Text>
|
||||
</Row>
|
||||
</span>
|
||||
</Button>
|
||||
@@ -613,7 +615,7 @@ export const Peers = () => {
|
||||
>
|
||||
<span style={{ textAlign: "left" }}>
|
||||
<Row>
|
||||
<Text strong>{peer.name}</Text>
|
||||
<Text className="font-500">{peer.name}</Text>
|
||||
</Row>
|
||||
<Row>
|
||||
<Text type="secondary">{userEmail}</Text>
|
||||
@@ -632,19 +634,12 @@ export const Peers = () => {
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Peers</Title>
|
||||
{showTutorial && (
|
||||
<Paragraph type={"secondary"}>
|
||||
A list of all the machines in your account including their
|
||||
name, IP and status.
|
||||
</Paragraph>
|
||||
)}
|
||||
{!showTutorial && (
|
||||
<Paragraph>
|
||||
A list of all the machines in your account including their
|
||||
name, IP and status.
|
||||
</Paragraph>
|
||||
)}
|
||||
<Title className="page-heading">Peers</Title>
|
||||
<Paragraph type={"secondary"}>
|
||||
A list of all the machines in your account including their
|
||||
name, IP and status.
|
||||
</Paragraph>
|
||||
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
@@ -695,7 +690,7 @@ export const Peers = () => {
|
||||
type="primary"
|
||||
onClick={() => setAddPeerModalOpen(true)}
|
||||
>
|
||||
Add peer
|
||||
Add Peer
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
@@ -1,436 +1,581 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from '../store/user';
|
||||
import {Container} from "../components/Container";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as userActions } from "../store/user";
|
||||
import { Container } from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown, Empty,
|
||||
Input,
|
||||
Menu,
|
||||
message, Modal,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space, Switch,
|
||||
Table,
|
||||
Tag, Tooltip,
|
||||
Typography,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {User, UserToSave} from "../store/user/types";
|
||||
import {filter} from "lodash";
|
||||
import { User, UserToSave } from "../store/user/types";
|
||||
import { filter } from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { actions as groupActions } from "../store/group";
|
||||
import { Group } from "../store/group/types";
|
||||
import { TooltipPlacement } from "antd/es/tooltip";
|
||||
import { capitalize, isLocalDev, isNetBirdHosted } from "../utils/common";
|
||||
import { usePageSizeHelpers } from "../utils/pageSize";
|
||||
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
|
||||
import InviteUserPopup from "../components/popups/InviteUserPopup";
|
||||
import {Peer, PeerDataTable} from "../store/peer/types";
|
||||
import {ExclamationCircleOutlined, MinusOutlined} from "@ant-design/icons";
|
||||
import {actions as peerActions} from "../store/peer";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import { Peer, PeerDataTable } from "../store/peer/types";
|
||||
import { ExclamationCircleOutlined, MinusOutlined } from "@ant-design/icons";
|
||||
import { actions as peerActions } from "../store/peer";
|
||||
import { useOidcUser } from "@axa-fr/react-oidc";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Column } = Table;
|
||||
|
||||
interface UserDataTable extends User {
|
||||
key: string
|
||||
key: string;
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
const styleNotification = { marginTop: 85 };
|
||||
|
||||
export const RegularUsers = () => {
|
||||
const {onChangePageSize, pageSizeOptions, pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.regularUsers);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const users = useSelector((state: RootState) => state.user.regularUsers);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const updateUserDrawerVisible = useSelector(
|
||||
(state: RootState) => state.user.updateUserDrawerVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("");
|
||||
const [userToAction, setUserToAction] = useState(null as UserDataTable | null);
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("");
|
||||
const [userToAction, setUserToAction] = useState(
|
||||
null as UserDataTable | null
|
||||
);
|
||||
const [textToSearch, setTextToSearch] = useState("");
|
||||
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (user: User) => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current
|
||||
} as User));
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (user: User) => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(
|
||||
userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
} as User)
|
||||
);
|
||||
};
|
||||
|
||||
const transformDataTable = (d: User[]): UserDataTable[] => {
|
||||
return d.map((p) => ({ key: p.id, ...p } as UserDataTable));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
userActions.getRegularUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, [savedUser]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(users));
|
||||
}, [users]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()));
|
||||
}, [textToSearch]);
|
||||
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim();
|
||||
let f: User[] = filter(
|
||||
users,
|
||||
(f: User) =>
|
||||
(f.email || f.id).toLowerCase().includes(t) ||
|
||||
f.name.toLowerCase().includes(t) ||
|
||||
f.role.includes(t) ||
|
||||
t === ""
|
||||
) as User[];
|
||||
return f;
|
||||
};
|
||||
|
||||
const onChangeTextToSearch = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setTextToSearch(e.target.value);
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable();
|
||||
setDataTable(transformDataTable(data));
|
||||
};
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(
|
||||
userActions.setUser({
|
||||
id: userToAction?.id,
|
||||
email: userToAction?.email,
|
||||
auto_groups: userToAction?.auto_groups ? userToAction?.auto_groups : [],
|
||||
name: userToAction?.name,
|
||||
role: userToAction?.role,
|
||||
is_blocked: userToAction?.is_blocked,
|
||||
} as User)
|
||||
);
|
||||
};
|
||||
|
||||
const onClickInviteUser = () => {
|
||||
dispatch(userActions.setInviteUserPopupVisible(true));
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (users) {
|
||||
let currentUser = users.find((user) => user.is_current);
|
||||
if (currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
}
|
||||
}
|
||||
}, [users]);
|
||||
|
||||
const renderPopoverGroups = (
|
||||
label: string,
|
||||
rowGroups: string[] | string[] | null,
|
||||
userToAction: UserDataTable
|
||||
) => {
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach((g) => {
|
||||
groupsMap.set(g.id!, g);
|
||||
});
|
||||
|
||||
let displayGroups: Group[] = [];
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups
|
||||
.filter((g) => groupsMap.get(g))
|
||||
.map((g) => groupsMap.get(g)!);
|
||||
}
|
||||
|
||||
const transformDataTable = (d: User[]): UserDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as UserDataTable))
|
||||
let btn = (
|
||||
<Button type="link" onClick={() => setUserAndView(userToAction)}>
|
||||
{displayGroups.length}
|
||||
</Button>
|
||||
);
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getRegularUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [savedUser])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(users))
|
||||
}, [users])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: User[] = filter(users, (f: User) =>
|
||||
((f.email || f.id).toLowerCase().includes(t) || f.name.toLowerCase().includes(t) || f.role.includes(t) || t === "")
|
||||
) as User[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(userActions.setUser({
|
||||
id: userToAction?.id,
|
||||
email: userToAction?.email,
|
||||
auto_groups: userToAction?.auto_groups ? userToAction?.auto_groups : [],
|
||||
name: userToAction?.name,
|
||||
role: userToAction?.role,
|
||||
is_blocked: userToAction?.is_blocked,
|
||||
} as User));
|
||||
}
|
||||
|
||||
const onClickInviteUser = () => {
|
||||
dispatch(userActions.setInviteUserPopupVisible(true));
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(users) {
|
||||
let currentUser = users.find((user) => user.is_current)
|
||||
if(currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
}
|
||||
}
|
||||
}, [users])
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, userToAction: UserDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
{_g.name}
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, userToAction.key)}
|
||||
open={groupPopupVisible === userToAction.key}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}, [updateUserDrawerVisible])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedUser.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedUser.success) {
|
||||
message.success({
|
||||
content: 'User has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
dispatch(userActions.setSavedUser({...savedUser, success: false}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
} else if (savedUser.error) {
|
||||
let errorMsg = "Failed to update user"
|
||||
switch (savedUser.error.statusCode) {
|
||||
case 412:
|
||||
case 403:
|
||||
if (savedUser.error.data) {
|
||||
errorMsg = capitalize(savedUser.error.data.message)
|
||||
}
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setSavedUser({...savedUser, error: null}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
}
|
||||
}, [savedUser])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
if (b) {
|
||||
setGroupPopupVisible(key)
|
||||
} else {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
},
|
||||
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const handleEditUser = (user: UserDataTable) => {
|
||||
dispatch(userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
is_service_user: user.is_service_user,
|
||||
is_blocked: user.is_blocked
|
||||
} as User));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
const handleBlockUser = (block: boolean, user: UserDataTable) => {
|
||||
if (block) {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Are you sure you want to block " + user.name + "?",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
<Paragraph>Blocking this user will disconnect their devices and disable dashboard access.</Paragraph>
|
||||
</Space>,
|
||||
onOk() {
|
||||
let userToSave = createUserToSave(user, block)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
} else {
|
||||
let userToSave = createUserToSave(user, block)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const createUserToSave = (values: UserDataTable, block: boolean): UserToSave => {
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: Array.of(),
|
||||
auto_groups: values.auto_groups,
|
||||
is_service_user: values.is_service_user,
|
||||
is_blocked: block
|
||||
} as UserToSave
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group;
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${
|
||||
!_g.peers_count || parseInt(_g.peers_count) !== 1 ? "peers" : "peer"
|
||||
} `;
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag color="blue" style={{ marginRight: 3 }}>
|
||||
{_g.name}
|
||||
</Tag>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const mainContent = <Space direction="vertical">{content}</Space>;
|
||||
let popoverPlacement = "top";
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{padding: "0px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Paragraph>Manage users and their
|
||||
permissions.{(window.location.hostname == "app.netbird.io") ? "Same-domain email users are added automatically on first sign-in." : ""}</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
{(isNetBirdHosted() || isLocalDev()) &&
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickInviteUser}>Invite user</Button>
|
||||
</Col>
|
||||
</Row>}
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
|
||||
type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Email" dataIndex="email"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).email.includes(value)}
|
||||
sorter={(a, b) => ((a as any).email.localeCompare((b as any).email))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
const btn = <Button type="text"
|
||||
onClick={() => handleEditUser(record as UserDataTable)}
|
||||
className="tooltip-label">
|
||||
<Text
|
||||
strong>{(text && text.trim() !== "") ? text : (record as User).id}</Text>
|
||||
<Popover
|
||||
placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={(b: boolean) =>
|
||||
onPopoverVisibleChange(b, userToAction.key)
|
||||
}
|
||||
open={groupPopupVisible === userToAction.key}
|
||||
content={mainContent}
|
||||
title={null}
|
||||
>
|
||||
{btn}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
</Button>
|
||||
useEffect(() => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible("");
|
||||
}
|
||||
}, [updateUserDrawerVisible]);
|
||||
|
||||
if ((record as User).is_current) {
|
||||
return <div>{btn}
|
||||
<Tag color="blue">me</Tag>
|
||||
</div>
|
||||
}
|
||||
const createKey = "saving";
|
||||
useEffect(() => {
|
||||
if (savedUser.loading) {
|
||||
message.loading({
|
||||
content: "Saving...",
|
||||
key: createKey,
|
||||
duration: 0,
|
||||
style: styleNotification,
|
||||
});
|
||||
} else if (savedUser.success) {
|
||||
message.success({
|
||||
content: "User has been successfully saved.",
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
dispatch(userActions.setSavedUser({ ...savedUser, success: false }));
|
||||
dispatch(userActions.resetSavedUser(null));
|
||||
} else if (savedUser.error) {
|
||||
let errorMsg = "Failed to update user";
|
||||
switch (savedUser.error.statusCode) {
|
||||
case 412:
|
||||
case 403:
|
||||
if (savedUser.error.data) {
|
||||
errorMsg = capitalize(savedUser.error.data.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(userActions.setSavedUser({ ...savedUser, error: null }));
|
||||
dispatch(userActions.resetSavedUser(null));
|
||||
}
|
||||
}, [savedUser]);
|
||||
|
||||
if ((record as User).status === "invited") {
|
||||
return <div>{btn}
|
||||
<Tag color="gold">invited</Tag>
|
||||
</div>
|
||||
}
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible("");
|
||||
} else {
|
||||
if (b) {
|
||||
setGroupPopupVisible(key);
|
||||
} else {
|
||||
setGroupPopupVisible("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ((record as User).status === "blocked") {
|
||||
return <div>{btn}
|
||||
<Tag color="red">blocked</Tag>
|
||||
</div>
|
||||
}
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (
|
||||
<Button type="text" onClick={() => onClickEdit()}>
|
||||
View
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
const actionsMenu = <Menu items={itemsMenuAction}></Menu>;
|
||||
|
||||
return btn
|
||||
}}
|
||||
/>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: UserDataTable, index) => {
|
||||
return renderPopoverGroups(text, record.auto_groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Role" dataIndex="role"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).role.includes(value)}
|
||||
sorter={(a, b) => ((a as any).role.localeCompare((b as any).role))}/>
|
||||
{isAdmin && (
|
||||
<Column title="Block user" align="center" width="150px" dataIndex="is_blocked"
|
||||
render={(e, record: UserDataTable, index) => {
|
||||
let witch = <Switch size={"small"} checked={e}
|
||||
disabled={record.is_current}
|
||||
onClick={(active: boolean) => {
|
||||
handleBlockUser(active, record)
|
||||
}}
|
||||
/>
|
||||
const handleEditUser = (user: UserDataTable) => {
|
||||
dispatch(
|
||||
userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
is_service_user: user.is_service_user,
|
||||
is_blocked: user.is_blocked,
|
||||
} as User)
|
||||
);
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
};
|
||||
|
||||
if (record.is_current) {
|
||||
return <Tooltip
|
||||
title="You can't block or unblock yourself">
|
||||
<Empty image={""} description={""} style={{height: "1px", width: "auto"}}/>
|
||||
</Tooltip>
|
||||
}
|
||||
const handleBlockUser = (block: boolean, user: UserDataTable) => {
|
||||
if (block) {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: (
|
||||
<span className="font-500">
|
||||
Are you sure you want to block {user.name} ?
|
||||
</span>
|
||||
),
|
||||
width: 600,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
<Paragraph>
|
||||
Blocking this user will disconnect their devices and disable
|
||||
dashboard access.
|
||||
</Paragraph>
|
||||
</Space>
|
||||
),
|
||||
onOk() {
|
||||
let userToSave = createUserToSave(user, block);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
} else {
|
||||
let userToSave = createUserToSave(user, block);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return witch
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<InviteUserPopup/>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const createUserToSave = (
|
||||
values: UserDataTable,
|
||||
block: boolean
|
||||
): UserToSave => {
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: Array.of(),
|
||||
auto_groups: values.auto_groups,
|
||||
is_service_user: values.is_service_user,
|
||||
is_blocked: block,
|
||||
} as UserToSave;
|
||||
};
|
||||
|
||||
export default RegularUsers;
|
||||
return (
|
||||
<>
|
||||
<Container style={{ padding: "0px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Paragraph>
|
||||
Manage users and their permissions.
|
||||
{window.location.hostname == "app.netbird.io"
|
||||
? "Same-domain email users are added automatically on first sign-in."
|
||||
: ""}
|
||||
</Paragraph>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
style={{ display: "flex" }}
|
||||
>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input
|
||||
allowClear
|
||||
value={textToSearch}
|
||||
onPressEnter={searchDataTable}
|
||||
placeholder="Search..."
|
||||
onChange={onChangeTextToSearch}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
options={pageSizeOptions}
|
||||
onChange={onChangePageSize}
|
||||
className="select-rows-per-page-en"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
{(isNetBirdHosted() || isLocalDev()) && (
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickInviteUser}>
|
||||
Invite user
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{failed && (
|
||||
<Alert
|
||||
message={failed.message}
|
||||
description={failed.data ? failed.data.message : " "}
|
||||
type="error"
|
||||
showIcon
|
||||
closable
|
||||
/>
|
||||
)}
|
||||
<Card bodyStyle={{ padding: 0 }}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total, range) =>
|
||||
`Showing ${range[0]} to ${range[1]} of ${total} users`,
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{ x: true }}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
>
|
||||
<Column
|
||||
title="Email"
|
||||
dataIndex="email"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).email.includes(value)
|
||||
}
|
||||
sorter={(a, b) =>
|
||||
(a as any).email.localeCompare((b as any).email)
|
||||
}
|
||||
defaultSortOrder="ascend"
|
||||
render={(text, record, index) => {
|
||||
const btn = (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() =>
|
||||
handleEditUser(record as UserDataTable)
|
||||
}
|
||||
className="tooltip-label"
|
||||
>
|
||||
<Text className="font-500">
|
||||
{text && text.trim() !== ""
|
||||
? text
|
||||
: (record as User).id}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
|
||||
if ((record as User).is_current) {
|
||||
return (
|
||||
<div>
|
||||
{btn}
|
||||
<Tag color="blue">me</Tag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ((record as User).status === "invited") {
|
||||
return (
|
||||
<div>
|
||||
{btn}
|
||||
<Tag color="gold">invited</Tag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ((record as User).status === "blocked") {
|
||||
return (
|
||||
<div>
|
||||
{btn}
|
||||
<Tag color="red">blocked</Tag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return btn;
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Name"
|
||||
dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).name.includes(value)
|
||||
}
|
||||
sorter={(a, b) =>
|
||||
(a as any).name.localeCompare((b as any).name)
|
||||
}
|
||||
/>
|
||||
<Column
|
||||
title="Groups"
|
||||
dataIndex="groupsCount"
|
||||
align="center"
|
||||
render={(text, record: UserDataTable, index) => {
|
||||
return renderPopoverGroups(
|
||||
text,
|
||||
record.auto_groups,
|
||||
record
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Role"
|
||||
dataIndex="role"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).role.includes(value)
|
||||
}
|
||||
sorter={(a, b) =>
|
||||
(a as any).role.localeCompare((b as any).role)
|
||||
}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<Column
|
||||
title="Block user"
|
||||
align="center"
|
||||
width="150px"
|
||||
dataIndex="is_blocked"
|
||||
render={(e, record: UserDataTable, index) => {
|
||||
let witch = (
|
||||
<Switch
|
||||
size={"small"}
|
||||
checked={e}
|
||||
disabled={record.is_current}
|
||||
onClick={(active: boolean) => {
|
||||
handleBlockUser(active, record);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (record.is_current) {
|
||||
return (
|
||||
<Tooltip title="You can't block or unblock yourself">
|
||||
<Empty
|
||||
image={""}
|
||||
description={""}
|
||||
style={{ height: "1px", width: "auto" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return witch;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<InviteUserPopup />
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegularUsers;
|
||||
|
||||
@@ -280,7 +280,7 @@ export const Routes = () => {
|
||||
let name = routeToAction ? routeToAction.network_id : "";
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: 'Delete network route "' + name + '"',
|
||||
title: <span className="font-500">Delete network route {name}</span>,
|
||||
width: 600,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
@@ -320,8 +320,6 @@ export const Routes = () => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const onClickViewRoute = () => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(false));
|
||||
dispatch(
|
||||
@@ -365,11 +363,15 @@ export const Routes = () => {
|
||||
checked: boolean
|
||||
) => {
|
||||
let label = record.network_id ? record.network_id : record.network;
|
||||
let tittle = 'Enable Masquerade for "' + label + '"?';
|
||||
let tittle = (
|
||||
<span className="font-500">Enable Masquerade for {label} ?</span>
|
||||
);
|
||||
let content = masqueradeDisabledMSG;
|
||||
|
||||
if (!checked) {
|
||||
tittle = 'Disable Masquerade for "' + label + '"?';
|
||||
tittle = (
|
||||
<span className="font-500">Disable Masquerade for {label} ?</span>
|
||||
);
|
||||
content = masqueradeEnabledMSG;
|
||||
}
|
||||
|
||||
@@ -571,8 +573,8 @@ export const Routes = () => {
|
||||
<Container className="container-main">
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Network Routes</Title>
|
||||
<Paragraph>
|
||||
<Title className="page-heading">Network Routes</Title>
|
||||
<Paragraph type="secondary">
|
||||
Network routes allow you to create routes to access other networks
|
||||
without installing NetBird on every resource.
|
||||
</Paragraph>
|
||||
@@ -692,7 +694,7 @@ export const Routes = () => {
|
||||
title={desc !== "" ? desc : "no description"}
|
||||
arrowPointAtCenter
|
||||
>
|
||||
<Text strong>{text}</Text>
|
||||
<Text className="font-500">{text}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,263 +1,373 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from '../store/user';
|
||||
import {Container} from "../components/Container";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as userActions } from "../store/user";
|
||||
import { Container } from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
message, Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {User} from "../store/user/types";
|
||||
import {filter} from "lodash";
|
||||
import { User } from "../store/user/types";
|
||||
import { filter } from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { actions as groupActions } from "../store/group";
|
||||
import { capitalize, isLocalDev, isNetBirdHosted } from "../utils/common";
|
||||
import { usePageSizeHelpers } from "../utils/pageSize";
|
||||
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Column } = Table;
|
||||
|
||||
interface UserDataTable extends User {
|
||||
key: string
|
||||
key: string;
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
const styleNotification = { marginTop: 85 };
|
||||
|
||||
export const ServiceUsers = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const users = useSelector((state: RootState) => state.user.serviceUsers);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const deletedUser = useSelector((state: RootState) => state.user.deletedUser)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
const users = useSelector((state: RootState) => state.user.serviceUsers);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const updateUserDrawerVisible = useSelector(
|
||||
(state: RootState) => state.user.updateUserDrawerVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
const deletedUser = useSelector((state: RootState) => state.user.deletedUser);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [textToSearch, setTextToSearch] = useState("");
|
||||
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
|
||||
|
||||
const transformDataTable = (d: User[]): UserDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as UserDataTable))
|
||||
const transformDataTable = (d: User[]): UserDataTable[] => {
|
||||
return d.map((p) => ({ key: p.id, ...p } as UserDataTable));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
userActions.getServiceUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, [savedUser, deletedUser]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(users));
|
||||
}, [users, groups]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()));
|
||||
}, [textToSearch]);
|
||||
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim();
|
||||
let f: User[] = filter(
|
||||
users,
|
||||
(f: User) =>
|
||||
(f.email || f.id).toLowerCase().includes(t) ||
|
||||
f.name.toLowerCase().includes(t) ||
|
||||
f.role.includes(t) ||
|
||||
t === ""
|
||||
) as User[];
|
||||
return f;
|
||||
};
|
||||
|
||||
const onChangeTextToSearch = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setTextToSearch(e.target.value);
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable();
|
||||
setDataTable(transformDataTable(data));
|
||||
};
|
||||
|
||||
const onClickCreateServiceUser = () => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(true));
|
||||
};
|
||||
|
||||
const createKey = "saving";
|
||||
useEffect(() => {
|
||||
if (savedUser.loading) {
|
||||
message.loading({
|
||||
content: "Saving...",
|
||||
key: createKey,
|
||||
duration: 0,
|
||||
style: styleNotification,
|
||||
});
|
||||
} else if (savedUser.success) {
|
||||
message.success({
|
||||
content: "User has been successfully saved.",
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
dispatch(userActions.setSavedUser({ ...savedUser, success: false }));
|
||||
dispatch(userActions.resetSavedUser(null));
|
||||
} else if (savedUser.error) {
|
||||
let errorMsg = "Failed to update user";
|
||||
switch (savedUser.error.statusCode) {
|
||||
case 412:
|
||||
case 403:
|
||||
if (savedUser.error.data) {
|
||||
errorMsg = capitalize(savedUser.error.data.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(userActions.setSavedUser({ ...savedUser, error: null }));
|
||||
dispatch(userActions.resetSavedUser(null));
|
||||
}
|
||||
}, [savedUser]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getServiceUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [savedUser, deletedUser])
|
||||
const handleEditUser = (user: UserDataTable) => {
|
||||
dispatch(
|
||||
userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
is_service_user: user.is_service_user,
|
||||
} as User)
|
||||
);
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(users))
|
||||
}, [users, groups])
|
||||
const handleDeleteUser = (user: UserDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: <span className="font-500">Delete token {user.name}</span>,
|
||||
width: 500,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
<Paragraph>
|
||||
Are you sure you want to delete this service user?
|
||||
</Paragraph>
|
||||
</Space>
|
||||
),
|
||||
onOk() {
|
||||
dispatch(
|
||||
userActions.deleteUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: user.id,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
userActions.getServiceUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: User[] = filter(users, (f: User) =>
|
||||
((f.email || f.id).toLowerCase().includes(t) || f.name.toLowerCase().includes(t) || f.role.includes(t) || t === "")
|
||||
) as User[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onClickCreateServiceUser = () => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(true));
|
||||
}
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedUser.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedUser.success) {
|
||||
message.success({
|
||||
content: 'User has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
dispatch(userActions.setSavedUser({...savedUser, success: false}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
} else if (savedUser.error) {
|
||||
let errorMsg = "Failed to update user"
|
||||
switch (savedUser.error.statusCode) {
|
||||
case 412:
|
||||
case 403:
|
||||
if (savedUser.error.data) {
|
||||
errorMsg = capitalize(savedUser.error.data.message)
|
||||
}
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setSavedUser({...savedUser, error: null}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
}
|
||||
}, [savedUser])
|
||||
|
||||
const handleEditUser = (user: UserDataTable) => {
|
||||
dispatch(userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
is_service_user: user.is_service_user,
|
||||
} as User));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
const handleDeleteUser = (user: UserDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete token \"" + user.name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to delete this service user?</Paragraph>
|
||||
</Space>,
|
||||
onOk() {
|
||||
dispatch(userActions.deleteUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: user.id
|
||||
}));
|
||||
dispatch(userActions.getServiceUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!user && <Container style={{padding: "0px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{maxWidth: "70%"}}>Service users are non-login users that are not associated with any specific person. Network administrators
|
||||
use them to create tokens for API access to avoid losing automated access to critical systems when employees leave the company.</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickCreateServiceUser}>Create Service User</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} service users`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
return <Button type="text"
|
||||
onClick={() => handleEditUser(record as UserDataTable)}>
|
||||
<Text strong>{(text && text.trim() !== "") ? text : (record as User).name}</Text>
|
||||
</Button>
|
||||
}}/>
|
||||
<Column title="Status" dataIndex="status"
|
||||
align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).status.includes(value)}
|
||||
sorter={(a, b) => ((a as any).status.localeCompare((b as any).status))}
|
||||
render={(text, record, index) => {
|
||||
if (text == "active") {
|
||||
return <Tag color="green">{text}</Tag>
|
||||
} else if (text === "invited"){
|
||||
return <Tag color="gold">{text}</Tag>
|
||||
}
|
||||
return <Tag color="red">{text}</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Role" dataIndex="role"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).role.includes(value)}
|
||||
sorter={(a, b) => ((a as any).role.localeCompare((b as any).role))}/>
|
||||
<Column title="" align="center" width="250px"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button danger={true} type={"text"} style={{marginLeft: "3px", marginRight: "3px"}}
|
||||
onClick={() => {
|
||||
let userRecord = (record as UserDataTable)
|
||||
handleDeleteUser(userRecord)
|
||||
}}
|
||||
>Delete</Button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
return (
|
||||
<>
|
||||
{!user && (
|
||||
<Container style={{ padding: "0px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ maxWidth: "70%" }}>
|
||||
Service users are non-login users that are not associated with
|
||||
any specific person. Network administrators use them to create
|
||||
tokens for API access to avoid losing automated access to
|
||||
critical systems when employees leave the company.
|
||||
</Paragraph>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
style={{ display: "flex" }}
|
||||
>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input
|
||||
allowClear
|
||||
value={textToSearch}
|
||||
onPressEnter={searchDataTable}
|
||||
placeholder="Search..."
|
||||
onChange={onChangeTextToSearch}
|
||||
/>
|
||||
</Col>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
>
|
||||
<Space size="middle">
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
options={pageSizeOptions}
|
||||
onChange={onChangePageSize}
|
||||
className="select-rows-per-page-en"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClickCreateServiceUser}
|
||||
>
|
||||
Create Service User
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>}
|
||||
<AddServiceUserPopup/>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{failed && (
|
||||
<Alert
|
||||
message={failed.message}
|
||||
description={failed.data ? failed.data.message : " "}
|
||||
type="error"
|
||||
showIcon
|
||||
closable
|
||||
/>
|
||||
)}
|
||||
<Card bodyStyle={{ padding: 0 }}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total, range) =>
|
||||
`Showing ${range[0]} to ${range[1]} of ${total} service users`,
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{ x: true }}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
>
|
||||
<Column
|
||||
title="Name"
|
||||
dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).name.includes(value)
|
||||
}
|
||||
sorter={(a, b) =>
|
||||
(a as any).name.localeCompare((b as any).name)
|
||||
}
|
||||
defaultSortOrder="ascend"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() =>
|
||||
handleEditUser(record as UserDataTable)
|
||||
}
|
||||
>
|
||||
<Text className="font-500">
|
||||
{text && text.trim() !== ""
|
||||
? text
|
||||
: (record as User).name}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Status"
|
||||
dataIndex="status"
|
||||
align="center"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).status.includes(value)
|
||||
}
|
||||
sorter={(a, b) =>
|
||||
(a as any).status.localeCompare((b as any).status)
|
||||
}
|
||||
render={(text, record, index) => {
|
||||
if (text == "active") {
|
||||
return <Tag color="green">{text}</Tag>;
|
||||
} else if (text === "invited") {
|
||||
return <Tag color="gold">{text}</Tag>;
|
||||
}
|
||||
return <Tag color="red">{text}</Tag>;
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Role"
|
||||
dataIndex="role"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).role.includes(value)
|
||||
}
|
||||
sorter={(a, b) =>
|
||||
(a as any).role.localeCompare((b as any).role)
|
||||
}
|
||||
/>
|
||||
<Column
|
||||
title=""
|
||||
align="center"
|
||||
width="250px"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button
|
||||
danger={true}
|
||||
type={"text"}
|
||||
style={{ marginLeft: "3px", marginRight: "3px" }}
|
||||
onClick={() => {
|
||||
let userRecord = record as UserDataTable;
|
||||
handleDeleteUser(userRecord);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
<AddServiceUserPopup />
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceUsers;
|
||||
export default ServiceUsers;
|
||||
|
||||
@@ -1,227 +1,298 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Button, Card, Col, Form, List, message, Modal, Radio, Row, Space, Typography,} from "antd";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {Container} from "../components/Container";
|
||||
import ExpiresInInput, {expiresInToSeconds, secondsToExpiresIn} from "./ExpiresInInput";
|
||||
import {checkExpiresIn} from "../utils/common";
|
||||
import {actions as accountActions} from "../store/account";
|
||||
import {Account, FormAccount} from "../store/account/types";
|
||||
import {ExclamationCircleOutlined, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
List,
|
||||
message,
|
||||
Modal,
|
||||
Radio,
|
||||
Row,
|
||||
Space,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
import { Container } from "../components/Container";
|
||||
import ExpiresInInput, {
|
||||
expiresInToSeconds,
|
||||
secondsToExpiresIn,
|
||||
} from "./ExpiresInInput";
|
||||
import { checkExpiresIn } from "../utils/common";
|
||||
import { actions as accountActions } from "../store/account";
|
||||
import { Account, FormAccount } from "../store/account/types";
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
QuestionCircleFilled,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
const styleNotification = { marginTop: 85 };
|
||||
|
||||
export const Settings = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
} = useGetGroupTagHelpers()
|
||||
const {} = useGetGroupTagHelpers();
|
||||
|
||||
const accounts = useSelector((state: RootState) => state.account.data);
|
||||
const failed = useSelector((state: RootState) => state.account.failed);
|
||||
const loading = useSelector((state: RootState) => state.account.loading);
|
||||
const updatedAccount = useSelector((state: RootState) => state.account.updatedAccount);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const [formAccount, setFormAccount] = useState({} as FormAccount);
|
||||
const [accountToAction, setAccountToAction] = useState({} as FormAccount);
|
||||
const [formPeerExpirationEnabled, setFormPeerExpirationEnabled] = useState(true);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const accounts = useSelector((state: RootState) => state.account.data);
|
||||
const failed = useSelector((state: RootState) => state.account.failed);
|
||||
const loading = useSelector((state: RootState) => state.account.loading);
|
||||
const updatedAccount = useSelector(
|
||||
(state: RootState) => state.account.updatedAccount
|
||||
);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const [formAccount, setFormAccount] = useState({} as FormAccount);
|
||||
const [accountToAction, setAccountToAction] = useState({} as FormAccount);
|
||||
const [formPeerExpirationEnabled, setFormPeerExpirationEnabled] =
|
||||
useState(true);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(accountActions.getAccounts.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
accountActions.getAccounts.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (accounts.length < 1) {
|
||||
console.debug("invalid account data returned from the Management API", accounts)
|
||||
return
|
||||
useEffect(() => {
|
||||
if (accounts.length < 1) {
|
||||
console.debug(
|
||||
"invalid account data returned from the Management API",
|
||||
accounts
|
||||
);
|
||||
return;
|
||||
}
|
||||
let account = accounts[0];
|
||||
|
||||
let fAccount = {
|
||||
id: account.id,
|
||||
settings: account.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(
|
||||
account.settings.peer_login_expiration,
|
||||
["hour", "day"]
|
||||
),
|
||||
peer_login_expiration_enabled:
|
||||
account.settings.peer_login_expiration_enabled,
|
||||
} as FormAccount;
|
||||
setFormAccount(fAccount);
|
||||
setFormPeerExpirationEnabled(fAccount.peer_login_expiration_enabled);
|
||||
form.setFieldsValue(fAccount);
|
||||
}, [accounts]);
|
||||
|
||||
const updatingSettings = "updating_settings";
|
||||
useEffect(() => {
|
||||
if (updatedAccount.loading) {
|
||||
message.loading({
|
||||
content: "Saving...",
|
||||
key: updatingSettings,
|
||||
duration: 0,
|
||||
style: styleNotification,
|
||||
});
|
||||
} else if (updatedAccount.success) {
|
||||
message.success({
|
||||
content: "Account settings have been successfully saved.",
|
||||
key: updatingSettings,
|
||||
duration: 2,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(
|
||||
accountActions.setUpdateAccount({ ...updatedAccount, success: false })
|
||||
);
|
||||
dispatch(accountActions.resetUpdateAccount(null));
|
||||
let fAccount = {
|
||||
id: updatedAccount.data.id,
|
||||
settings: updatedAccount.data.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(
|
||||
updatedAccount.data.settings.peer_login_expiration,
|
||||
["hour", "day"]
|
||||
),
|
||||
peer_login_expiration_enabled:
|
||||
updatedAccount.data.settings.peer_login_expiration_enabled,
|
||||
} as FormAccount;
|
||||
setFormAccount(fAccount);
|
||||
} else if (updatedAccount.error) {
|
||||
let errorMsg = "Failed to update account settings";
|
||||
switch (updatedAccount.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg =
|
||||
"Failed to update account settings. You might not have enough permissions.";
|
||||
break;
|
||||
default:
|
||||
errorMsg = updatedAccount.error.data.message
|
||||
? updatedAccount.error.data.message
|
||||
: errorMsg;
|
||||
break;
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: updatingSettings,
|
||||
duration: 5,
|
||||
style: styleNotification,
|
||||
});
|
||||
}
|
||||
}, [updatedAccount]);
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
confirmSave(values);
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again";
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0];
|
||||
}
|
||||
let account = accounts[0]
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let fAccount = {
|
||||
id: account.id,
|
||||
settings: account.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(account.settings.peer_login_expiration, ["hour", "day"]),
|
||||
peer_login_expiration_enabled: account.settings.peer_login_expiration_enabled
|
||||
} as FormAccount
|
||||
setFormAccount(fAccount)
|
||||
setFormPeerExpirationEnabled(fAccount.peer_login_expiration_enabled)
|
||||
form.setFieldsValue(fAccount)
|
||||
}, [accounts])
|
||||
const createAccountToSave = (values: FormAccount): Account => {
|
||||
return {
|
||||
id: formAccount.id,
|
||||
settings: {
|
||||
peer_login_expiration: expiresInToSeconds(
|
||||
values.peer_login_expiration_formatted
|
||||
),
|
||||
peer_login_expiration_enabled: values.peer_login_expiration_enabled,
|
||||
},
|
||||
} as Account;
|
||||
};
|
||||
|
||||
const updatingSettings = 'updating_settings';
|
||||
useEffect(() => {
|
||||
if (updatedAccount.loading) {
|
||||
message.loading({content: 'Saving...', key: updatingSettings, duration: 0, style: styleNotification});
|
||||
} else if (updatedAccount.success) {
|
||||
message.success({
|
||||
content: 'Account settings have been successfully saved.',
|
||||
key: updatingSettings,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(accountActions.setUpdateAccount({...updatedAccount, success: false}));
|
||||
dispatch(accountActions.resetUpdateAccount(null))
|
||||
let fAccount = {
|
||||
id: updatedAccount.data.id,
|
||||
settings: updatedAccount.data.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(updatedAccount.data.settings.peer_login_expiration, ["hour", "day"]),
|
||||
peer_login_expiration_enabled: updatedAccount.data.settings.peer_login_expiration_enabled
|
||||
} as FormAccount
|
||||
setFormAccount(fAccount)
|
||||
} else if (updatedAccount.error) {
|
||||
let errorMsg = "Failed to update account settings"
|
||||
switch (updatedAccount.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update account settings. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = updatedAccount.error.data.message ? updatedAccount.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: updatingSettings,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
}
|
||||
}, [updatedAccount])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
confirmSave(values)
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
const confirmSave = (newValues: FormAccount) => {
|
||||
if (
|
||||
newValues.peer_login_expiration_enabled !=
|
||||
formAccount.peer_login_expiration_enabled
|
||||
) {
|
||||
let content = newValues.peer_login_expiration_enabled
|
||||
? "Enabling peer expiration will cause some peers added with the SSO login to disconnect, and re-authentication will be required. Do you want to enable peer login expiration?"
|
||||
: "Disabling peer expiration will cause peers added with the SSO login never to expire. For security reasons, keeping peers expiring periodically is usually better. Do you want to disable peer login expiration?";
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: "Before you update your account settings.",
|
||||
width: 600,
|
||||
content: content,
|
||||
onOk() {
|
||||
saveAccount(newValues);
|
||||
},
|
||||
onCancel() {},
|
||||
});
|
||||
} else {
|
||||
saveAccount(newValues);
|
||||
}
|
||||
};
|
||||
|
||||
const createAccountToSave = (values: FormAccount): Account => {
|
||||
return {
|
||||
id: formAccount.id,
|
||||
settings: {
|
||||
peer_login_expiration: expiresInToSeconds(values.peer_login_expiration_formatted),
|
||||
peer_login_expiration_enabled: values.peer_login_expiration_enabled
|
||||
}
|
||||
} as Account
|
||||
}
|
||||
const saveAccount = (newValues: FormAccount) => {
|
||||
let accountToSave = createAccountToSave(newValues);
|
||||
dispatch(
|
||||
accountActions.updateAccount.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: accountToSave,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const confirmSave = (newValues: FormAccount) => {
|
||||
if (newValues.peer_login_expiration_enabled != formAccount.peer_login_expiration_enabled) {
|
||||
let content = newValues.peer_login_expiration_enabled ? "Enabling peer expiration will cause some peers added with the SSO login to disconnect, and re-authentication will be required. Do you want to enable peer login expiration?" : "Disabling peer expiration will cause peers added with the SSO login never to expire. For security reasons, keeping peers expiring periodically is usually better. Do you want to disable peer login expiration?"
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Before you update your account settings.",
|
||||
width: 600,
|
||||
content: content,
|
||||
onOk() {
|
||||
saveAccount(newValues)
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
saveAccount(newValues)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title className="page-heading">Settings</Title>
|
||||
<Paragraph type="secondary">Manage your account's settings</Paragraph>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
style={{ display: "flex" }}
|
||||
>
|
||||
<Card bodyStyle={{ padding: 0 }}>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"} style={{ display: "flex" }}>
|
||||
<Card
|
||||
title="Authentication"
|
||||
loading={loading}
|
||||
defaultValue={"Enabled"}
|
||||
>
|
||||
<Form.Item
|
||||
label="Peer login expiration"
|
||||
name="peer_login_expiration_enabled"
|
||||
tooltip="Peer login expiration allows to periodically request re-authentication of peers that were added with the SSO login. You can disable the expiration per peer in the peers tab."
|
||||
//rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Radio.Group
|
||||
options={[
|
||||
{ label: "Enabled", value: true },
|
||||
{
|
||||
label: "Disabled",
|
||||
value: false,
|
||||
},
|
||||
]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={function (e) {
|
||||
setFormPeerExpirationEnabled(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="peer_login_expiration_formatted"
|
||||
label="Peer login expires in"
|
||||
tooltip="Time after which every peer added with SSO login will require re-authentication."
|
||||
rules={[{ validator: checkExpiresIn }]}
|
||||
>
|
||||
<ExpiresInInput
|
||||
disabled={!formPeerExpirationEnabled}
|
||||
options={Array.of(
|
||||
{ key: "hour", title: "Hours" },
|
||||
{
|
||||
key: "day",
|
||||
title: "Days",
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
icon={<QuestionCircleFilled />}
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://docs.netbird.io/how-to/enforce-periodic-user-authentication"
|
||||
>
|
||||
Learn more about login expiration
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{ textAlign: "center" }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const saveAccount = (newValues: FormAccount) => {
|
||||
let accountToSave = createAccountToSave(newValues)
|
||||
dispatch(accountActions.updateAccount.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: accountToSave
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Settings</Title>
|
||||
<Paragraph>Manage your account's settings</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"}
|
||||
style={{display: 'flex'}}>
|
||||
<Card
|
||||
title="Authentication"
|
||||
loading={loading}
|
||||
defaultValue={"Enabled"}
|
||||
>
|
||||
<Form.Item
|
||||
label="Peer login expiration"
|
||||
name="peer_login_expiration_enabled"
|
||||
tooltip="Peer login expiration allows to periodically request re-authentication of peers that were added with the SSO login. You can disable the expiration per peer in the peers tab."
|
||||
//rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Radio.Group
|
||||
options={[{label: 'Enabled', value: true}, {
|
||||
label: 'Disabled',
|
||||
value: false
|
||||
}]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={function (e) {
|
||||
setFormPeerExpirationEnabled(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="peer_login_expiration_formatted"
|
||||
label="Peer login expires in"
|
||||
tooltip="Time after which every peer added with SSO login will require re-authentication."
|
||||
rules={[{validator: checkExpiresIn}]}>
|
||||
<ExpiresInInput
|
||||
disabled={!formPeerExpirationEnabled}
|
||||
options={Array.of({key: "hour", title: "Hours"}, {
|
||||
key: "day",
|
||||
title: "Days"
|
||||
})
|
||||
}/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/how-to/enforce-periodic-user-authentication">Learn more about login expiration</Button>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{textAlign: 'center'}}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
export default Settings;
|
||||
|
||||
@@ -217,8 +217,8 @@ export const SetupKeys = () => {
|
||||
let name = setupKeyToAction ? setupKeyToAction.name : "";
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: 'Revoke setupKey "' + name + '"',
|
||||
width: 600,
|
||||
title: <span className="font-500">Revoke setupKey {name}</span>,
|
||||
width: 500,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to revoke key?</Paragraph>
|
||||
@@ -384,12 +384,8 @@ export const SetupKeys = () => {
|
||||
{!setupEditKeyVisible && (
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Setup Keys</Title>
|
||||
<Paragraph
|
||||
style={{
|
||||
color: dataTable.length ? "black" : "#818183",
|
||||
}}
|
||||
>
|
||||
<Title className="page-heading">Setup Keys</Title>
|
||||
<Paragraph type="secondary">
|
||||
A list of all the setup keys in your account including their
|
||||
name, state, type and expiration.
|
||||
</Paragraph>
|
||||
@@ -444,7 +440,7 @@ export const SetupKeys = () => {
|
||||
type="primary"
|
||||
onClick={onClickAddNewSetupKey}
|
||||
>
|
||||
Add key
|
||||
Add Key
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -569,7 +565,7 @@ export const SetupKeys = () => {
|
||||
className="tooltip-label"
|
||||
>
|
||||
{" "}
|
||||
<Text strong>{text}</Text>
|
||||
<Text className="font-500">{text}</Text>
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user