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

feat: generate API Credentials in Admin Portal #1016

Closed
wants to merge 9 commits into from
Closed
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
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ FEATURE_SETTINGS_PAGE_LMS_TAB='true'
FEATURE_SETTINGS_PAGE_APPEARANCE_TAB='true'
FEATURE_LEARNER_CREDIT_MANAGEMENT='true'
FEATURE_CONTENT_HIGHLIGHTS='true'
FEATURE_API_CREDENTIALS_TAB='true'
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContactCustomerSupportButton/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ContactCustomerSupportButton.propTypes = {

ContactCustomerSupportButton.defaultProps = {
children: 'Contact support',
variant: 'btn-outline-primary',
variant: 'outline-primary',
};

export default ContactCustomerSupportButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useContext, useState } from 'react';
import { MailtoLink, Form } from '@edx/paragon';
import RegenerateCredentialWarningModal from './RegenerateCredentialWarningModal';
import CopiedButton from './CopiedButton';
import { ENTERPRISE_CUSTOMER_SUPPORT_EMAIL } from '../data/constants';
import { DataContext } from './Context';

const APICredentialsPage = () => {
const [formValue, setFormValue] = useState('');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [data, setData] = useContext(DataContext);
const handleFormChange = (e) => {
setFormValue(e.target.value);
};
return (
<div>
<div className="mb-4">
<h3>Your API credentials</h3>
<p>
Copy and paste the following credential information and send it to your API developer(s).
</p>
</div>
<div className="mb-4">
<h4>
Application name:&nbsp;
<span style={{ fontWeight: 'normal' }}>{data?.name}</span>
</h4>
<h4>
Allowed URIs:&nbsp;
<span style={{ fontWeight: 'normal' }}>{data?.redirect_uris}</span>
</h4>
<h4>
API client ID:&nbsp;
<span style={{ fontWeight: 'normal' }}>{data?.client_id}</span>
</h4>
<h4>
API client secret:&nbsp;
<span style={{ fontWeight: 'normal' }}>{data?.client_secret}</span>
</h4>
<h4>API client documentation:&nbsp;
<span style={{ fontWeight: 'normal' }}>{data?.api_client_documentation}</span>
</h4>
<h4>
Last generated on:&nbsp;
<span style={{ fontWeight: 'normal' }}>{data?.updated}</span>
</h4>
<div className="my-3">
<CopiedButton />
</div>
</div>
<div className="mb-4">
<h3>Redirect URIs (optional)</h3>
<p>
If you need additional redirect URIs, add them below and regenerate your API credentials.
You will need to communicate the new credentials to your API developers.
</p>
<Form.Control
value={formValue}
onChange={handleFormChange}
floatingLabel="Redirect URIs"
data-testid="form-control"
/>
<p>
Allowed URI&apos;s list, space separated
</p>
<RegenerateCredentialWarningModal
redirectURLs={formValue}
setRedirectURIs={setFormValue}
/>
</div>
<div className="mb-4">
<h3>Questions or modifications?</h3>
<p>
To troubleshoot your API credentialing, or to request additional API endpoints to your
credentials,&nbsp;
<MailtoLink to={ENTERPRISE_CUSTOMER_SUPPORT_EMAIL} target="_blank">
contact Enterprise Customer Support.
</MailtoLink>
</p>
</div>
</div>
);
};

export default APICredentialsPage;
6 changes: 6 additions & 0 deletions src/components/settings/SettingsApiCredentialsTab/Context.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createContext } from 'react';

export const ZeroStateHandlerContext = createContext(null);
export const ErrorContext = createContext(null);
export const ShowSuccessToast = createContext(null);
export const DataContext = createContext(null);
43 changes: 43 additions & 0 deletions src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useContext, useState } from 'react';
import { Button } from '@edx/paragon';
import { ContentCopy } from '@edx/paragon/icons';
import CopiedToast from './CopiedToast';
import { DataContext } from './Context';

const CopiedButton = () => {
const [isCopyLinkToastOpen, setIsCopyLinkToastOpen] = useState(false);
const [data] = useContext(DataContext);
const [copiedError, setCopiedError] = useState(false);

const handleCopyLink = async () => {
try {
const jsonString = JSON.stringify(data);
await navigator.clipboard.writeText(jsonString);
} catch (error) {
setCopiedError(true);
} finally {
setIsCopyLinkToastOpen(true);
}
};
const handleCloseLinkCopyToast = () => {
setIsCopyLinkToastOpen(false);

Check warning on line 23 in src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx#L23

Added line #L23 was not covered by tests
};
return (
<>
<Button
variant="primary"
iconAfter={ContentCopy}
onClick={handleCopyLink}
>
Copy credentials to clipboard
</Button>
<CopiedToast
content={copiedError ? 'Cannot copied to the clipboard' : 'Copied Successfully'}
show={isCopyLinkToastOpen}
onClose={handleCloseLinkCopyToast}
/>
</>
);
};

export default CopiedButton;
13 changes: 13 additions & 0 deletions src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Toast } from '@edx/paragon';

const CopiedToast = ({ content, ...rest }) => (
<Toast {...rest}>
{content}
</Toast>
);
CopiedToast.propTypes = {
content: PropTypes.string.isRequired,
};
export default CopiedToast;
17 changes: 17 additions & 0 deletions src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Alert } from '@edx/paragon';
import { Error } from '@edx/paragon/icons';

const FailedAlert = () => (
<Alert variant="danger" icon={Error}>
<Alert.Heading>
Credential generation failed
</Alert.Heading>
<p>
Something went wrong while generating your credentials.
Please try again.
If the issue continues, contact Enterprise Customer Support.
</p>
</Alert>
);

export default FailedAlert;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { Hyperlink } from '@edx/paragon';
import PropTypes from 'prop-types';
import classNames from 'classnames';

const HelpCenterButton = ({
variant,
url,
children,
...rest
}) => {
const destinationUrl = url;

return (
<Hyperlink
{...rest}
target="_blank"
className={classNames('btn', `btn-${variant}`)}
destination={destinationUrl}
>
{children}
</Hyperlink>
);
};

HelpCenterButton.defaultProps = {
children: 'Help Center',
variant: 'outline-primary',
};

HelpCenterButton.propTypes = {
children: PropTypes.node,
variant: PropTypes.string,
url: PropTypes.string,
};

export default HelpCenterButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useContext } from 'react';
import {
ActionRow, Button, ModalDialog, useToggle, Icon,
} from '@edx/paragon';
import { Warning } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import { DataContext, ErrorContext, ShowSuccessToast } from './Context';
import LmsApiService from '../../../data/services/LmsApiService';

const RegenerateCredentialWarningModal = ({
modalSize,
modalVariant,
redirectURLs,
setRedirectURIs,
}) => {
const [isOn, setOn, setOff] = useToggle(false);
const [, setHasError] = useContext(ErrorContext);
const [, setData] = useContext(DataContext);
const [, setShowSuccessToast] = useContext(ShowSuccessToast);
const handleOnClickRegeneration = async () => {
try {
const response = await LmsApiService.regenerateAPICredentials(redirectURLs);
setData(response.data);
setShowSuccessToast(true);
setRedirectURIs('');
} catch (error) {
setHasError(true);
} finally {
setOff(true);
}
};
return (
<>
<Button
variant="primary"
onClick={setOn}
className="mb-2 mb-sm-0"
>
Regenerate API Credentials
</Button>
<ModalDialog
title="Warning Message"
size={modalSize}
variant={modalVariant}
isOpen={isOn}
onClose={setOff}
hasCloseButton
isFullscreenOnMobile
isFullscreenScroll
>
<ModalDialog.Header>
<ModalDialog.Title>
<div className="d-flex">
<Icon src={Warning} className="warning-icon mr-2 align-items-baseline-center" />
Regenerate API credentials?
</div>
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<p>
Any system, job, or script using the previous credentials will no
longer be able to authenticate with the edX API.
</p>
<p>
If you do regenerate, you will need to send the new credentials to your developers.
</p>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
Cancel
</ModalDialog.CloseButton>
<Button
variant="primary"
onClick={handleOnClickRegeneration}
>
Regenerate
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
</>
);
};

RegenerateCredentialWarningModal.defaultProps = {
modalSize: 'md',
modalVariant: 'default',
};

RegenerateCredentialWarningModal.propTypes = {
modalSize: PropTypes.string,
modalVariant: PropTypes.string,
redirectURLs: PropTypes.string.isRequired,
setRedirectURIs: PropTypes.func.isRequired,
};

export default RegenerateCredentialWarningModal;
Loading