Skip to content

Commit e26d925

Browse files
authored
Dashboard API Key Management (#3423)
Develop a UI page where a logged-in user can generate a new key for their account and manage existing keys. PBENCH-1131
1 parent 5d3ac3c commit e26d925

File tree

10 files changed

+362
-2
lines changed

10 files changed

+362
-2
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import * as TYPES from "actions/types";
2+
3+
import { DANGER, ERROR_MSG, SUCCESS } from "assets/constants/toastConstants";
4+
5+
import API from "../utils/axiosInstance";
6+
import { showToast } from "./toastActions";
7+
import { uriTemplate } from "utils/helper";
8+
9+
export const getAPIkeysList = async (dispatch, getState) => {
10+
try {
11+
dispatch({ type: TYPES.LOADING });
12+
13+
const endpoints = getState().apiEndpoint.endpoints;
14+
const response = await API.get(uriTemplate(endpoints, "key", { key: "" }));
15+
16+
if (response.status === 200) {
17+
dispatch({
18+
type: TYPES.SET_API_KEY_LIST,
19+
payload: response.data,
20+
});
21+
} else {
22+
dispatch(showToast(DANGER, ERROR_MSG));
23+
}
24+
} catch (error) {
25+
dispatch(showToast(DANGER, error));
26+
}
27+
dispatch({ type: TYPES.COMPLETED });
28+
};
29+
30+
export const deleteAPIKey = (id) => async (dispatch, getState) => {
31+
try {
32+
dispatch({ type: TYPES.LOADING });
33+
const endpoints = getState().apiEndpoint.endpoints;
34+
const response = await API.delete(
35+
uriTemplate(endpoints, "key", { key: id })
36+
);
37+
38+
if (response.status === 200) {
39+
dispatch({
40+
type: TYPES.SET_API_KEY_LIST,
41+
payload: getState().keyManagement.keyList.filter(
42+
(item) => item.id !== id
43+
),
44+
});
45+
46+
const message = response.data ?? "Deleted";
47+
const toastMsg = message?.charAt(0).toUpperCase() + message?.slice(1);
48+
49+
dispatch(showToast(SUCCESS, toastMsg));
50+
} else {
51+
dispatch(showToast(DANGER, ERROR_MSG));
52+
}
53+
} catch (error) {
54+
dispatch(showToast(DANGER, error));
55+
}
56+
dispatch({ type: TYPES.COMPLETED });
57+
};
58+
59+
export const sendNewKeyRequest = (label) => async (dispatch, getState) => {
60+
try {
61+
dispatch({ type: TYPES.LOADING });
62+
const endpoints = getState().apiEndpoint.endpoints;
63+
const keyList = [...getState().keyManagement.keyList];
64+
65+
const response = await API.post(
66+
uriTemplate(endpoints, "key", { key: "" }),
67+
null,
68+
{ params: { label } }
69+
);
70+
if (response.status === 201) {
71+
keyList.push(response.data);
72+
dispatch({
73+
type: TYPES.SET_API_KEY_LIST,
74+
payload: keyList,
75+
});
76+
dispatch(showToast(SUCCESS, "API key created successfully"));
77+
78+
dispatch(toggleNewAPIKeyModal(false));
79+
dispatch(setNewKeyLabel(""));
80+
} else {
81+
dispatch(showToast(DANGER, response.data.message));
82+
}
83+
} catch {
84+
dispatch(showToast(DANGER, ERROR_MSG));
85+
}
86+
dispatch({ type: TYPES.COMPLETED });
87+
};
88+
89+
export const toggleNewAPIKeyModal = (isOpen) => ({
90+
type: TYPES.TOGGLE_NEW_KEY_MODAL,
91+
payload: isOpen,
92+
});
93+
94+
export const setNewKeyLabel = (label) => ({
95+
type: TYPES.SET_NEW_KEY_LABEL,
96+
payload: label,
97+
});

dashboard/src/actions/types.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,8 @@ export const UPDATE_TOC_LOADING = "UPDATE_TOC_LOADING";
5252

5353
/* SIDEBAR */
5454
export const SET_ACTIVE_MENU_ITEM = "SET_ACTIVE_MENU_ITEM";
55+
56+
/* KEY MANAGEMENT */
57+
export const SET_API_KEY_LIST = "SET_API_KEY_LIST";
58+
export const TOGGLE_NEW_KEY_MODAL = "TOGGLE_NEW_KEY_MODAL";
59+
export const SET_NEW_KEY_LABEL = "SET_NEW_KEY_LABEL";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const DANGER = "danger";
22
export const ERROR_MSG = "Something went wrong!";
3+
export const SUCCESS = "success";
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Button, ClipboardCopy } from "@patternfly/react-core";
2+
import {
3+
TableComposable,
4+
Tbody,
5+
Td,
6+
Th,
7+
Thead,
8+
Tr,
9+
} from "@patternfly/react-table";
10+
import { useDispatch, useSelector } from "react-redux";
11+
12+
import React from "react";
13+
import { TrashIcon } from "@patternfly/react-icons";
14+
import { deleteAPIKey } from "actions/keyManagementActions";
15+
import { formatDateTime } from "utils/dateFunctions";
16+
17+
const KeyListTable = () => {
18+
const dispatch = useDispatch();
19+
const keyList = useSelector((state) => state.keyManagement.keyList);
20+
const columnNames = {
21+
label: "Label",
22+
created: "Created Date & Time",
23+
key: "API key",
24+
};
25+
26+
return (
27+
<TableComposable aria-label="key list table" isStriped>
28+
<Thead>
29+
<Tr>
30+
<Th width={10}>{columnNames.label}</Th>
31+
<Th width={20}>{columnNames.created}</Th>
32+
<Th width={20}>{columnNames.key}</Th>
33+
<Th width={5}></Th>
34+
</Tr>
35+
</Thead>
36+
<Tbody className="keylist-table-body">
37+
{keyList.map((item) => (
38+
<Tr key={item.key}>
39+
<Td dataLabel={columnNames.label}>{item.label}</Td>
40+
<Td dataLabel={columnNames.created}>
41+
{formatDateTime(item.created)}
42+
</Td>
43+
<Td dataLabel={columnNames.key} className="key-cell">
44+
<ClipboardCopy
45+
hoverTip="Copy API key"
46+
clickTip="Copied"
47+
variant="plain"
48+
>
49+
{item.key}
50+
</ClipboardCopy>
51+
</Td>
52+
53+
<Td className="delete-icon-cell">
54+
<Button
55+
variant="plain"
56+
aria-label="Delete Action"
57+
onClick={() => dispatch(deleteAPIKey(item.id))}
58+
>
59+
<TrashIcon />
60+
</Button>
61+
</Td>
62+
</Tr>
63+
))}
64+
</Tbody>
65+
</TableComposable>
66+
);
67+
};
68+
69+
export default KeyListTable;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Button, Card, CardBody } from "@patternfly/react-core";
2+
import React, { useEffect } from "react";
3+
import {
4+
getAPIkeysList,
5+
setNewKeyLabel,
6+
toggleNewAPIKeyModal,
7+
} from "actions/keyManagementActions";
8+
import { useDispatch, useSelector } from "react-redux";
9+
10+
import KeyListTable from "./KeyListTable";
11+
import NewKeyModal from "./NewKeyModal";
12+
13+
const KeyManagementComponent = () => {
14+
const dispatch = useDispatch();
15+
const isModalOpen = useSelector((state) => state.keyManagement.isModalOpen);
16+
const { idToken } = useSelector((state) => state.apiEndpoint?.keycloak);
17+
useEffect(() => {
18+
if (idToken) {
19+
dispatch(getAPIkeysList);
20+
}
21+
}, [dispatch, idToken]);
22+
const handleModalToggle = () => {
23+
dispatch(setNewKeyLabel(""));
24+
dispatch(toggleNewAPIKeyModal(!isModalOpen));
25+
};
26+
return (
27+
<Card className="key-management-container">
28+
<CardBody>
29+
<div className="heading-wrapper">
30+
<p className="heading-title">API Keys</p>
31+
<Button variant="tertiary" onClick={handleModalToggle}>
32+
New API key
33+
</Button>
34+
</div>
35+
<p className="key-desc">
36+
This is a list of API keys associated with your account. Remove any
37+
keys that you do not recognize.
38+
</p>
39+
<KeyListTable />
40+
</CardBody>
41+
<NewKeyModal handleModalToggle={handleModalToggle} />
42+
</Card>
43+
);
44+
};
45+
46+
export default KeyManagementComponent;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import "./index.less";
2+
3+
import {
4+
Button,
5+
Form,
6+
FormGroup,
7+
Modal,
8+
ModalVariant,
9+
TextInput,
10+
} from "@patternfly/react-core";
11+
import {
12+
sendNewKeyRequest,
13+
setNewKeyLabel,
14+
} from "actions/keyManagementActions";
15+
import { useDispatch, useSelector } from "react-redux";
16+
17+
import React from "react";
18+
19+
const NewKeyModal = (props) => {
20+
const dispatch = useDispatch();
21+
const { isModalOpen, newKeyLabel } = useSelector(
22+
(state) => state.keyManagement
23+
);
24+
25+
return (
26+
<Modal
27+
variant={ModalVariant.small}
28+
title="New API Key"
29+
isOpen={isModalOpen}
30+
showClose={false}
31+
actions={[
32+
<Button
33+
key="create"
34+
variant="primary"
35+
form="modal-with-form-form"
36+
onClick={() => dispatch(sendNewKeyRequest(newKeyLabel))}
37+
>
38+
Create
39+
</Button>,
40+
<Button key="cancel" variant="link" onClick={props.handleModalToggle}>
41+
Cancel
42+
</Button>,
43+
]}
44+
>
45+
<Form id="new-api-key-form">
46+
<FormGroup label="Enter the label" fieldId="new-api-key-form">
47+
<TextInput
48+
type="text"
49+
id="new-api-key-form"
50+
name="new-api-key-form"
51+
value={newKeyLabel}
52+
onChange={(value) => dispatch(setNewKeyLabel(value))}
53+
/>
54+
</FormGroup>
55+
</Form>
56+
</Modal>
57+
);
58+
};
59+
60+
export default NewKeyModal;

dashboard/src/modules/components/ProfileComponent/index.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React from "react";
1+
import "./index.less";
2+
23
import {
34
Card,
45
CardBody,
@@ -12,7 +13,9 @@ import {
1213
isValidDate,
1314
} from "@patternfly/react-core";
1415
import { KeyIcon, UserAltIcon } from "@patternfly/react-icons";
15-
import "./index.less";
16+
17+
import KeyManagementComponent from "./KeyManagement";
18+
import React from "react";
1619
import avatar from "assets/images/avatar.jpg";
1720
import { useKeycloak } from "@react-keycloak/web";
1821

@@ -104,6 +107,9 @@ const ProfileComponent = () => {
104107
</div>
105108
</CardBody>
106109
</Card>
110+
<GridItem span={8}>
111+
<KeyManagementComponent />
112+
</GridItem>
107113
</GridItem>
108114
<GridItem span={4}>
109115
<Card className="card">

dashboard/src/modules/components/ProfileComponent/index.less

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,45 @@
6161
font-weight: 700;
6262
}
6363
}
64+
65+
.key-management-container {
66+
margin-top: 2vh;
67+
.key-desc {
68+
margin-bottom: 2vh;
69+
}
70+
.heading-wrapper {
71+
display: flex;
72+
justify-content: space-between;
73+
.heading-title {
74+
font-weight: 700;
75+
}
76+
}
77+
.keylist-table-body {
78+
.key-cell {
79+
width: 30vw;
80+
overflow: hidden;
81+
white-space: nowrap;
82+
display: block;
83+
text-overflow: ellipsis;
84+
}
85+
.pf-c-clipboard-copy__group {
86+
input {
87+
background-color: transparent;
88+
border: 1px solid transparent;
89+
}
90+
button {
91+
background-color: transparent;
92+
}
93+
input:focus,
94+
input:focus-visible {
95+
outline: none;
96+
}
97+
button::after {
98+
border: 1px solid transparent;
99+
}
100+
svg {
101+
fill: #6a6373;
102+
}
103+
}
104+
}
105+
}

dashboard/src/reducers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import DatasetListReducer from "./datasetListReducer";
22
import EndpointReducer from "./endpointReducer";
3+
import KeyManagementReducer from "./keyManagementReducer";
34
import LoadingReducer from "./loadingReducer";
45
import NavbarReducer from "./navbarReducer";
56
import OverviewReducer from "./overviewReducer";
@@ -17,4 +18,5 @@ export default combineReducers({
1718
overview: OverviewReducer,
1819
tableOfContent: TableOfContentReducer,
1920
sidebar: SidebarReducer,
21+
keyManagement: KeyManagementReducer,
2022
});

0 commit comments

Comments
 (0)