Skip to content

Commit

Permalink
Dashboard API Key Management (#3423)
Browse files Browse the repository at this point in the history
Develop a UI page where a logged-in user can generate a new key for their account and manage existing keys.

PBENCH-1131
  • Loading branch information
MVarshini committed May 18, 2023
1 parent 5d3ac3c commit e26d925
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 2 deletions.
97 changes: 97 additions & 0 deletions dashboard/src/actions/keyManagementActions.js
@@ -0,0 +1,97 @@
import * as TYPES from "actions/types";

import { DANGER, ERROR_MSG, SUCCESS } from "assets/constants/toastConstants";

import API from "../utils/axiosInstance";
import { showToast } from "./toastActions";
import { uriTemplate } from "utils/helper";

export const getAPIkeysList = async (dispatch, getState) => {
try {
dispatch({ type: TYPES.LOADING });

const endpoints = getState().apiEndpoint.endpoints;
const response = await API.get(uriTemplate(endpoints, "key", { key: "" }));

if (response.status === 200) {
dispatch({
type: TYPES.SET_API_KEY_LIST,
payload: response.data,
});
} else {
dispatch(showToast(DANGER, ERROR_MSG));
}
} catch (error) {
dispatch(showToast(DANGER, error));
}
dispatch({ type: TYPES.COMPLETED });
};

export const deleteAPIKey = (id) => async (dispatch, getState) => {
try {
dispatch({ type: TYPES.LOADING });
const endpoints = getState().apiEndpoint.endpoints;
const response = await API.delete(
uriTemplate(endpoints, "key", { key: id })
);

if (response.status === 200) {
dispatch({
type: TYPES.SET_API_KEY_LIST,
payload: getState().keyManagement.keyList.filter(
(item) => item.id !== id
),
});

const message = response.data ?? "Deleted";
const toastMsg = message?.charAt(0).toUpperCase() + message?.slice(1);

dispatch(showToast(SUCCESS, toastMsg));
} else {
dispatch(showToast(DANGER, ERROR_MSG));
}
} catch (error) {
dispatch(showToast(DANGER, error));
}
dispatch({ type: TYPES.COMPLETED });
};

export const sendNewKeyRequest = (label) => async (dispatch, getState) => {
try {
dispatch({ type: TYPES.LOADING });
const endpoints = getState().apiEndpoint.endpoints;
const keyList = [...getState().keyManagement.keyList];

const response = await API.post(
uriTemplate(endpoints, "key", { key: "" }),
null,
{ params: { label } }
);
if (response.status === 201) {
keyList.push(response.data);
dispatch({
type: TYPES.SET_API_KEY_LIST,
payload: keyList,
});
dispatch(showToast(SUCCESS, "API key created successfully"));

dispatch(toggleNewAPIKeyModal(false));
dispatch(setNewKeyLabel(""));
} else {
dispatch(showToast(DANGER, response.data.message));
}
} catch {
dispatch(showToast(DANGER, ERROR_MSG));
}
dispatch({ type: TYPES.COMPLETED });
};

export const toggleNewAPIKeyModal = (isOpen) => ({
type: TYPES.TOGGLE_NEW_KEY_MODAL,
payload: isOpen,
});

export const setNewKeyLabel = (label) => ({
type: TYPES.SET_NEW_KEY_LABEL,
payload: label,
});
5 changes: 5 additions & 0 deletions dashboard/src/actions/types.js
Expand Up @@ -52,3 +52,8 @@ export const UPDATE_TOC_LOADING = "UPDATE_TOC_LOADING";

/* SIDEBAR */
export const SET_ACTIVE_MENU_ITEM = "SET_ACTIVE_MENU_ITEM";

/* KEY MANAGEMENT */
export const SET_API_KEY_LIST = "SET_API_KEY_LIST";
export const TOGGLE_NEW_KEY_MODAL = "TOGGLE_NEW_KEY_MODAL";
export const SET_NEW_KEY_LABEL = "SET_NEW_KEY_LABEL";
1 change: 1 addition & 0 deletions dashboard/src/assets/constants/toastConstants.js
@@ -1,2 +1,3 @@
export const DANGER = "danger";
export const ERROR_MSG = "Something went wrong!";
export const SUCCESS = "success";
69 changes: 69 additions & 0 deletions dashboard/src/modules/components/ProfileComponent/KeyListTable.jsx
@@ -0,0 +1,69 @@
import { Button, ClipboardCopy } from "@patternfly/react-core";
import {
TableComposable,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";
import { useDispatch, useSelector } from "react-redux";

import React from "react";
import { TrashIcon } from "@patternfly/react-icons";
import { deleteAPIKey } from "actions/keyManagementActions";
import { formatDateTime } from "utils/dateFunctions";

const KeyListTable = () => {
const dispatch = useDispatch();
const keyList = useSelector((state) => state.keyManagement.keyList);
const columnNames = {
label: "Label",
created: "Created Date & Time",
key: "API key",
};

return (
<TableComposable aria-label="key list table" isStriped>
<Thead>
<Tr>
<Th width={10}>{columnNames.label}</Th>
<Th width={20}>{columnNames.created}</Th>
<Th width={20}>{columnNames.key}</Th>
<Th width={5}></Th>
</Tr>
</Thead>
<Tbody className="keylist-table-body">
{keyList.map((item) => (
<Tr key={item.key}>
<Td dataLabel={columnNames.label}>{item.label}</Td>
<Td dataLabel={columnNames.created}>
{formatDateTime(item.created)}
</Td>
<Td dataLabel={columnNames.key} className="key-cell">
<ClipboardCopy
hoverTip="Copy API key"
clickTip="Copied"
variant="plain"
>
{item.key}
</ClipboardCopy>
</Td>

<Td className="delete-icon-cell">
<Button
variant="plain"
aria-label="Delete Action"
onClick={() => dispatch(deleteAPIKey(item.id))}
>
<TrashIcon />
</Button>
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
);
};

export default KeyListTable;
@@ -0,0 +1,46 @@
import { Button, Card, CardBody } from "@patternfly/react-core";
import React, { useEffect } from "react";
import {
getAPIkeysList,
setNewKeyLabel,
toggleNewAPIKeyModal,
} from "actions/keyManagementActions";
import { useDispatch, useSelector } from "react-redux";

import KeyListTable from "./KeyListTable";
import NewKeyModal from "./NewKeyModal";

const KeyManagementComponent = () => {
const dispatch = useDispatch();
const isModalOpen = useSelector((state) => state.keyManagement.isModalOpen);
const { idToken } = useSelector((state) => state.apiEndpoint?.keycloak);
useEffect(() => {
if (idToken) {
dispatch(getAPIkeysList);
}
}, [dispatch, idToken]);
const handleModalToggle = () => {
dispatch(setNewKeyLabel(""));
dispatch(toggleNewAPIKeyModal(!isModalOpen));
};
return (
<Card className="key-management-container">
<CardBody>
<div className="heading-wrapper">
<p className="heading-title">API Keys</p>
<Button variant="tertiary" onClick={handleModalToggle}>
New API key
</Button>
</div>
<p className="key-desc">
This is a list of API keys associated with your account. Remove any
keys that you do not recognize.
</p>
<KeyListTable />
</CardBody>
<NewKeyModal handleModalToggle={handleModalToggle} />
</Card>
);
};

export default KeyManagementComponent;
60 changes: 60 additions & 0 deletions dashboard/src/modules/components/ProfileComponent/NewKeyModal.jsx
@@ -0,0 +1,60 @@
import "./index.less";

import {
Button,
Form,
FormGroup,
Modal,
ModalVariant,
TextInput,
} from "@patternfly/react-core";
import {
sendNewKeyRequest,
setNewKeyLabel,
} from "actions/keyManagementActions";
import { useDispatch, useSelector } from "react-redux";

import React from "react";

const NewKeyModal = (props) => {
const dispatch = useDispatch();
const { isModalOpen, newKeyLabel } = useSelector(
(state) => state.keyManagement
);

return (
<Modal
variant={ModalVariant.small}
title="New API Key"
isOpen={isModalOpen}
showClose={false}
actions={[
<Button
key="create"
variant="primary"
form="modal-with-form-form"
onClick={() => dispatch(sendNewKeyRequest(newKeyLabel))}
>
Create
</Button>,
<Button key="cancel" variant="link" onClick={props.handleModalToggle}>
Cancel
</Button>,
]}
>
<Form id="new-api-key-form">
<FormGroup label="Enter the label" fieldId="new-api-key-form">
<TextInput
type="text"
id="new-api-key-form"
name="new-api-key-form"
value={newKeyLabel}
onChange={(value) => dispatch(setNewKeyLabel(value))}
/>
</FormGroup>
</Form>
</Modal>
);
};

export default NewKeyModal;
10 changes: 8 additions & 2 deletions dashboard/src/modules/components/ProfileComponent/index.jsx
@@ -1,4 +1,5 @@
import React from "react";
import "./index.less";

import {
Card,
CardBody,
Expand All @@ -12,7 +13,9 @@ import {
isValidDate,
} from "@patternfly/react-core";
import { KeyIcon, UserAltIcon } from "@patternfly/react-icons";
import "./index.less";

import KeyManagementComponent from "./KeyManagement";
import React from "react";
import avatar from "assets/images/avatar.jpg";
import { useKeycloak } from "@react-keycloak/web";

Expand Down Expand Up @@ -104,6 +107,9 @@ const ProfileComponent = () => {
</div>
</CardBody>
</Card>
<GridItem span={8}>
<KeyManagementComponent />
</GridItem>
</GridItem>
<GridItem span={4}>
<Card className="card">
Expand Down
42 changes: 42 additions & 0 deletions dashboard/src/modules/components/ProfileComponent/index.less
Expand Up @@ -61,3 +61,45 @@
font-weight: 700;
}
}

.key-management-container {
margin-top: 2vh;
.key-desc {
margin-bottom: 2vh;
}
.heading-wrapper {
display: flex;
justify-content: space-between;
.heading-title {
font-weight: 700;
}
}
.keylist-table-body {
.key-cell {
width: 30vw;
overflow: hidden;
white-space: nowrap;
display: block;
text-overflow: ellipsis;
}
.pf-c-clipboard-copy__group {
input {
background-color: transparent;
border: 1px solid transparent;
}
button {
background-color: transparent;
}
input:focus,
input:focus-visible {
outline: none;
}
button::after {
border: 1px solid transparent;
}
svg {
fill: #6a6373;
}
}
}
}
2 changes: 2 additions & 0 deletions dashboard/src/reducers/index.js
@@ -1,5 +1,6 @@
import DatasetListReducer from "./datasetListReducer";
import EndpointReducer from "./endpointReducer";
import KeyManagementReducer from "./keyManagementReducer";
import LoadingReducer from "./loadingReducer";
import NavbarReducer from "./navbarReducer";
import OverviewReducer from "./overviewReducer";
Expand All @@ -17,4 +18,5 @@ export default combineReducers({
overview: OverviewReducer,
tableOfContent: TableOfContentReducer,
sidebar: SidebarReducer,
keyManagement: KeyManagementReducer,
});

0 comments on commit e26d925

Please sign in to comment.