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 #1018

Closed
wants to merge 19 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;
11 changes: 4 additions & 7 deletions src/components/forms/FormWorkflow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import type { Dispatch } from 'react';
import {
ActionRow, Button, FullscreenModal, Hyperlink, Stepper, useToggle,
ActionRow, Button, FullscreenModal, Stepper, useToggle,
} from '@edx/paragon';
import { Launch } from '@edx/paragon/icons';

Expand All @@ -14,6 +14,7 @@ import { HELP_CENTER_LINK, SUBMIT_TOAST_MESSAGE } from '../settings/data/constan
import UnsavedChangesModal from '../settings/SettingsLMSTab/UnsavedChangesModal';
import ConfigErrorModal from '../settings/ConfigErrorModal';
import { channelMapping, pollAsync } from '../../utils';
import HelpCenterButton from '../settings/HelpCenterButton';

export const WAITING_FOR_ASYNC_OPERATION = 'WAITING FOR ASYNC OPERATION';

Expand Down Expand Up @@ -201,13 +202,9 @@ const FormWorkflow = <FormConfigData extends unknown>({
className="stepper-modal"
footerNode={(
<ActionRow>
<Hyperlink
destination={helpCenterLink}
className="btn btn-outline-tertiary"
target="_blank"
>
<HelpCenterButton url={helpCenterLink}>
Help Center: Integrations
</Hyperlink>
</HelpCenterButton>
<ActionRow.Spacer />
<Button variant="tertiary" onClick={onCancel}>Cancel</Button>
{nextButtonConfig && (
Expand Down
33 changes: 33 additions & 0 deletions src/components/settings/HelpCenterButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Hyperlink } from '@edx/paragon';
import PropTypes from 'prop-types';

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

return (
<Hyperlink
{...rest}
target="_blank"
className="btn btn-outline-primary side-button"
destination={destinationUrl}
>
{children}
</Hyperlink>
);
};

HelpCenterButton.defaultProps = {
children: 'Help Center',
};

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

export default HelpCenterButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';

import { Form, Hyperlink } from '@edx/paragon';
import { dataPropType } from './constants';
import RegenerateCredentialWarningModal from './RegenerateCredentialWarningModal';
import CopyButton from './CopyButton';
import { API_CLIENT_DOCUMENTATION, HELP_CENTER_LINK } from '../data/constants';

const APICredentialsPage = ({ data, setData }) => {
const [formValue, setFormValue] = useState('');
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 api-cred-fields">
<h4>
Application name:
<span>{data?.name}</span>
</h4>
<h4>
Allowed URIs:
<span>{data?.redirect_uris}</span>
</h4>
<h4>
API client ID:
<span>{data?.client_id}</span>
</h4>
<h4>
API client secret:
<span>{data?.client_secret}</span>
</h4>
<h4>API client documentation:
<span>{API_CLIENT_DOCUMENTATION}</span>
</h4>
<h4>
Last generated on:
<span>{data?.updated}</span>
</h4>
<div className="my-3">
<CopyButton data={data} />
</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 URIs list, space separated
</p>
<RegenerateCredentialWarningModal
redirectURIs={formValue}
data={data}
setData={setData}
/>
</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;
<Hyperlink
variant="muted"
destination={HELP_CENTER_LINK}
>
contact Enterprise Customer Support.
</Hyperlink>
</p>
</div>
</div>
);
};

APICredentialsPage.defaultProps = {
data: null,
};

APICredentialsPage.propTypes = {
data: PropTypes.shape(dataPropType),
setData: PropTypes.func.isRequired,
};

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

export const ErrorContext = createContext(null);
export const ShowSuccessToast = createContext(null);
export const EnterpriseId = createContext(null);
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;
48 changes: 48 additions & 0 deletions src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';

import { Button } from '@edx/paragon';
import { ContentCopy } from '@edx/paragon/icons';
import CopiedToast from './CopiedToast';
import { dataPropType } from './constants';

const CopyButton = ({ data }) => {
const [isCopyLinkToastOpen, setIsCopyLinkToastOpen] = useState(false);
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 24 in src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx#L24

Added line #L24 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}
/>
</>
);
};

CopyButton.propTypes = {
data: PropTypes.shape(dataPropType),
};

export default CopyButton;
16 changes: 16 additions & 0 deletions src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Alert } from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import { credentialErrorMessage } from './constants';

const FailedAlert = () => (
<Alert variant="danger" icon={Error}>
<Alert.Heading>
Credential generation failed
</Alert.Heading>
<p>
{credentialErrorMessage}
</p>
</Alert>
);

export default FailedAlert;
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
ActionRow, Button, Icon, ModalDialog, useToggle,
} from '@edx/paragon';
import { Warning } from '@edx/paragon/icons';

import {
ErrorContext,
ShowSuccessToast, EnterpriseId,
} from './Context';
import LmsApiService from '../../../data/services/LmsApiService';
import { dataPropType } from './constants';

const RegenerateCredentialWarningModal = ({
redirectURIs,
data,
setData,
}) => {
const [isOn, setOn, setOff] = useToggle(false);
const [, setHasError] = useContext(ErrorContext);
const [, setShowSuccessToast] = useContext(ShowSuccessToast);
const enterpriseId = useContext(EnterpriseId);
const handleOnClickRegeneration = async () => {
try {
const response = await LmsApiService.regenerateAPICredentials(redirectURIs, enterpriseId);
const newURIs = response.data.redirect_uris;
setShowSuccessToast(true);
const updatedData = data;
updatedData.redirect_uris = newURIs;
setData(updatedData);
} 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="md"
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.propTypes = {
redirectURIs: PropTypes.string.isRequired,
data: PropTypes.shape(dataPropType),
setData: PropTypes.func.isRequired,
};

export default RegenerateCredentialWarningModal;
Loading