diff --git a/package-lock.json b/package-lock.json
index 7728a198..f119192f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,13 +14,14 @@
"@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",
"axios": "^0.24.0",
"downshift": "^6.1.12",
"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",
@@ -2128,16 +2129,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",
@@ -2676,6 +2682,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",
@@ -3933,11 +3973,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": {
@@ -3946,13 +3986,12 @@
}
},
"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"
},
@@ -3961,10 +4000,120 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "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": {
+ "@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": {
@@ -4842,9 +4991,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",
@@ -4882,18 +5031,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": "*"
}
@@ -13196,9 +13337,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"
}
@@ -16148,7 +16289,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",
diff --git a/package.json b/package.json
index e8f008a4..d175978d 100644
--- a/package.json
+++ b/package.json
@@ -9,13 +9,14 @@
"@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",
"axios": "^0.24.0",
"downshift": "^6.1.12",
"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",
@@ -24,14 +25,14 @@
"web-vitals": "^2.1.3"
},
"devDependencies": {
+ "@babel/plugin-proposal-private-property-in-object": "^7.16.7",
"@playwright/test": "^1.28.1",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.8",
"prettier": "^2.7.1",
- "react-scripts": "^5.0.1",
- "@babel/plugin-proposal-private-property-in-object": "^7.16.7"
+ "react-scripts": "^5.0.1"
},
"scripts": {
"start": "react-scripts start",
diff --git a/src/App.js b/src/App.js
index 7e03a54f..4849a21a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,13 +2,14 @@ import React, { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { isAuthenticated } 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() {
} />
} />
} />
+ } />
} />
}>
diff --git a/src/api.js b/src/api.js
index 15b7f794..7f687820 100644
--- a/src/api.js
+++ b/src/api.js
@@ -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',
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize
diff --git a/src/components/Header/UserAccountMenu.jsx b/src/components/Header/UserAccountMenu.jsx
index a27fb947..d27e698d 100644
--- a/src/components/Header/UserAccountMenu.jsx
+++ b/src/components/Header/UserAccountMenu.jsx
@@ -3,10 +3,16 @@ import React, { useState } from 'react';
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
import { getLoggedInUser, logoutUser } 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() {
>
+
+
>
diff --git a/src/components/Repo/RepoDetails.jsx b/src/components/Repo/RepoDetails.jsx
index b3df2e9b..50d6895e 100644
--- a/src/components/Repo/RepoDetails.jsx
+++ b/src/components/Repo/RepoDetails.jsx
@@ -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: {
diff --git a/src/components/Shared/LayerCard.jsx b/src/components/Shared/LayerCard.jsx
index f88a12db..133cadc8 100644
--- a/src/components/Shared/LayerCard.jsx
+++ b/src/components/Shared/LayerCard.jsx
@@ -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: {
diff --git a/src/components/Tag/TagDetails.jsx b/src/components/Tag/TagDetails.jsx
index a9579bd7..8242edc5 100644
--- a/src/components/Tag/TagDetails.jsx
+++ b/src/components/Tag/TagDetails.jsx
@@ -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 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';
+import makeStyles from '@mui/styles/makeStyles';
+
+// 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';
const useStyles = makeStyles((theme) => ({
pageWrapper: {
diff --git a/src/components/Tag/TagDetailsMetadata.jsx b/src/components/Tag/TagDetailsMetadata.jsx
index 3a33d77c..3da99cd3 100644
--- a/src/components/Tag/TagDetailsMetadata.jsx
+++ b/src/components/Tag/TagDetailsMetadata.jsx
@@ -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',
diff --git a/src/components/User/ApiKeys/ApiKeyCard.jsx b/src/components/User/ApiKeys/ApiKeyCard.jsx
new file mode 100644
index 00000000..02b663bf
--- /dev/null
+++ b/src/components/User/ApiKeys/ApiKeyCard.jsx
@@ -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 (
+
+
+
+
+
+ {apiKey.label}
+
+
+
+
+ {getExpirationDisplay()}
+
+
+
+
+
+ {!isNil(apiKey.apiKey) && (
+ <>
+
+
+
+
+ setOpenDropdown((prevOpenState) => !prevOpenState)}>
+ {!openDropdown ? (
+
+ ) : (
+
+ )}
+ KEY
+
+
+
+
+ {apiKey.apiKey}
+
+
+
+
+ >
+ )}
+
+
+
+
+ );
+}
+
+export default ApiKeyCard;
diff --git a/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx b/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
new file mode 100644
index 00000000..ec51930e
--- /dev/null
+++ b/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx
@@ -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 (
+
+ );
+}
+
+export default ApiKeyConfirmDialog;
diff --git a/src/components/User/ApiKeys/ApiKeyDialog.jsx b/src/components/User/ApiKeys/ApiKeyDialog.jsx
new file mode 100644
index 00000000..f6fc1a06
--- /dev/null
+++ b/src/components/User/ApiKeys/ApiKeyDialog.jsx
@@ -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 (
+
+ );
+}
+
+export default ApiKeyDialog;
diff --git a/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx b/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
new file mode 100644
index 00000000..d5dcb5d7
--- /dev/null
+++ b/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx
@@ -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 (
+
+ );
+}
+
+export default ApiKeyRevokeDialog;
diff --git a/src/components/User/ApiKeys/ApiKeys.jsx b/src/components/User/ApiKeys/ApiKeys.jsx
new file mode 100644
index 00000000..9117111b
--- /dev/null
+++ b/src/components/User/ApiKeys/ApiKeys.jsx
@@ -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) => (
+
+ ));
+ };
+
+ return (
+ <>
+ {isLoading ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+ Manage your API Keys
+
+
+
+
+
+
+
+
+ {!isLoading && !isEmpty(apiKeys) && (
+
+
+
+
+ {renderApiKeys()}
+
+
+
+
+ )}
+
+ {!isNil(newApiKey) && (
+
+ )}
+
+ )}
+ >
+ );
+}
+
+export default ApiKeys;
diff --git a/src/host.js b/src/host.js
index 582b6ac4..97fedb6a 100644
--- a/src/host.js
+++ b/src/host.js
@@ -1,6 +1,6 @@
const hostConfig = {
- auto: true,
- default: 'http://localhost:5000'
+ auto: false,
+ default: 'http://127.0.0.1:5000'
};
const host = (manualHost = null) => {
diff --git a/src/index.js b/src/index.js
index dff19ee6..c21d1b97 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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(
-
+
+
+
,
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index c09daaa6..29ed5f1e 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -14,7 +14,6 @@ const useStyles = makeStyles(() => ({
minWidth: '60%'
},
gridWrapper: {
- // backgroundColor: "#fff",
border: '0.0625em #f2f2f2 dashed'
},
pageWrapper: {
diff --git a/src/pages/UserManagementPage.jsx b/src/pages/UserManagementPage.jsx
new file mode 100644
index 00000000..85119e0a
--- /dev/null
+++ b/src/pages/UserManagementPage.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default UserManagementPage;