Skip to content

Commit

Permalink
feat: generate API Credentials in Admin Portal (#1027)
Browse files Browse the repository at this point in the history
* feat: adding api credential tab

* feat: add zero state card under api credentails tab

* feat: add a new tab

* fix: make lms-service run as expected

* fix: fix coupon.test.jsx lint error

* feat: generate API Credentials Tab in Admin Portal

* fix: modify modal

* fix: modify lmsservice url

* fix: remove dependency in useffect

* fix: add api-document url

* fix: lots of little fixes

* fix: test fixes

* fix: more changes

* fix: more fixes

* fix: PR review requests
  • Loading branch information
kiram15 authored Sep 8, 2023
1 parent 31f4dc8 commit 4da552c
Show file tree
Hide file tree
Showing 24 changed files with 924 additions and 33 deletions.
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);
};
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

0 comments on commit 4da552c

Please sign in to comment.