feat: Implement api key management (#403)
Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com>
This commit is contained in:
parent
e037c6c577
commit
33524ce3cc
210
package-lock.json
generated
210
package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"@mui/lab": "^5.0.0-alpha.89",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@mui/styles": "^5.8.6",
|
||||
"@mui/x-date-pickers": "^6.18.4",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@ -21,7 +22,7 @@
|
||||
"downshift": "^6.1.12",
|
||||
"export-from-json": "^1.7.3",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.5.2",
|
||||
"luxon": "^3.4.4",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
@ -2130,16 +2131,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
|
||||
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz",
|
||||
"integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
|
||||
@ -2678,6 +2684,40 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz",
|
||||
"integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
|
||||
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.4.2",
|
||||
"@floating-ui/utils": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz",
|
||||
"integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
||||
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||
@ -3935,11 +3975,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
|
||||
"integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
|
||||
"version": "7.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz",
|
||||
"integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
"@types/react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@ -3948,25 +3988,134 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "5.13.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.6.tgz",
|
||||
"integrity": "sha512-ggNlxl5NPSbp+kNcQLmSig6WVB0Id+4gOxhx644987v4fsji+CSXc+MFYLocFB/x4oHtzCUlSzbVHlJfP/fXoQ==",
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.0.tgz",
|
||||
"integrity": "sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.5",
|
||||
"@types/prop-types": "^15.7.5",
|
||||
"@types/react-is": "^18.2.0",
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@types/prop-types": "^15.7.11",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^18.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers": {
|
||||
"version": "6.18.4",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.4.tgz",
|
||||
"integrity": "sha512-YqJ6lxZHBIt344B3bvRAVbdYSQz4dcmJQXGcfvJTn26VdKjpgzjAqwhlbQhbAt55audJOWzGB99ImuQuljDROA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"@mui/base": "^5.0.0-beta.22",
|
||||
"@mui/utils": "^5.14.16",
|
||||
"@types/react-transition-group": "^4.4.8",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@mui/system": "^5.8.0",
|
||||
"date-fns": "^2.25.0",
|
||||
"date-fns-jalali": "^2.13.0-0",
|
||||
"dayjs": "^1.10.7",
|
||||
"luxon": "^3.0.2",
|
||||
"moment": "^2.29.4",
|
||||
"moment-hijri": "^2.1.2",
|
||||
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
},
|
||||
"date-fns": {
|
||||
"optional": true
|
||||
},
|
||||
"date-fns-jalali": {
|
||||
"optional": true
|
||||
},
|
||||
"dayjs": {
|
||||
"optional": true
|
||||
},
|
||||
"luxon": {
|
||||
"optional": true
|
||||
},
|
||||
"moment": {
|
||||
"optional": true
|
||||
},
|
||||
"moment-hijri": {
|
||||
"optional": true
|
||||
},
|
||||
"moment-jalaali": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers/node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.27.tgz",
|
||||
"integrity": "sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@floating-ui/react-dom": "^2.0.4",
|
||||
"@mui/types": "^7.2.11",
|
||||
"@mui/utils": "^5.15.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers/node_modules/clsx": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
@ -4844,9 +4993,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
||||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
||||
},
|
||||
"node_modules/@types/q": {
|
||||
"version": "1.5.5",
|
||||
@ -4884,18 +5033,10 @@
|
||||
"@types/react": "^17"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-is": {
|
||||
"version": "18.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz",
|
||||
"integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==",
|
||||
"dependencies": {
|
||||
"@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==",
|
||||
"version": "4.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
|
||||
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@ -13250,9 +13391,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz",
|
||||
"integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==",
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -16202,7 +16343,8 @@
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
"version": "0.15.1",
|
||||
|
@ -9,6 +9,7 @@
|
||||
"@mui/lab": "^5.0.0-alpha.89",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@mui/styles": "^5.8.6",
|
||||
"@mui/x-date-pickers": "^6.18.4",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@ -16,7 +17,7 @@
|
||||
"downshift": "^6.1.12",
|
||||
"export-from-json": "^1.7.3",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.5.2",
|
||||
"luxon": "^3.4.4",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
|
||||
import { isAuthenticated } from 'utilities/authUtilities';
|
||||
import { isAuthenticated, isApiKeyEnabled } from 'utilities/authUtilities';
|
||||
import { AuthWrapper } from 'utilities/AuthWrapper';
|
||||
|
||||
import HomePage from './pages/HomePage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import { AuthWrapper } from 'utilities/AuthWrapper';
|
||||
import RepoPage from 'pages/RepoPage';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import ExplorePage from 'pages/ExplorePage';
|
||||
import UserManagementPage from 'pages/UserManagementPage';
|
||||
|
||||
import './App.css';
|
||||
|
||||
@ -25,6 +26,7 @@ function App() {
|
||||
<Route path="/explore" element={<ExplorePage />} />
|
||||
<Route path="/image/:name" element={<RepoPage />} />
|
||||
<Route path="/image/:reponame/tag/:tag" element={<TagPage />} />
|
||||
{isApiKeyEnabled() && <Route path="/user/apikey" element={<UserManagementPage />} />}
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Route>
|
||||
<Route element={<AuthWrapper isLoggedIn={!isLoggedIn} redirect="/" />}>
|
||||
|
@ -67,11 +67,14 @@ const api = {
|
||||
return axios.put(urli, payload, config);
|
||||
},
|
||||
|
||||
delete(urli, abortSignal, cfg) {
|
||||
delete(urli, params, abortSignal, cfg) {
|
||||
let config = isEmpty(cfg) ? this.getRequestCfg() : cfg;
|
||||
if (!isEmpty(abortSignal) && isEmpty(config.signal)) {
|
||||
config = { ...config, signal: abortSignal };
|
||||
}
|
||||
if (!isEmpty(params)) {
|
||||
config = { ...config, params };
|
||||
}
|
||||
return axios.delete(urli, config);
|
||||
}
|
||||
};
|
||||
@ -81,6 +84,7 @@ const endpoints = {
|
||||
authConfig: `/v2/_zot/ext/mgmt`,
|
||||
openidAuth: `/zot/auth/login`,
|
||||
logout: `/zot/auth/logout`,
|
||||
apiKeys: '/zot/auth/apikey',
|
||||
deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`,
|
||||
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
||||
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
||||
|
@ -2,11 +2,17 @@ import React, { useState } from 'react';
|
||||
|
||||
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
|
||||
|
||||
import { getLoggedInUser, logoutUser } from '../../utilities/authUtilities';
|
||||
import { getLoggedInUser, logoutUser, isApiKeyEnabled } from '../../utilities/authUtilities';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
function UserAccountMenu() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const openMenu = Boolean(anchorEl);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const apiKeyManagement = () => {
|
||||
navigate('/user/apikey');
|
||||
};
|
||||
|
||||
const handleUserClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@ -37,6 +43,8 @@ function UserAccountMenu() {
|
||||
>
|
||||
<MenuItem onClick={handleUserClose}>{getLoggedInUser()}</MenuItem>
|
||||
<Divider />
|
||||
{isApiKeyEnabled() && <MenuItem onClick={apiKeyManagement}>API Keys</MenuItem>}
|
||||
<Divider />
|
||||
<MenuItem onClick={logoutUser}>Log out</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
|
@ -9,6 +9,8 @@ import { isEmpty, uniq } from 'lodash';
|
||||
import { api, endpoints } from '../../api';
|
||||
import { host } from '../../host';
|
||||
import { useParams, useNavigate, createSearchParams } from 'react-router-dom';
|
||||
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
||||
import { isAuthenticated } from 'utilities/authUtilities';
|
||||
|
||||
// components
|
||||
import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography, IconButton } from '@mui/material';
|
||||
@ -16,7 +18,11 @@ import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import Tags from './Tabs/Tags.jsx';
|
||||
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
|
||||
// placeholder images
|
||||
import repocube1 from '../../assets/repocube-1.png';
|
||||
@ -24,13 +30,7 @@ import repocube2 from '../../assets/repocube-2.png';
|
||||
import repocube3 from '../../assets/repocube-3.png';
|
||||
import repocube4 from '../../assets/repocube-4.png';
|
||||
|
||||
import Tags from './Tabs/Tags.jsx';
|
||||
import RepoDetailsMetadata from './RepoDetailsMetadata';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';
|
||||
import { isAuthenticated } from 'utilities/authUtilities';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import transform from 'utilities/transform';
|
||||
|
||||
import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse } from '@mui/material';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import transform from 'utilities/transform';
|
||||
import { useState } from 'react';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
|
@ -3,7 +3,10 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
import { host } from '../../host';
|
||||
import { mapToImage } from '../../utilities/objectModels';
|
||||
import { isEmpty, head } from 'lodash';
|
||||
|
||||
// components
|
||||
import {
|
||||
Card,
|
||||
@ -19,23 +22,21 @@ import {
|
||||
Typography,
|
||||
InputLabel
|
||||
} from '@mui/material';
|
||||
import TagDetailsMetadata from './TagDetailsMetadata';
|
||||
import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails';
|
||||
import HistoryLayers from './Tabs/HistoryLayers';
|
||||
import DependsOn from './Tabs/DependsOn';
|
||||
import IsDependentOn from './Tabs/IsDependentOn';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import ReferredBy from './Tabs/ReferredBy';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../../host';
|
||||
|
||||
// placeholder images
|
||||
import repocube1 from '../../assets/repocube-1.png';
|
||||
import repocube2 from '../../assets/repocube-2.png';
|
||||
import repocube3 from '../../assets/repocube-3.png';
|
||||
import repocube4 from '../../assets/repocube-4.png';
|
||||
import TagDetailsMetadata from './TagDetailsMetadata';
|
||||
import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails';
|
||||
import HistoryLayers from './Tabs/HistoryLayers';
|
||||
import DependsOn from './Tabs/DependsOn';
|
||||
import IsDependentOn from './Tabs/IsDependentOn';
|
||||
import { isEmpty, head } from 'lodash';
|
||||
import Loading from '../Shared/Loading';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import ReferredBy from './Tabs/ReferredBy';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import transform from '../../utilities/transform';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import transform from '../../utilities/transform';
|
||||
|
||||
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
|
||||
import PullCommandButton from 'components/Shared/PullCommandButton';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
display: 'flex',
|
||||
|
163
src/components/User/ApiKeys/ApiKeyCard.jsx
Normal file
163
src/components/User/ApiKeys/ApiKeyCard.jsx
Normal file
@ -0,0 +1,163 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse, Button } from '@mui/material';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import ApiKeyRevokeDialog from './ApiKeyRevokeDialog';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
border: '1px solid #E0E5EB',
|
||||
borderRadius: '0.75rem',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#52637A',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
padding: '1rem',
|
||||
backgroundColor: '#FFFFFF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
'&:last-child': {
|
||||
paddingBottom: '1rem'
|
||||
}
|
||||
},
|
||||
label: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
paddingRight: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem',
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
expirationDate: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem',
|
||||
textAlign: 'right'
|
||||
},
|
||||
revokeButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'right'
|
||||
},
|
||||
dropdownText: {
|
||||
color: '#1479FF',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
},
|
||||
dropdownButton: {
|
||||
color: '#1479FF',
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
dropdownContentBox: {
|
||||
boxSizing: 'border-box',
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#F7F7F7',
|
||||
borderRadius: '0.9rem',
|
||||
overflowWrap: 'break-word'
|
||||
},
|
||||
keyCardDivider: {
|
||||
margin: '1rem 0'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyCard(props) {
|
||||
const classes = useStyles();
|
||||
const { apiKey, onRevoke } = props;
|
||||
const [openDropdown, setOpenDropdown] = useState(false);
|
||||
const [apiKeyRevokeOpen, setApiKeyRevokeOpen] = useState(false);
|
||||
|
||||
const getExpirationDisplay = () => {
|
||||
const expDateTime = DateTime.fromISO(apiKey.expirationDate);
|
||||
return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`;
|
||||
};
|
||||
|
||||
const handleApiKeyRevokeDialogOpen = () => {
|
||||
setApiKeyRevokeOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card variant="outlined" className={classes.card}>
|
||||
<CardContent className={classes.content}>
|
||||
<Grid container alignItems="center" justifyContent="space-between">
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body1" className={classes.label}>
|
||||
{apiKey.label}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="body1" className={classes.expirationDate}>
|
||||
{getExpirationDisplay()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2} className={classes.revokeButton}>
|
||||
<Button color="error" variant="contained" onClick={handleApiKeyRevokeDialogOpen}>
|
||||
Revoke
|
||||
</Button>
|
||||
</Grid>
|
||||
{!isNil(apiKey.apiKey) && (
|
||||
<>
|
||||
<Grid item xs={12}>
|
||||
<Divider className={classes.keyCardDivider} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" onClick={() => setOpenDropdown((prevOpenState) => !prevOpenState)}>
|
||||
{!openDropdown ? (
|
||||
<KeyboardArrowRight className={classes.dropdownText} />
|
||||
) : (
|
||||
<KeyboardArrowDown className={classes.dropdownText} />
|
||||
)}
|
||||
<Typography className={classes.dropdownButton}>KEY</Typography>
|
||||
</Stack>
|
||||
<Collapse in={openDropdown} timeout="auto" unmountOnExit sx={{ marginTop: '1rem' }}>
|
||||
<Stack direction="column" spacing="1.2rem">
|
||||
<Typography variant="body1" align="left" className={classes.dropdownContentBox}>
|
||||
{apiKey.apiKey}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<ApiKeyRevokeDialog
|
||||
open={apiKeyRevokeOpen}
|
||||
setOpen={setApiKeyRevokeOpen}
|
||||
apiKey={apiKey}
|
||||
onConfirm={onRevoke}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyCard;
|
57
src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
Normal file
57
src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem'
|
||||
},
|
||||
apiKeyDisplay: {
|
||||
boxSizing: 'border-box',
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#F7F7F7',
|
||||
borderRadius: '0.9rem',
|
||||
overflowWrap: 'break-word'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyConfirmDialog(props) {
|
||||
const { open, setOpen, apiKey } = props;
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>Api Key "{apiKey?.label}" Created</DialogTitle>
|
||||
<DialogContent className={classes.apiKeyForm}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>Please copy the api key, you will not be able to see it once the page is refreshed</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1" align="center" className={classes.apiKeyDisplay}>
|
||||
{apiKey?.apiKey}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyConfirmDialog;
|
172
src/components/User/ApiKeys/ApiKeyDialog.jsx
Normal file
172
src/components/User/ApiKeys/ApiKeyDialog.jsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { isNil, isNumber } from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from 'host';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
TextField,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem'
|
||||
},
|
||||
apiKeyLabel: {
|
||||
paddingBottom: '1rem'
|
||||
},
|
||||
expirationDateContainer: {
|
||||
width: '100%'
|
||||
},
|
||||
expirationDateInput: {
|
||||
width: '100%'
|
||||
},
|
||||
expirationDateDisplay: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyDialog(props) {
|
||||
const { open, setOpen, onConfirm } = props;
|
||||
|
||||
const [apiKeyLabel, setApiKeyLabel] = useState();
|
||||
const [expirationDateOffset, setExpirationDateOffset] = useState(30);
|
||||
const [selectedExpirationDate, setSelectedExpirationDate] = useState();
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
api
|
||||
.post(`${host()}${endpoints.apiKeys}`, {
|
||||
label: apiKeyLabel,
|
||||
expirationDate: getExpirationDatetime().toISO()
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
onConfirm(response.data);
|
||||
setOpen(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLabelChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setApiKeyLabel(value);
|
||||
};
|
||||
|
||||
const handleExpirationDateChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setExpirationDateOffset(value);
|
||||
};
|
||||
|
||||
const handleDatePickerChange = (newValue) => {
|
||||
setSelectedExpirationDate(newValue);
|
||||
};
|
||||
|
||||
const getExpirationDatetime = () => {
|
||||
if (isNumber(expirationDateOffset)) {
|
||||
return DateTime.now().plus({ days: expirationDateOffset }).endOf('day');
|
||||
} else if (expirationDateOffset === 'custom') {
|
||||
return DateTime.fromISO(selectedExpirationDate);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getExpirationDisplay = () => {
|
||||
const expDateTime = getExpirationDatetime();
|
||||
return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>Create Api Key</DialogTitle>
|
||||
<DialogContent className={classes.apiKeyForm}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item container className={classes.apiKeyLabel} xs={12}>
|
||||
<TextField
|
||||
autoFocus
|
||||
required
|
||||
id="apikeylabel"
|
||||
label="Label"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleLabelChange}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container item xs={12}>
|
||||
<Grid item xs={5}>
|
||||
<FormControl className={classes.expirationDateContainer} size="small" required>
|
||||
<InputLabel disableAnimation>Expiration date</InputLabel>
|
||||
<Select
|
||||
labelId="expirationDate"
|
||||
id="expirationDate"
|
||||
label="Expiration time"
|
||||
onChange={handleExpirationDateChange}
|
||||
value={expirationDateOffset}
|
||||
className={classes.expirationDateInput}
|
||||
>
|
||||
<MenuItem value={7}>7 days</MenuItem>
|
||||
<MenuItem value={30}>30 days</MenuItem>
|
||||
<MenuItem value={60}>60 days</MenuItem>
|
||||
<MenuItem value={90}>90 days</MenuItem>
|
||||
<MenuItem value="custom">custom</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item className={classes.expirationDateDisplay} xs={7}>
|
||||
{expirationDateOffset === 'custom' ? (
|
||||
<DatePicker
|
||||
valueType="date"
|
||||
slotProps={{ textField: { size: 'small' } }}
|
||||
onChange={handleDatePickerChange}
|
||||
/>
|
||||
) : (
|
||||
<Typography>{getExpirationDisplay()}</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={handleSubmit}
|
||||
disabled={expirationDateOffset === 'custom' && isNil(selectedExpirationDate)}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyDialog;
|
71
src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
Normal file
71
src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from 'host';
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
gridWrapper: {
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem'
|
||||
},
|
||||
apiKeyDisplay: {
|
||||
boxSizing: 'border-box',
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: '#F7F7F7',
|
||||
borderRadius: '0.9rem',
|
||||
overflowWrap: 'break-word'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeyRevokeDialog(props) {
|
||||
const { open, setOpen, apiKey, onConfirm } = props;
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
api
|
||||
.delete(`${host()}${endpoints.apiKeys}`, { id: apiKey.uuid })
|
||||
.then((response) => {
|
||||
onConfirm(response?.status, apiKey);
|
||||
setOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>Revoke "{apiKey?.label}" key</DialogTitle>
|
||||
<DialogContent className={classes.apiKeyForm}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>Are you sure you want to revoke this api key?</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="error" onClick={handleSubmit}>
|
||||
Revoke
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyRevokeDialog;
|
146
src/components/User/ApiKeys/ApiKeys.jsx
Normal file
146
src/components/User/ApiKeys/ApiKeys.jsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { isEmpty, isNil } from 'lodash';
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from '../../../host';
|
||||
|
||||
import { Grid, Stack, Card, CardContent, Typography, Button } from '@mui/material';
|
||||
import Loading from '../../Shared/Loading';
|
||||
import ApiKeyDialog from './ApiKeyDialog';
|
||||
import ApiKeyConfirmDialog from './ApiKeyConfirmDialog';
|
||||
import ApiKeyCard from './ApiKeyCard';
|
||||
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
pageWrapper: {
|
||||
backgroundColor: 'transparent',
|
||||
height: '100%'
|
||||
},
|
||||
header: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
},
|
||||
cardRoot: {
|
||||
boxShadow: 'none!important'
|
||||
},
|
||||
pageTitle: {
|
||||
fontWeight: '600',
|
||||
fontSize: '1.5rem',
|
||||
color: theme.palette.secondary.main,
|
||||
textAlign: 'left'
|
||||
},
|
||||
apikeysContainer: {
|
||||
marginTop: '1.5rem',
|
||||
height: '100%',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '0'
|
||||
}
|
||||
},
|
||||
apikeysContent: {
|
||||
padding: '1.5rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function ApiKeys() {
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
const [newApiKey, setNewApiKey] = useState();
|
||||
const classes = useStyles();
|
||||
|
||||
// ApiKey dialog props
|
||||
const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false);
|
||||
const [apiKeyConfirmationOpen, setApiKeyConfirmationOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
api
|
||||
.get(`${host()}${endpoints.apiKeys}`)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.apiKeys) {
|
||||
setApiKeys(response.data.apiKeys);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setIsLoading(false);
|
||||
});
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNil(newApiKey) && !apiKeyConfirmationOpen) {
|
||||
setApiKeyConfirmationOpen(true);
|
||||
}
|
||||
}, [newApiKey]);
|
||||
|
||||
const handleApiKeyDialogOpen = () => {
|
||||
setApiKeyDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleApiKeyCreateConfirm = (apiKey) => {
|
||||
setNewApiKey(apiKey);
|
||||
setApiKeys((prevState) => [...prevState, apiKey]);
|
||||
};
|
||||
|
||||
const handleApiKeyRevokeConfirm = (status, apiKey) => {
|
||||
if (status === 200) setApiKeys((prevState) => prevState.filter((ak) => ak.uuid != apiKey.uuid));
|
||||
};
|
||||
|
||||
const renderApiKeys = () => {
|
||||
return apiKeys.map((apiKey) => (
|
||||
<ApiKeyCard key={apiKey.uuid} apiKey={apiKey} onRevoke={handleApiKeyRevokeConfirm} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Grid container className={classes.pageWrapper}>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent>
|
||||
<Grid container className={classes.header}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="h4" className={classes.pageTitle}>
|
||||
Manage your API Keys
|
||||
</Typography>
|
||||
<Button variant="contained" color="success" onClick={handleApiKeyDialogOpen}>
|
||||
Create new API key
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
{!isLoading && !isEmpty(apiKeys) && (
|
||||
<Grid item xs={12} className={classes.apikeysContainer}>
|
||||
<Card className={classes.cardRoot}>
|
||||
<CardContent className={classes.apikeysContent}>
|
||||
<Stack direction="column" spacing={1}>
|
||||
{renderApiKeys()}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
<ApiKeyDialog open={apiKeyDialogOpen} setOpen={setApiKeyDialogOpen} onConfirm={handleApiKeyCreateConfirm} />
|
||||
{!isNil(newApiKey) && (
|
||||
<ApiKeyConfirmDialog open={apiKeyConfirmationOpen} setOpen={setApiKeyConfirmationOpen} apiKey={newApiKey} />
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeys;
|
@ -5,6 +5,8 @@ import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
|
||||
|
||||
const theme = createTheme(
|
||||
adaptV4Theme({
|
||||
@ -36,7 +38,9 @@ ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<LocalizationProvider dateAdapter={AdapterLuxon}>
|
||||
<App />
|
||||
</LocalizationProvider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</React.StrictMode>,
|
||||
|
@ -14,7 +14,6 @@ const useStyles = makeStyles(() => ({
|
||||
minWidth: '60%'
|
||||
},
|
||||
gridWrapper: {
|
||||
// backgroundColor: "#fff",
|
||||
border: '0.0625em #f2f2f2 dashed'
|
||||
},
|
||||
pageWrapper: {
|
||||
|
57
src/pages/UserManagementPage.jsx
Normal file
57
src/pages/UserManagementPage.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { getLoggedInUser } from 'utilities/authUtilities.js';
|
||||
|
||||
import { Container, Grid, Stack } from '@mui/material';
|
||||
|
||||
import Header from '../components/Header/Header.jsx';
|
||||
import ApiKeys from '../components/User/ApiKeys/ApiKeys.jsx';
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
container: {
|
||||
paddingTop: 30,
|
||||
paddingBottom: 5,
|
||||
height: '100%',
|
||||
minWidth: '60%'
|
||||
},
|
||||
gridWrapper: {
|
||||
border: '0.0625rem #f2f2f2 dashed'
|
||||
},
|
||||
pageWrapper: {
|
||||
height: '100%'
|
||||
},
|
||||
tile: {
|
||||
width: '100%',
|
||||
padding: 5
|
||||
}
|
||||
}));
|
||||
|
||||
function UserManagementPage() {
|
||||
const classes = useStyles();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(getLoggedInUser())) {
|
||||
navigate('/home');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack className={classes.pageWrapper} direction="column" data-testid="explore-container">
|
||||
<Header />
|
||||
<Container className={classes.container}>
|
||||
<Grid container className={classes.gridWrapper}>
|
||||
<Grid item className={classes.tile}>
|
||||
<ApiKeys />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserManagementPage;
|
@ -41,10 +41,15 @@ const isAuthenticationEnabled = () => {
|
||||
return Object.keys(authMethods).length > 0;
|
||||
};
|
||||
|
||||
const isApiKeyEnabled = () => {
|
||||
const authConfig = JSON.parse(localStorage.getItem('authConfig')) || {};
|
||||
return authConfig?.apikey;
|
||||
};
|
||||
|
||||
const getLoggedInUser = () => {
|
||||
const userCookie = getCookie('user');
|
||||
if (!userCookie) return null;
|
||||
return userCookie;
|
||||
};
|
||||
|
||||
export { isAuthenticated, isAuthenticationEnabled, getLoggedInUser, logoutUser };
|
||||
export { isAuthenticated, isAuthenticationEnabled, isApiKeyEnabled, getLoggedInUser, logoutUser };
|
||||
|
Loading…
Reference in New Issue
Block a user