Update site fonts (#208)

This commit is contained in:
Sarooj bukhari
2023-06-17 00:00:54 +05:00
committed by GitHub
parent 09e6de74ee
commit 312c60dd45
22 changed files with 3103 additions and 1949 deletions

399
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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:

View File

@@ -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>

View File

@@ -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} />
)}
</>
);
};

View File

@@ -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",
}}
>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -1,6 +1,6 @@
export interface Route {
id?: string
id?: string | null
description: string
enabled: boolean
peer: string

View File

@@ -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",
}}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>
);
}}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
}}