Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboard API Key Management #3423

Merged
merged 5 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions dashboard/src/actions/keyManagementActions.js
Original file line number Diff line number Diff line change
@@ -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));
webbnh marked this conversation as resolved.
Show resolved Hide resolved
}
} 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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,
});
Loading