From d30576703a1ab772734d8900fdffd2db4ba4d255 Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Tue, 11 Jul 2023 21:42:03 +0000 Subject: [PATCH 01/15] feat: adding api credential tab --- .../SettingsApiCredentialsTab/Success.jsx | 82 +++++++++++++++++++ .../ZeroStateCard.jsx | 50 +++++++++++ .../SettingsApiCredentialsTab/index.jsx | 22 +++++ src/data/images/ZeroState.svg | 45 ++++++++++ 4 files changed, 199 insertions(+) create mode 100644 src/components/settings/SettingsApiCredentialsTab/Success.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/index.jsx create mode 100644 src/data/images/ZeroState.svg diff --git a/src/components/settings/SettingsApiCredentialsTab/Success.jsx b/src/components/settings/SettingsApiCredentialsTab/Success.jsx new file mode 100644 index 0000000000..470c352b4c --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/Success.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { + Col, Row, Hyperlink, Button, +} from '@edx/paragon'; + +const Success = () => ( +
+ + +

Your API credentials

+ +
+ + +

+ Copy and paste the following information and send it to your API developer(s). +

+ +
+ + +

Application name:

+ +
+ + +

Allowed URLs:

+ +
+ + +

API client secret

+ +
+ + +

API client documentation:

+ +
+ + +

Redirect URLs (optional)

+ +
+ + +

+ If you need additional redirect URLs, add them below and regenerate your API credentials. + You will need to communicate the new credentials to your API developers. +

+ +
+ + + + + + + +

Questions or modifications?

+ +
+ + +

+ To troubleshoot your API credentialing, or to request additional API endpoints to your credentials,  + + contact ECS. + +

+ +
+
+); + +export default Success; + diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx new file mode 100644 index 0000000000..d1da7f198b --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -0,0 +1,50 @@ +import { Card, Button } from '@edx/paragon'; +import { Add } from '@edx/paragon/icons'; +import PropTypes from 'prop-types'; +import cardImage from '../../../data/images/ZeroState.svg'; + +const ZeroStateCard = ({ + setShowZeroStateCard, +}) => { + const handleClick = () => { + // setShowZeroStateCard(false); + // eslint-disable-next-line no-console + console.log('ss'); + }; + + return ( + + + +

You don't hava API credentials yet.

+

+ edX for business API credentials will provide access to the following edX API endpoints: + reporting dashboard, dashboard, and catalog administration. +
+
+ By clicking the button below, you and your organization accept the {'\n'} + edX API terms of service. +

+ +
+
+ ); +}; + +ZeroStateCard.propTypes = { + setShowZeroStateCard: PropTypes.func.isRequired, +}; + +export default ZeroStateCard; diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx new file mode 100644 index 0000000000..ea47c334d5 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Hyperlink } from '@edx/paragon'; +import ZeroStateCard from './ZeroStateCard'; +import Success from './Success'; + +const SettingsApiCredentialsTab = () => ( +
+

API credentials + + Help Center: Credentials + +

+ + +
+); + +export default SettingsApiCredentialsTab; diff --git a/src/data/images/ZeroState.svg b/src/data/images/ZeroState.svg new file mode 100644 index 0000000000..ffbdc2813d --- /dev/null +++ b/src/data/images/ZeroState.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bf2d61fd11090718865a565b3dce90693fde3a66 Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Wed, 12 Jul 2023 20:07:07 +0000 Subject: [PATCH 02/15] feat: add zero state card under api credentails tab --- .env.development | 1 + .../ZeroStateCard.jsx | 4 +--- .../SettingsApiCredentialsTab/index.jsx | 2 +- src/components/settings/SettingsTabs.jsx | 19 ++++++++++++++++++- src/components/settings/data/constants.js | 4 ++++ src/config/index.js | 1 + 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.env.development b/.env.development index d74a56083c..03858d1ff4 100644 --- a/.env.development +++ b/.env.development @@ -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='' diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index d1da7f198b..7fc3fb5dbf 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -7,9 +7,7 @@ const ZeroStateCard = ({ setShowZeroStateCard, }) => { const handleClick = () => { - // setShowZeroStateCard(false); - // eslint-disable-next-line no-console - console.log('ss'); + }; return ( diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index ea47c334d5..35999cc95f 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -14,7 +14,7 @@ const SettingsApiCredentialsTab = () => ( Help Center: Credentials - + ); diff --git a/src/components/settings/SettingsTabs.jsx b/src/components/settings/SettingsTabs.jsx index 3b4f292ffd..7e62383f79 100644 --- a/src/components/settings/SettingsTabs.jsx +++ b/src/components/settings/SettingsTabs.jsx @@ -25,6 +25,7 @@ import SettingsLMSTab from './SettingsLMSTab'; import SettingsSSOTab from './SettingsSSOTab'; import { features } from '../../config'; import { updatePortalConfigurationEvent } from '../../data/actions/portalConfiguration'; +import SettingsApiCredentialsTab from './SettingsApiCredentialsTab'; const SettingsTabs = ({ enterpriseId, @@ -39,7 +40,11 @@ const SettingsTabs = ({ enterpriseBranding, }) => { const [hasSSOConfig, setHasSSOConfig] = useState(false); - const { FEATURE_SSO_SETTINGS_TAB, SETTINGS_PAGE_LMS_TAB, SETTINGS_PAGE_APPEARANCE_TAB } = features; + const { + FEATURE_SSO_SETTINGS_TAB, SETTINGS_PAGE_LMS_TAB, + SETTINGS_PAGE_APPEARANCE_TAB, + FEATURE_API_CREDENTIALS_TAB, + } = features; const tab = useCurrentSettingsTab(); @@ -113,9 +118,21 @@ const SettingsTabs = ({ , ); } + if (FEATURE_API_CREDENTIALS_TAB) { + initialTabs.push( + + + , + ); + } return initialTabs; }, [ FEATURE_SSO_SETTINGS_TAB, + FEATURE_API_CREDENTIALS_TAB, SETTINGS_PAGE_APPEARANCE_TAB, SETTINGS_PAGE_LMS_TAB, enableIntegratedCustomerLearnerPortalSearch, diff --git a/src/components/settings/data/constants.js b/src/components/settings/data/constants.js index e5b15ff434..3a719914f2 100644 --- a/src/components/settings/data/constants.js +++ b/src/components/settings/data/constants.js @@ -5,11 +5,13 @@ const ACCESS_TAB = 'access'; const LMS_TAB = 'lms'; const SSO_TAB = 'sso'; const APPEARANCE_TAB = 'appearance'; +const API_CREDENTIALS_TAB = 'api_credentials'; const ACCESS_TAB_LABEL = 'Configure Access'; const LMS_TAB_LABEL = 'Learning Platform'; const SSO_TAB_LABEL = 'Single Sign On (SSO)'; const APPEARANCE_TAB_LABEL = 'Portal Appearance'; +const API_CREDENTIALS_TAB_LABEL = 'API Credentials'; export const HELP_CENTER_LINK = 'https://business-support.edx.org/hc/en-us/categories/360000368453-Integrations'; export const HELP_CENTER_BLACKBOARD = 'https://business-support.edx.org/hc/en-us/sections/4405096719895-Blackboard'; @@ -58,6 +60,7 @@ export const SETTINGS_TABS_VALUES = { [LMS_TAB]: LMS_TAB, [SSO_TAB]: SSO_TAB, [APPEARANCE_TAB]: APPEARANCE_TAB, + [API_CREDENTIALS_TAB]: API_CREDENTIALS_TAB, }; /** @@ -68,6 +71,7 @@ export const SETTINGS_TAB_LABELS = { [LMS_TAB]: LMS_TAB_LABEL, [SSO_TAB]: SSO_TAB_LABEL, [APPEARANCE_TAB]: APPEARANCE_TAB_LABEL, + [API_CREDENTIALS_TAB]: API_CREDENTIALS_TAB_LABEL, }; /** Default tab when no parameter is given */ diff --git a/src/config/index.js b/src/config/index.js index 4fe281804e..409f9e25bb 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -52,6 +52,7 @@ const features = { SETTINGS_PAGE_APPEARANCE_TAB: process.env.FEATURE_SETTINGS_PAGE_APPEARANCE_TAB || hasFeatureFlagEnabled('SETTINGS_PAGE_APPEARANCE_TAB'), FEATURE_SSO_SETTINGS_TAB: process.env.FEATURE_SSO_SETTINGS_TAB || hasFeatureFlagEnabled('SSO_SETTINGS_TAB'), FEATURE_INTEGRATION_REPORTING: process.env.FEATURE_INTEGRATION_REPORTING || hasFeatureFlagEnabled('FEATURE_INTEGRATION_REPORTING'), + FEATURE_API_CREDENTIALS_TAB: process.env.FEATURE_API_CREDENTIALS_TAB || hasFeatureFlagEnabled('FEATURE_API_CREDENTIALS_TAB'), SUBSCRIPTION_LPR: process.env.SUBSCRIPTION_LPR || hasFeatureFlagEnabled('SUBSCRIPTION_LPR'), }; From ba90ddce40e25408c246a6eefd2be4195dca0dbb Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Mon, 17 Jul 2023 13:57:33 +0000 Subject: [PATCH 03/15] feat: add a new tab --- .../ContactCustomerSupportButton/index.jsx | 2 +- .../CopiedButton.jsx | 42 +++++++ .../SettingsApiCredentialsTab/CopiedToast.jsx | 13 +++ .../HelpCenterButton.jsx | 37 ++++++ .../RegenarateCredentialWarningModal.jsx | 58 ++++++++++ .../SettingsApiCredentialsTab/Success.jsx | 82 ------------- .../SettingsApiCredentialsTab/SuccessPage.jsx | 109 ++++++++++++++++++ .../ZeroStateCard.jsx | 44 +++++-- .../SettingsApiCredentialsTab/index.jsx | 46 +++++--- src/components/settings/data/constants.js | 1 + 10 files changed, 324 insertions(+), 110 deletions(-) create mode 100644 src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/HelpCenterButton.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx delete mode 100644 src/components/settings/SettingsApiCredentialsTab/Success.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx diff --git a/src/components/ContactCustomerSupportButton/index.jsx b/src/components/ContactCustomerSupportButton/index.jsx index 8c2e1408ca..b0bdee42cf 100644 --- a/src/components/ContactCustomerSupportButton/index.jsx +++ b/src/components/ContactCustomerSupportButton/index.jsx @@ -31,7 +31,7 @@ ContactCustomerSupportButton.propTypes = { ContactCustomerSupportButton.defaultProps = { children: 'Contact support', - variant: 'btn-outline-primary', + variant: 'outline-primary', }; export default ContactCustomerSupportButton; diff --git a/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx b/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx new file mode 100644 index 0000000000..c20e289ed8 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import { logError } from '@edx/frontend-platform/logging'; +import { Button } from '@edx/paragon'; +import { ContentCopy } from '@edx/paragon/icons'; +import CopiedToast from './CopiedToast'; + +const CopiedButton = () => { + const [isCopyLinkToastOpen, setIsCopyLinkToastOpen] = useState(false); + const hasClipboard = !!navigator.clipboard; + const text = []; + const addToClipboard = async () => { + try { + await navigator.clipboard.writeText(text); + setIsCopyLinkToastOpen(true); + } catch (error) { + logError(error); + } + }; + const handleCopyLink = () => { + if (!hasClipboard) { + return; + } + addToClipboard(); + }; + const handleCloseLinkCopyToast = () => { + setIsCopyLinkToastOpen(false); + }; + return ( +
+ + +
+ ); +}; + +export default CopiedButton; diff --git a/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx b/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx new file mode 100644 index 0000000000..9a2d011332 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Toast } from '@edx/paragon'; + +const CopiedToast = ({ content, ...rest }) => ( + + content + +); +CopiedToast.propTypes = { + content: PropTypes.string.isRequired, +}; +export default CopiedToast; diff --git a/src/components/settings/SettingsApiCredentialsTab/HelpCenterButton.jsx b/src/components/settings/SettingsApiCredentialsTab/HelpCenterButton.jsx new file mode 100644 index 0000000000..ea89801ae8 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/HelpCenterButton.jsx @@ -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 ( + + {children} + + ); +}; + +HelpCenterButton.defaultProps = { + children: 'Help Center', + variant: 'outline-primary', +}; + +HelpCenterButton.propTypes = { + children: PropTypes.node, + variant: PropTypes.string, + url: PropTypes.string, +}; + +export default HelpCenterButton; diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx new file mode 100644 index 0000000000..1326d823ea --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { + ActionRow, Icon, Button, ModalDialog, useToggle, Row, Col, +} from '@edx/paragon'; +import { Warning } from '@edx/paragon/icons'; + +const RegenarateCredentialWarningModal = ({ modalSize, modalVariant }) => { + const [isOn, setOn, setOff] = useToggle(false); + return ( + <> + + + + +
+ + API Regeneration Warning +
+
+
+ +

+ Are you sure you are ready to regenerate your API credentials?
+ Any system, job, or script using the previous credentials will no longer  + be able to authenticate with the edX API.
+ If you do regenerate, you will need to send the new credentials to your developers. +

+
+ + + + Cancel + + + + +
+ + ); +}; +export default RegenarateCredentialWarningModal; diff --git a/src/components/settings/SettingsApiCredentialsTab/Success.jsx b/src/components/settings/SettingsApiCredentialsTab/Success.jsx deleted file mode 100644 index 470c352b4c..0000000000 --- a/src/components/settings/SettingsApiCredentialsTab/Success.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import { - Col, Row, Hyperlink, Button, -} from '@edx/paragon'; - -const Success = () => ( -
- - -

Your API credentials

- -
- - -

- Copy and paste the following information and send it to your API developer(s). -

- -
- - -

Application name:

- -
- - -

Allowed URLs:

- -
- - -

API client secret

- -
- - -

API client documentation:

- -
- - -

Redirect URLs (optional)

- -
- - -

- If you need additional redirect URLs, add them below and regenerate your API credentials. - You will need to communicate the new credentials to your API developers. -

- -
- - - - - - - -

Questions or modifications?

- -
- - -

- To troubleshoot your API credentialing, or to request additional API endpoints to your credentials,  - - contact ECS. - -

- -
-
-); - -export default Success; - diff --git a/src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx b/src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx new file mode 100644 index 0000000000..3c68db6c96 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import { + Col, Row, Hyperlink, Form, Button, Toast +} from '@edx/paragon'; +import { ContentCopy } from '@edx/paragon/icons'; +import RegenarateCredentialWarningModal from './RegenarateCredentialWarningModal'; +import CopiedButton from './CopiedButton'; + +const SuccessPage = () => { + const [showToast, setShowToast] = useState(true); + + return ( +
+ setShowToast(false)} + show={showToast} + > + API credentials successfully generated + + + +

Your API credentials

+ +
+ + +

+ Copy and paste the following information and send it to your API developer(s). +

+ +
+ + +

Application name:{}

+ +
+ + +

Allowed URLs:{}

+ +
+ + +

API client secret:{}

+ +
+ + +

API client documentation:

+ +
+ + +

Last generated on:{}

+ +
+ + + {/* */} + + + + +

Redirect URLs (optional)

+ +
+ + +

+ If you need additional redirect URLs, add them below and regenerate your API credentials. + You will need to communicate the new credentials to your API developers. +

+ +
+ } + floatingLabel="Redirect URIs" + /> +

+ Allowed URI's list, space separated +

+ + + + + + + + +

Questions or modifications?

+ +
+ + +

+ To troubleshoot your API credentialing, or to request additional API endpoints to your credentials,  + + contact ECS. + +

+ +
+
+ ); +}; + +export default SuccessPage; diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index 7fc3fb5dbf..c7e4ab4a1b 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -1,13 +1,34 @@ import { Card, Button } from '@edx/paragon'; -import { Add } from '@edx/paragon/icons'; +import { Add, SpinnerSimple } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; +import React, { useState } from 'react'; +import { logError } from '@edx/frontend-platform/logging'; import cardImage from '../../../data/images/ZeroState.svg'; -const ZeroStateCard = ({ - setShowZeroStateCard, -}) => { +const ZeroStateCard = ({ onClickStateChange }) => { + const apiService = 'https://dummyjson.com/products/1'; + + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState([]); + + // const [isSuccessPageOpen, openSuccessPage, closeSuccessPage] = useToggle(false); + + async function fetchData() { + try { + const response = await fetch(apiService); + const responseData = await response.json(); + setData(responseData); + onClickStateChange(true); + } catch (err) { + logError(err); + } finally { + setIsLoading(false); + } + } + const handleClick = () => { - + setIsLoading(true); + fetchData(); }; return ( @@ -20,8 +41,11 @@ const ZeroStateCard = ({

You don't hava API credentials yet.

- edX for business API credentials will provide access to the following edX API endpoints: - reporting dashboard, dashboard, and catalog administration. + This page allows you to generate API credentials to send to  + your developers so they can work on integration projects. + If you believe you are seeing this page in error, contact Enterprise Customer Support. + edX for Business API credentials credentials will provide access  + to the following edX API endpoints: reporting dashboard, dashboard, and catalog administration.

By clicking the button below, you and your organization accept the {'\n'} @@ -30,11 +54,11 @@ const ZeroStateCard = ({ @@ -42,7 +66,7 @@ const ZeroStateCard = ({ }; ZeroStateCard.propTypes = { - setShowZeroStateCard: PropTypes.func.isRequired, + onClickStateChange: PropTypes.func.isRequired, }; export default ZeroStateCard; diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index 35999cc95f..c5eb86cd0f 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -1,22 +1,34 @@ -import React from 'react'; -import { Hyperlink } from '@edx/paragon'; +import React, { + useState, +} from 'react'; +import { ActionRow } from '@edx/paragon'; import ZeroStateCard from './ZeroStateCard'; -import Success from './Success'; +import SuccessPage from './SuccessPage'; +import { HELP_CENTER_API_GUIDE } from '../data/constants'; +import HelpCenterButton from './HelpCenterButton'; -const SettingsApiCredentialsTab = () => ( -

-

API credentials - - Help Center: Credentials - -

- +const SettingsApiCredentialsTab = () => { + const [displaySuccessPage, setDisplaySuccessPage] = useState(false); + const handleonClickStateChange = (state) => { + setDisplaySuccessPage(state); + }; -
-); + return ( +
+ +

API credentials +

+ + + Help Center: EdX Enterprise API Guide + +
+
+ {displaySuccessPage ? () : ()} +
+
+
+ ); +}; export default SettingsApiCredentialsTab; diff --git a/src/components/settings/data/constants.js b/src/components/settings/data/constants.js index 3a719914f2..8fba017be4 100644 --- a/src/components/settings/data/constants.js +++ b/src/components/settings/data/constants.js @@ -20,6 +20,7 @@ export const HELP_CENTER_CORNERSTONE = 'https://business-support.edx.org/hc/en-u export const HELP_CENTER_DEGREED = 'https://business-support.edx.org/hc/en-us/sections/360000868494-Degreed'; export const HELP_CENTER_MOODLE = 'https://business-support.edx.org/hc/en-us/sections/1500002758722-Moodle'; export const HELP_CENTER_SAP = 'https://business-support.edx.org/hc/en-us/sections/360000868534-SuccessFactors'; +export const HELP_CENTER_API_GUIDE = ''; export const HELP_CENTER_SAML_LINK = 'https://business-support.edx.org/hc/en-us/articles/360005421073-5-Implementing-Single-Sign-on-SSO-with-edX'; export const HELP_CENTER_SAP_IDP_LINK = 'https://business-support.edx.org/hc/en-us/articles/360005205314'; From a9505b8cc53018740db5f7f8427ec537bb0988cf Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Thu, 27 Jul 2023 12:49:50 +0000 Subject: [PATCH 04/15] fix: make lms-service run as expected --- .../APICredrentialsPage.jsx | 93 +++++++++++++++ .../SettingsApiCredentialsTab/CardFooter.jsx | 25 ++++ .../SettingsApiCredentialsTab/Context.jsx | 6 + .../CopiedButton.jsx | 16 +-- .../SettingsApiCredentialsTab/CopiedToast.jsx | 2 +- .../SettingsApiCredentialsTab/FailedAlert.jsx | 17 +++ .../RegenarateCredentialWarningModal.jsx | 58 ---------- .../RegenerateCredentialWarningModal.jsx | 98 ++++++++++++++++ .../SettingsApiCredentialsTab/SuccessPage.jsx | 109 ------------------ .../ZeroStateCard.jsx | 61 ++++++---- .../SettingsApiCredentialsTab/index.jsx | 95 +++++++++++---- src/components/settings/SettingsTabs.jsx | 7 +- src/components/settings/data/constants.js | 6 +- src/components/settings/settings.scss | 11 ++ src/data/services/LmsApiService.js | 17 +++ 15 files changed, 394 insertions(+), 227 deletions(-) create mode 100644 src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/Context.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx delete mode 100644 src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx delete mode 100644 src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx diff --git a/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx b/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx new file mode 100644 index 0000000000..0b47880b59 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { MailtoLink, Form } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import RegenerateCredentialWarningModal from './RegenerateCredentialWarningModal'; +import CopiedButton from './CopiedButton'; +import { ENTERPRISE_CUSTOMER_SUPPORT_EMAIL } from '../data/constants'; + +const APICredentialsPage = ({ data }) => { + const [formValue, setFormValue] = useState(''); + const handleFormChange = (e) => { + setFormValue(e.target.value); + }; + return ( +
+
+

Your API credentials

+

+ Copy and paste the following credential information and send it to your API developer(s). +

+
+
+

+ Application name:  + {data.name} +

+

+ Allowed URIs:  + {data.redirect_uris} +

+

+ API client ID:  + {data.client_id} +

+

+ API client secret:  + {data.client_secret} +

+

API client documentation:  + {data.api_client_documentation} +

+

+ Last generated on:  + {data.updated} +

+
+ +
+
+
+

Redirect URIs (optional)

+

+ 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. +

+ +

+ Allowed URI's list, space separated +

+ +
+
+

Questions or modifications?

+

+ To troubleshoot your API credentialing, or to request additional API endpoints to your + credentials,  + + contact Enterprise Customer Support. + +

+
+
+ ); +}; + +APICredentialsPage.propTypes = { + data: PropTypes.shape({ + name: PropTypes.string, + redirect_uris: PropTypes.string, + client_id: PropTypes.string, + client_secret: PropTypes.string, + api_client_documentation: PropTypes.string, + updated: PropTypes.string, + }), +}; + +export default APICredentialsPage; diff --git a/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx b/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx new file mode 100644 index 0000000000..43c739d346 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx @@ -0,0 +1,25 @@ +import React, { +} from 'react'; +import { Card, Icon } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; +import PropTypes from 'prop-types'; + +const CardFooter = ({ hasError, children }) => ( + + { hasError && ( +

+ + Something went wrong while generating your credentials. + Please try again. If the issue continues, contact Enterprise Customer Support. +

+ )} + {children} +
+); + +CardFooter.propTypes = { + hasError: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, +}; +export default CardFooter; + diff --git a/src/components/settings/SettingsApiCredentialsTab/Context.jsx b/src/components/settings/SettingsApiCredentialsTab/Context.jsx new file mode 100644 index 0000000000..e097bec004 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/Context.jsx @@ -0,0 +1,6 @@ +import { createContext } from 'react'; + +export const ZeroStateHandlerContext = createContext(true); +export const ErrorContext = createContext(false); +export const ShowSuccessToast = createContext(false); +export const DataContext = createContext(null); diff --git a/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx b/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx index c20e289ed8..8dc4389f1d 100644 --- a/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx @@ -4,13 +4,12 @@ import { Button } from '@edx/paragon'; import { ContentCopy } from '@edx/paragon/icons'; import CopiedToast from './CopiedToast'; -const CopiedButton = () => { +const CopiedButton = (api_credentials) => { const [isCopyLinkToastOpen, setIsCopyLinkToastOpen] = useState(false); const hasClipboard = !!navigator.clipboard; - const text = []; - const addToClipboard = async () => { + const addToClipboard = async (data) => { try { - await navigator.clipboard.writeText(text); + await navigator.clipboard.writeText(data); setIsCopyLinkToastOpen(true); } catch (error) { logError(error); @@ -20,13 +19,14 @@ const CopiedButton = () => { if (!hasClipboard) { return; } - addToClipboard(); + const jsonString = JSON.stringify(api_credentials); + addToClipboard(jsonString); }; const handleCloseLinkCopyToast = () => { setIsCopyLinkToastOpen(false); }; return ( -
+ <> - -
+ + ); }; diff --git a/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx b/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx index 9a2d011332..5506b7e14f 100644 --- a/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx @@ -4,7 +4,7 @@ import { Toast } from '@edx/paragon'; const CopiedToast = ({ content, ...rest }) => ( - content + {content} ); CopiedToast.propTypes = { diff --git a/src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx b/src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx new file mode 100644 index 0000000000..680211e91b --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx @@ -0,0 +1,17 @@ +import { Alert } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; + +const FailedAlert = () => ( + + + Credential generation failed + +

+ Something went wrong while generating your credentials. + Please try again. + If the issue continues, contact Enterprise Customer Support. +

+
+); + +export default FailedAlert; diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx deleted file mode 100644 index 1326d823ea..0000000000 --- a/src/components/settings/SettingsApiCredentialsTab/RegenarateCredentialWarningModal.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { - ActionRow, Icon, Button, ModalDialog, useToggle, Row, Col, -} from '@edx/paragon'; -import { Warning } from '@edx/paragon/icons'; - -const RegenarateCredentialWarningModal = ({ modalSize, modalVariant }) => { - const [isOn, setOn, setOff] = useToggle(false); - return ( - <> - - - - -
- - API Regeneration Warning -
-
-
- -

- Are you sure you are ready to regenerate your API credentials?
- Any system, job, or script using the previous credentials will no longer  - be able to authenticate with the edX API.
- If you do regenerate, you will need to send the new credentials to your developers. -

-
- - - - Cancel - - - - -
- - ); -}; -export default RegenarateCredentialWarningModal; diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx new file mode 100644 index 0000000000..c60cec482e --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx @@ -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 { ErrorContext, DataContext, ShowSuccessToast } from './Context'; +import LmsApiService from '../../../data/services/LmsApiService'; + +const RegenerateCredentialWarningModal = ({ + modalSize, + modalVariant, + redirectURLs, + setRedirectURIs, +}) => { + const [isOn, setOn, setOff] = useToggle(false); + const hasErrorContext = useContext(ErrorContext); + const dataContext = useContext(DataContext); + const showSuccessToast = useContext(ShowSuccessToast); + const handleOnClickRegeneration = async () => { + try { + const response = await LmsApiService.regenerateAPICredentials(redirectURLs); + dataContext(response.data); + showSuccessToast(true); + setRedirectURIs(''); + } catch (error) { + hasErrorContext(true); + } finally { + setOff(true); + } + }; + return ( + <> + + + + +
+ + Regenerate API credentials? +
+
+
+ +

+ Any system, job, or script using the previous credentials will no + longer be able to authenticate with the edX API. +

+

+ If you do regenerate, you will need to send the new credentials to your developers. +

+
+ + + + Cancel + + + + +
+ + ); +}; + +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; diff --git a/src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx b/src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx deleted file mode 100644 index 3c68db6c96..0000000000 --- a/src/components/settings/SettingsApiCredentialsTab/SuccessPage.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useState } from 'react'; -import { - Col, Row, Hyperlink, Form, Button, Toast -} from '@edx/paragon'; -import { ContentCopy } from '@edx/paragon/icons'; -import RegenarateCredentialWarningModal from './RegenarateCredentialWarningModal'; -import CopiedButton from './CopiedButton'; - -const SuccessPage = () => { - const [showToast, setShowToast] = useState(true); - - return ( -
- setShowToast(false)} - show={showToast} - > - API credentials successfully generated - - - -

Your API credentials

- -
- - -

- Copy and paste the following information and send it to your API developer(s). -

- -
- - -

Application name:{}

- -
- - -

Allowed URLs:{}

- -
- - -

API client secret:{}

- -
- - -

API client documentation:

- -
- - -

Last generated on:{}

- -
- - - {/* */} - - - - -

Redirect URLs (optional)

- -
- - -

- If you need additional redirect URLs, add them below and regenerate your API credentials. - You will need to communicate the new credentials to your API developers. -

- -
- } - floatingLabel="Redirect URIs" - /> -

- Allowed URI's list, space separated -

- - - - - - - - -

Questions or modifications?

- -
- - -

- To troubleshoot your API credentialing, or to request additional API endpoints to your credentials,  - - contact ECS. - -

- -
-
- ); -}; - -export default SuccessPage; diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index c7e4ab4a1b..244123b2f2 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -1,31 +1,32 @@ -import { Card, Button } from '@edx/paragon'; -import { Add, SpinnerSimple } from '@edx/paragon/icons'; -import PropTypes from 'prop-types'; -import React, { useState } from 'react'; -import { logError } from '@edx/frontend-platform/logging'; +import { Card, Button, Icon } from '@edx/paragon'; +import { Add, SpinnerSimple, Error } from '@edx/paragon/icons'; +import React, { useState, useContext } from 'react'; import cardImage from '../../../data/images/ZeroState.svg'; +import { ZeroStateHandlerContext, ShowSuccessToast, DataContext } from './Context'; +import LmsApiService from '../../../data/services/LmsApiService'; +import { API_TERMS_OF_SERVICE } from '../data/constants'; -const ZeroStateCard = ({ onClickStateChange }) => { - const apiService = 'https://dummyjson.com/products/1'; - +const ZeroStateCard = () => { + const zeroStateContextHandler = useContext(ZeroStateHandlerContext); + const showToastContext = useContext(ShowSuccessToast); + const dataContext = useContext(DataContext); const [isLoading, setIsLoading] = useState(false); - const [data, setData] = useState([]); - - // const [isSuccessPageOpen, openSuccessPage, closeSuccessPage] = useToggle(false); + const [displayFailureAlert, setFailureAlert] = useState(false); async function fetchData() { try { - const response = await fetch(apiService); - const responseData = await response.json(); - setData(responseData); - onClickStateChange(true); + const response = await LmsApiService.createNewAPICredentials(); + console.log(response); + dataContext(response.data); + zeroStateContextHandler(false); + showToastContext(true); } catch (err) { - logError(err); + setFailureAlert(true); + zeroStateContextHandler(true); } finally { setIsLoading(false); } } - const handleClick = () => { setIsLoading(true); fetchData(); @@ -40,17 +41,31 @@ const ZeroStateCard = ({ onClickStateChange }) => { />

You don't hava API credentials yet.

+ { !displayFailureAlert && (

This page allows you to generate API credentials to send to  your developers so they can work on integration projects. If you believe you are seeing this page in error, contact Enterprise Customer Support. + +

+ )} +

edX for Business API credentials credentials will provide access  to the following edX API endpoints: reporting dashboard, dashboard, and catalog administration. -
-
+

+

By clicking the button below, you and your organization accept the {'\n'} - edX API terms of service. + edX API terms of service. +

+
+ + { displayFailureAlert && ( +

+ + Something went wrong while generating your credentials. + Please try again. If the issue continues, contact Enterprise Customer Support.

+ )} - +
); }; -ZeroStateCard.propTypes = { - onClickStateChange: PropTypes.func.isRequired, -}; - export default ZeroStateCard; diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index c5eb86cd0f..1f4a6bbdb9 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -1,33 +1,82 @@ -import React, { - useState, -} from 'react'; -import { ActionRow } from '@edx/paragon'; +import React, { useState, useEffect } from 'react'; +import { ActionRow, Toast } from '@edx/paragon'; +import { logError } from '@edx/frontend-platform/logging'; import ZeroStateCard from './ZeroStateCard'; -import SuccessPage from './SuccessPage'; -import { HELP_CENTER_API_GUIDE } from '../data/constants'; +import APICredentialsPage from './APICredrentialsPage'; +import FailedAlert from './FailedAlert'; +import { HELP_CENTER_API_GUIDE, API_CLIENT_DOCUMENTATION } from '../data/constants'; import HelpCenterButton from './HelpCenterButton'; +import { + ZeroStateHandlerContext, ErrorContext, ShowSuccessToast, DataContext, +} from './Context'; +import LmsApiService from '../../../data/services/LmsApiService'; const SettingsApiCredentialsTab = () => { - const [displaySuccessPage, setDisplaySuccessPage] = useState(false); - const handleonClickStateChange = (state) => { - setDisplaySuccessPage(state); + const [displayZeroState, setDisplayZeroState] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [existingData, setExistingData] = useState({}); + const fetchExistingAPICredentials = async () => { + setIsLoading(false); + try { + const response = await LmsApiService.fetchAPICredentials(); + const { results } = response.data; + const result = results[0]; + setExistingData({ + name: result.name, + redirect_uris: result.redirect_uris, + client_id: result.client_id, + client_secret: result.client_secret, + api_client_documentation: API_CLIENT_DOCUMENTATION, + updated: result.updated, + }); + setDisplayZeroState(false); + } catch (error) { + setDisplayZeroState(true); + logError(error); + } }; + const [hasRegenerationError, setHasRegenerationError] = useState(false); + const [showToast, setShowToast] = useState(false); + useEffect(() => { + fetchExistingAPICredentials(); + }, []); return ( -
- -

API credentials -

- - - Help Center: EdX Enterprise API Guide - -
-
- {displaySuccessPage ? () : ()} -
-
-
+ + + + + { hasRegenerationError && } + +

API credentials

+ + + Help Center: EdX Enterprise API Guide + +
+
+ {!isLoading + && ( + !displayZeroState ? ( + + ) : () + )} +
+
+ { showToast && ( + setShowToast(false)} + show={showToast} + > + API credentials successfully generated + + )} + + + + ); }; diff --git a/src/components/settings/SettingsTabs.jsx b/src/components/settings/SettingsTabs.jsx index 7e62383f79..f00c977075 100644 --- a/src/components/settings/SettingsTabs.jsx +++ b/src/components/settings/SettingsTabs.jsx @@ -23,9 +23,9 @@ import SettingsAccessTab from './SettingsAccessTab'; import { SettingsAppearanceTab } from './SettingsAppearanceTab'; import SettingsLMSTab from './SettingsLMSTab'; import SettingsSSOTab from './SettingsSSOTab'; +import SettingsApiCredentialsTab from './SettingsApiCredentialsTab'; import { features } from '../../config'; import { updatePortalConfigurationEvent } from '../../data/actions/portalConfiguration'; -import SettingsApiCredentialsTab from './SettingsApiCredentialsTab'; const SettingsTabs = ({ enterpriseId, @@ -125,7 +125,10 @@ const SettingsTabs = ({ eventKey={SETTINGS_TABS_VALUES.api_credentials} title={SETTINGS_TAB_LABELS.api_credentials} > - + , ); } diff --git a/src/components/settings/data/constants.js b/src/components/settings/data/constants.js index 8fba017be4..1cb5775360 100644 --- a/src/components/settings/data/constants.js +++ b/src/components/settings/data/constants.js @@ -20,12 +20,16 @@ export const HELP_CENTER_CORNERSTONE = 'https://business-support.edx.org/hc/en-u export const HELP_CENTER_DEGREED = 'https://business-support.edx.org/hc/en-us/sections/360000868494-Degreed'; export const HELP_CENTER_MOODLE = 'https://business-support.edx.org/hc/en-us/sections/1500002758722-Moodle'; export const HELP_CENTER_SAP = 'https://business-support.edx.org/hc/en-us/sections/360000868534-SuccessFactors'; -export const HELP_CENTER_API_GUIDE = ''; +export const HELP_CENTER_API_GUIDE = 'https://edx-enterprise-api.readthedocs.io/en/latest/index.html'; export const HELP_CENTER_SAML_LINK = 'https://business-support.edx.org/hc/en-us/articles/360005421073-5-Implementing-Single-Sign-on-SSO-with-edX'; export const HELP_CENTER_SAP_IDP_LINK = 'https://business-support.edx.org/hc/en-us/articles/360005205314'; export const HELP_CENTER_BRANDING_LINK = 'https://business-support.edx.org/hc/en-us/sections/8739219372183'; +export const API_CLIENT_DOCUMENTATION = 'https://edx-enterprise-api.readthedocs.io/en/latest/index.html'; +export const API_TERMS_OF_SERVICE = 'https://courses.edx.org/api-admin/terms-of-service/'; +export const ENTERPRISE_CUSTOMER_SUPPORT_EMAIL = 'enterprise-support@edx.org'; + export const ACTIVATE_TOAST_MESSAGE = 'Learning platform integration successfully activated.'; export const DELETE_TOAST_MESSAGE = 'Learning platform integration successfully removed.'; export const INACTIVATE_TOAST_MESSAGE = 'Learning platform integration successfully disabled.'; diff --git a/src/components/settings/settings.scss b/src/components/settings/settings.scss index 0dca7c30bc..635e0e7ec6 100644 --- a/src/components/settings/settings.scss +++ b/src/components/settings/settings.scss @@ -160,4 +160,15 @@ .stepper-modal .pgn__modal-header { border-bottom: solid 7px; +} + +.warning-icon { + color: #F0CC00; +} + +.alert-block-header { + background: $danger-100; + .error-icon { + color: $danger-600; + } } \ No newline at end of file diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js index 8a2fb6c96e..2af4d53804 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -35,6 +35,8 @@ class LmsApiService { static enterpriseCustomerInviteKeyUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise-customer-invite-key/`; + static apiCredentialsUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise_customer_api_credentials/`; + static fetchEnterpriseList(options) { const queryParams = new URLSearchParams({ page: 1, @@ -329,6 +331,21 @@ class LmsApiService { const url = `${LmsApiService.enterpriseCustomerUrl}${enterpriseUUID}/toggle_universal_link/`; return LmsApiService.apiClient().patch(url, formData); } + + static fetchAPICredentials() { + return LmsApiService.apiClient().get(`${LmsApiService.apiCredentialsUrl}`); + } + + static createNewAPICredentials() { + return LmsApiService.apiClient().post(`${LmsApiService.apiCredentialsUrl}`); + } + + static regenerateAPICredentials(redirectURLs) { + const requestData = { + redirect_uris: redirectURLs, + }; + return LmsApiService.apiClient().patch(`${LmsApiService.apiCredentialsUrl}regenerate_credentials/`, requestData); + } } export default LmsApiService; From cf3e3c06e304bd901c19fe9d41658b26bcfa7d99 Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Thu, 27 Jul 2023 12:56:03 +0000 Subject: [PATCH 05/15] fix: fix coupon.test.jsx lint error --- src/components/Coupon/Coupon.test.jsx | 12 ++++++------ .../SettingsApiCredentialsTab/CardFooter.jsx | 2 +- .../SettingsApiCredentialsTab/ZeroStateCard.jsx | 1 - src/components/settings/settings.scss | 11 ----------- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/components/Coupon/Coupon.test.jsx b/src/components/Coupon/Coupon.test.jsx index e0a7bd0730..149d7bbf97 100644 --- a/src/components/Coupon/Coupon.test.jsx +++ b/src/components/Coupon/Coupon.test.jsx @@ -67,7 +67,7 @@ describe('', () => { expect(coupon).toMatchSnapshot(); }); - it("without max uses", () => { + it('without max uses', () => { const coupon = renderer .create( ', () => { ...initialCouponData, max_uses: null, }} - /> + />, ) .toJSON(); expect(coupon).toMatchSnapshot(); }); - it("with error state", () => { + it('with error state', () => { const coupon = renderer .create( + />, ) .toJSON(); expect(coupon).toMatchSnapshot(); @@ -138,7 +138,7 @@ describe('', () => { + />, ); fireEvent.keyDown(screen.getByRole('button'), { key: 'A', code: 'KeyA' }); diff --git a/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx b/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx index 43c739d346..f185c589e4 100644 --- a/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx @@ -21,5 +21,5 @@ CardFooter.propTypes = { hasError: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, }; -export default CardFooter; +export default CardFooter; diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index 244123b2f2..8fd2f48023 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -16,7 +16,6 @@ const ZeroStateCard = () => { async function fetchData() { try { const response = await LmsApiService.createNewAPICredentials(); - console.log(response); dataContext(response.data); zeroStateContextHandler(false); showToastContext(true); diff --git a/src/components/settings/settings.scss b/src/components/settings/settings.scss index 635e0e7ec6..ecbc1b9ff4 100644 --- a/src/components/settings/settings.scss +++ b/src/components/settings/settings.scss @@ -161,14 +161,3 @@ .stepper-modal .pgn__modal-header { border-bottom: solid 7px; } - -.warning-icon { - color: #F0CC00; -} - -.alert-block-header { - background: $danger-100; - .error-icon { - color: $danger-600; - } -} \ No newline at end of file From ada7260da5ed3e23e6a79c29fa89058b5a160014 Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Mon, 31 Jul 2023 18:10:28 +0000 Subject: [PATCH 06/15] feat: generate API Credentials Tab in Admin Portal --- .../APICredrentialsPage.jsx | 34 +-- .../SettingsApiCredentialsTab/CardFooter.jsx | 25 -- .../SettingsApiCredentialsTab/Context.jsx | 6 +- .../CopiedButton.jsx | 33 ++- .../RegenerateCredentialWarningModal.jsx | 16 +- .../ZeroStateCard.jsx | 25 +- .../SettingsApiCredentialsTab/index.jsx | 17 +- .../SettingsAPICredrentialsPage.test.jsx | 280 ++++++++++++++++++ src/components/settings/settings.scss | 4 + 9 files changed, 343 insertions(+), 97 deletions(-) delete mode 100644 src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx diff --git a/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx b/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx index 0b47880b59..ec8b89c8cb 100644 --- a/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx @@ -1,12 +1,14 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { MailtoLink, Form } from '@edx/paragon'; -import PropTypes from 'prop-types'; import RegenerateCredentialWarningModal from './RegenerateCredentialWarningModal'; import CopiedButton from './CopiedButton'; import { ENTERPRISE_CUSTOMER_SUPPORT_EMAIL } from '../data/constants'; +import { DataContext } from './Context'; -const APICredentialsPage = ({ data }) => { +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); }; @@ -21,29 +23,29 @@ const APICredentialsPage = ({ data }) => {

Application name:  - {data.name} + {data?.name}

Allowed URIs:  - {data.redirect_uris} + {data?.redirect_uris}

API client ID:  - {data.client_id} + {data?.client_id}

API client secret:  - {data.client_secret} + {data?.client_secret}

API client documentation:  - {data.api_client_documentation} + {data?.api_client_documentation}

Last generated on:  - {data.updated} + {data?.updated}

- +
@@ -56,6 +58,7 @@ const APICredentialsPage = ({ data }) => { value={formValue} onChange={handleFormChange} floatingLabel="Redirect URIs" + data-testid="form-control" />

Allowed URI's list, space separated @@ -79,15 +82,4 @@ const APICredentialsPage = ({ data }) => { ); }; -APICredentialsPage.propTypes = { - data: PropTypes.shape({ - name: PropTypes.string, - redirect_uris: PropTypes.string, - client_id: PropTypes.string, - client_secret: PropTypes.string, - api_client_documentation: PropTypes.string, - updated: PropTypes.string, - }), -}; - export default APICredentialsPage; diff --git a/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx b/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx deleted file mode 100644 index f185c589e4..0000000000 --- a/src/components/settings/SettingsApiCredentialsTab/CardFooter.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { -} from 'react'; -import { Card, Icon } from '@edx/paragon'; -import { Error } from '@edx/paragon/icons'; -import PropTypes from 'prop-types'; - -const CardFooter = ({ hasError, children }) => ( - - { hasError && ( -

- - Something went wrong while generating your credentials. - Please try again. If the issue continues, contact Enterprise Customer Support. -

- )} - {children} - -); - -CardFooter.propTypes = { - hasError: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, -}; - -export default CardFooter; diff --git a/src/components/settings/SettingsApiCredentialsTab/Context.jsx b/src/components/settings/SettingsApiCredentialsTab/Context.jsx index e097bec004..2b5a2f081b 100644 --- a/src/components/settings/SettingsApiCredentialsTab/Context.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/Context.jsx @@ -1,6 +1,6 @@ import { createContext } from 'react'; -export const ZeroStateHandlerContext = createContext(true); -export const ErrorContext = createContext(false); -export const ShowSuccessToast = createContext(false); +export const ZeroStateHandlerContext = createContext(null); +export const ErrorContext = createContext(null); +export const ShowSuccessToast = createContext(null); export const DataContext = createContext(null); diff --git a/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx b/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx index 8dc4389f1d..78e8d9309c 100644 --- a/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx @@ -1,26 +1,23 @@ -import React, { useState } from 'react'; -import { logError } from '@edx/frontend-platform/logging'; +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 = (api_credentials) => { +const CopiedButton = () => { const [isCopyLinkToastOpen, setIsCopyLinkToastOpen] = useState(false); - const hasClipboard = !!navigator.clipboard; - const addToClipboard = async (data) => { + const [data] = useContext(DataContext); + const [copiedError, setCopiedError] = useState(false); + + const handleCopyLink = async () => { try { - await navigator.clipboard.writeText(data); - setIsCopyLinkToastOpen(true); + const jsonString = JSON.stringify(data); + await navigator.clipboard.writeText(jsonString); } catch (error) { - logError(error); - } - }; - const handleCopyLink = () => { - if (!hasClipboard) { - return; + setCopiedError(true); + } finally { + setIsCopyLinkToastOpen(true); } - const jsonString = JSON.stringify(api_credentials); - addToClipboard(jsonString); }; const handleCloseLinkCopyToast = () => { setIsCopyLinkToastOpen(false); @@ -34,7 +31,11 @@ const CopiedButton = (api_credentials) => { > Copy credentials to clipboard - + ); }; diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx index c60cec482e..5e075512c4 100644 --- a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx @@ -4,7 +4,7 @@ import { } from '@edx/paragon'; import { Warning } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; -import { ErrorContext, DataContext, ShowSuccessToast } from './Context'; +import { DataContext, ErrorContext, ShowSuccessToast } from './Context'; import LmsApiService from '../../../data/services/LmsApiService'; const RegenerateCredentialWarningModal = ({ @@ -14,17 +14,17 @@ const RegenerateCredentialWarningModal = ({ setRedirectURIs, }) => { const [isOn, setOn, setOff] = useToggle(false); - const hasErrorContext = useContext(ErrorContext); - const dataContext = useContext(DataContext); - const showSuccessToast = useContext(ShowSuccessToast); + const [, setHasError] = useContext(ErrorContext); + const [, setData] = useContext(DataContext); + const [, setShowSuccessToast] = useContext(ShowSuccessToast); const handleOnClickRegeneration = async () => { try { const response = await LmsApiService.regenerateAPICredentials(redirectURLs); - dataContext(response.data); - showSuccessToast(true); + setData(response.data); + setShowSuccessToast(true); setRedirectURIs(''); } catch (error) { - hasErrorContext(true); + setHasError(true); } finally { setOff(true); } @@ -48,7 +48,7 @@ const RegenerateCredentialWarningModal = ({ isFullscreenOnMobile isFullscreenScroll > - +
diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index 8fd2f48023..721e7dff43 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -7,28 +7,26 @@ import LmsApiService from '../../../data/services/LmsApiService'; import { API_TERMS_OF_SERVICE } from '../data/constants'; const ZeroStateCard = () => { - const zeroStateContextHandler = useContext(ZeroStateHandlerContext); - const showToastContext = useContext(ShowSuccessToast); - const dataContext = useContext(DataContext); + const [, setZeroState] = useContext(ZeroStateHandlerContext); + const [, setShowToast] = useContext(ShowSuccessToast); + const [, setData] = useContext(DataContext); const [isLoading, setIsLoading] = useState(false); const [displayFailureAlert, setFailureAlert] = useState(false); - async function fetchData() { + const handleClick = async () => { + setIsLoading(true); try { const response = await LmsApiService.createNewAPICredentials(); - dataContext(response.data); - zeroStateContextHandler(false); - showToastContext(true); + setData(response.data); + setIsLoading(false); + setZeroState(false); + setShowToast(true); } catch (err) { setFailureAlert(true); - zeroStateContextHandler(true); - } finally { + setIsLoading(false); + setZeroState(true); setIsLoading(false); } - } - const handleClick = () => { - setIsLoading(true); - fetchData(); }; return ( @@ -45,7 +43,6 @@ const ZeroStateCard = () => { This page allows you to generate API credentials to send to  your developers so they can work on integration projects. If you believe you are seeing this page in error, contact Enterprise Customer Support. -

)}

diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index 1f4a6bbdb9..bb00b67495 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -1,6 +1,6 @@ +/* eslint-disable react/jsx-no-constructed-context-values */ import React, { useState, useEffect } from 'react'; import { ActionRow, Toast } from '@edx/paragon'; -import { logError } from '@edx/frontend-platform/logging'; import ZeroStateCard from './ZeroStateCard'; import APICredentialsPage from './APICredrentialsPage'; import FailedAlert from './FailedAlert'; @@ -14,7 +14,7 @@ import LmsApiService from '../../../data/services/LmsApiService'; const SettingsApiCredentialsTab = () => { const [displayZeroState, setDisplayZeroState] = useState(false); const [isLoading, setIsLoading] = useState(true); - const [existingData, setExistingData] = useState({}); + const [existingData, setExistingData] = useState(null); const fetchExistingAPICredentials = async () => { setIsLoading(false); try { @@ -32,7 +32,6 @@ const SettingsApiCredentialsTab = () => { setDisplayZeroState(false); } catch (error) { setDisplayZeroState(true); - logError(error); } }; const [hasRegenerationError, setHasRegenerationError] = useState(false); @@ -42,10 +41,10 @@ const SettingsApiCredentialsTab = () => { }, []); return ( - - - - + + + + { hasRegenerationError && }

API credentials

@@ -58,9 +57,7 @@ const SettingsApiCredentialsTab = () => { {!isLoading && ( !displayZeroState ? ( - + ) : () )}
diff --git a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx new file mode 100644 index 0000000000..6b9017d5cc --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx @@ -0,0 +1,280 @@ +import { + render, screen, waitFor, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import LmsApiService from '../../../../data/services/LmsApiService'; +import SettingsApiCredentialsTab from '../index'; +import { + API_CLIENT_DOCUMENTATION, HELP_CENTER_API_GUIDE, API_TERMS_OF_SERVICE, ENTERPRISE_CUSTOMER_SUPPORT_EMAIL, +} from '../../data/constants'; + +jest.mock('../../../../data/services/LmsApiService', () => ({ + fetchAPICredentials: jest.fn(), + createNewAPICredentials: jest.fn(), + regenerateAPICredentials: jest.fn(), +})); + +const name = 'The Whinery Spirits Company'; +const clientId = 'y0TCvOEvvIs6ll95irirzCJ5EaF0RnSbBIIXuNJE'; +const clientSecret = '1G896sVeT67jtjHO6FNd5qFqayZPIV7BtnW01P8zaAd4mDfmBVVVsUP33u'; +const apiClientDocumentation = API_CLIENT_DOCUMENTATION; +const updated = '2023-07-28T04:28:20.909550Z'; +const redirectUris = 'www.customercourses.edx.com, www.customercourses.edx.stage.com'; +const returnResponse = { + results: [{ + name, + client_id: clientId, + client_secret: clientSecret, + api_client_documentation: apiClientDocumentation, + updated, + }], +}; +const data = { + name, + client_id: clientId, + client_secret: clientSecret, + api_client_documentation: apiClientDocumentation, + updated, +}; +const regenerationDate = { + ...data, + redirect_uris: redirectUris, +}; + +describe('API Credentials Tab', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders zero state page when having no api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockFetchFn.mockRejectedValue(); + mockCreatFn.mockResolvedValue(); + + render( + + + , + ); + expect(screen.getByText('API credentials')).toBeInTheDocument(); + expect(screen.queryByText("You don't hava API credentials yet.")).toBeNull(); + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + expect(screen.getByText("You don't hava API credentials yet.")).toBeInTheDocument(); + expect(screen.queryByText('Help Center: EdX Enterprise API Guide')).toBeInTheDocument(); + const helpLink = screen.getByText('Help Center: EdX Enterprise API Guide'); + expect(helpLink.getAttribute('href')).toBe(HELP_CENTER_API_GUIDE); + const serviceLink = screen.getByText('edX API terms of service'); + expect(serviceLink.getAttribute('href')).toBe(API_TERMS_OF_SERVICE); + + expect(screen.getByText('Generate API Credentials').toBeInTheDocument); + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => expect(mockCreatFn).toHaveBeenCalled()); + }); + test('renders api credentials page when having existing api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data: returnResponse }); + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + + expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: 'Allowed URIs:' }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client documentation: ${apiClientDocumentation}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Last generated on: ${updated}` }).toBeInTheDocument); + const link = screen.getByText('contact Enterprise Customer Support.'); + expect(link.getAttribute('href')).toBe(`mailto:${ENTERPRISE_CUSTOMER_SUPPORT_EMAIL}`); + }); + test('renders error stage while creating new api credentials through clicking generation button', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockRejectedValue(); + const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockCreatFn.mockRejectedValue(); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => { expect(mockCreatFn).toHaveBeenCalled(); }); + expect( + screen.getByText( + 'Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.', + ), + ).toBeInTheDocument(); + }); + test('renders api credentials page after successfully creating api credentials through clicking generation button', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockRejectedValue(); + const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockCreatFn.mockResolvedValue({ data }); + const writeText = jest.fn(); + Object.assign(navigator, { + clipboard: { + writeText, + }, + }); + const jsonString = JSON.stringify(data); + navigator.clipboard.writeText.mockResolvedValue(jsonString); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => expect(mockCreatFn).toHaveBeenCalled()); + expect(screen.getByText('API credentials successfully generated')).toBeInTheDocument(); + const closeButton = screen.getByLabelText('Close'); + userEvent.click(closeButton); + await waitFor(() => { + expect(screen.queryByText('API credentials successfully generated')).not.toBeInTheDocument(); + }); + + expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: 'Allowed URIs:' }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client documentation: ${apiClientDocumentation}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Last generated on: ${updated}` }).toBeInTheDocument); + + const copyBtn = screen.getByText('Copy credentials to clipboard'); + userEvent.click(copyBtn); + await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith(jsonString)); + await waitFor(() => expect(screen.getByText('Copied Successfully')).toBeInTheDocument()); + }); + test('renders error message when failing to copying api credentials to clipboard', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockRejectedValue(); + const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockCreatFn.mockResolvedValue({ data }); + const writeText = jest.fn(); + Object.assign(navigator, { + clipboard: { + writeText, + }, + }); + const jsonString = JSON.stringify(data); + navigator.clipboard.writeText.mockRejectedValue(); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => expect(mockCreatFn).toHaveBeenCalled()); + const copyBtn = screen.getByText('Copy credentials to clipboard'); + userEvent.click(copyBtn); + await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith(jsonString)); + await waitFor(() => expect(screen.getByText('Cannot copied to the clipboard')).toBeInTheDocument()); + }); + test('renders api credentials page after successfully regenerating api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data: returnResponse }); + const mockPatchFn = jest.spyOn(LmsApiService, 'regenerateAPICredentials'); + mockPatchFn.mockResolvedValue({ data: regenerationDate }); + + render( + + + , + ); + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + const input = screen.getByTestId('form-control'); + expect(input).toHaveValue(''); + userEvent.type(input, redirectUris); + await waitFor(() => expect(input).toHaveValue(redirectUris)); + const button = screen.getByText('Regenerate API Credentials'); + userEvent.click(button); + + await waitFor(() => expect(screen.getByText('Regenerate API credentials?')).toBeInTheDocument()); + const confirmedButton = screen.getByText('Regenerate'); + userEvent.click(confirmedButton); + await waitFor(() => { + expect(mockPatchFn).toHaveBeenCalledWith(redirectUris); + }); + expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client documentation: ${apiClientDocumentation}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Last generated on: ${updated}` }).toBeInTheDocument); + expect(screen.queryByText('Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.')) + .not.toBeInTheDocument(); + }); + test('renders error state when failing to regenerating api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data: returnResponse }); + const mockPatchFn = jest.spyOn(LmsApiService, 'regenerateAPICredentials'); + mockPatchFn.mockRejectedValue(); + + render( + + + , + ); + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + const input = screen.getByTestId('form-control'); + expect(input).toHaveValue(''); + userEvent.type(input, redirectUris); + await waitFor(() => expect(input).toHaveValue(redirectUris)); + const button = screen.getByText('Regenerate API Credentials'); + userEvent.click(button); + + await waitFor(() => expect(screen.getByText('Regenerate API credentials?')).toBeInTheDocument()); + const confirmedButton = screen.getByText('Regenerate'); + userEvent.click(confirmedButton); + await waitFor(() => { + expect(mockPatchFn).toHaveBeenCalledWith(redirectUris); + }); + expect(screen.getByRole('heading', { name: 'Allowed URIs:' }).toBeInTheDocument); + expect(screen.getByText('Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.')) + .toBeInTheDocument(); + }); + test('renders api credentials when canceling regenerating api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data: returnResponse }); + const mockPatchFn = jest.spyOn(LmsApiService, 'regenerateAPICredentials'); + mockPatchFn.mockResolvedValue({ data: regenerationDate }); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + const input = screen.getByTestId('form-control'); + expect(input).toHaveValue(''); + userEvent.type(input, redirectUris); + await waitFor(() => expect(input).toHaveValue(redirectUris)); + const button = screen.getByText('Regenerate API Credentials'); + userEvent.click(button); + + await waitFor(() => expect(screen.getByText('Regenerate API credentials?')).toBeInTheDocument()); + const cancelButton = screen.getByText('Cancel'); + userEvent.click(cancelButton); + await waitFor(() => { + expect(mockPatchFn).not.toHaveBeenCalledWith(redirectUris); + }); + expect(screen.getByRole('heading', { name: 'Allowed URIs:' }).toBeInTheDocument); + }); +}); diff --git a/src/components/settings/settings.scss b/src/components/settings/settings.scss index ecbc1b9ff4..5816c137de 100644 --- a/src/components/settings/settings.scss +++ b/src/components/settings/settings.scss @@ -161,3 +161,7 @@ .stepper-modal .pgn__modal-header { border-bottom: solid 7px; } + +.warning-icon { + color: #F0CC00; +} From 6aadfed2fb1babdf6ffafc9ff196606433c109ed Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Mon, 7 Aug 2023 13:54:36 +0000 Subject: [PATCH 07/15] fix: modify modal --- .../RegenerateCredentialWarningModal.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx index 5e075512c4..02042a81fa 100644 --- a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx @@ -6,6 +6,7 @@ import { Warning } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; import { DataContext, ErrorContext, ShowSuccessToast } from './Context'; import LmsApiService from '../../../data/services/LmsApiService'; +import { API_CLIENT_DOCUMENTATION } from '../data/constants'; const RegenerateCredentialWarningModal = ({ modalSize, @@ -20,7 +21,8 @@ const RegenerateCredentialWarningModal = ({ const handleOnClickRegeneration = async () => { try { const response = await LmsApiService.regenerateAPICredentials(redirectURLs); - setData(response.data); + const data = { ...response.data, api_client_documentation: API_CLIENT_DOCUMENTATION }; + setData(data); setShowSuccessToast(true); setRedirectURIs(''); } catch (error) { From 1acddd930d6305c658bb437191bda009055e217f Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Mon, 14 Aug 2023 14:50:35 +0000 Subject: [PATCH 08/15] fix: modify lmsservice url --- .../SettingsApiCredentialsTab/Context.jsx | 1 + .../RegenerateCredentialWarningModal.jsx | 8 +- .../ZeroStateCard.jsx | 8 +- .../SettingsApiCredentialsTab/index.jsx | 73 ++++++++++--------- .../SettingsAPICredrentialsPage.test.jsx | 30 +++++--- .../settings/tests/SettingsTabs.test.jsx | 14 ++++ src/data/services/LmsApiService.js | 12 +-- 7 files changed, 90 insertions(+), 56 deletions(-) diff --git a/src/components/settings/SettingsApiCredentialsTab/Context.jsx b/src/components/settings/SettingsApiCredentialsTab/Context.jsx index 2b5a2f081b..e464b8eadd 100644 --- a/src/components/settings/SettingsApiCredentialsTab/Context.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/Context.jsx @@ -4,3 +4,4 @@ export const ZeroStateHandlerContext = createContext(null); export const ErrorContext = createContext(null); export const ShowSuccessToast = createContext(null); export const DataContext = createContext(null); +export const EnterpriseId = createContext(null); diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx index 02042a81fa..0a0f43682f 100644 --- a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx @@ -4,7 +4,10 @@ import { } from '@edx/paragon'; import { Warning } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; -import { DataContext, ErrorContext, ShowSuccessToast } from './Context'; +import { + DataContext, ErrorContext, + ShowSuccessToast, EnterpriseId, +} from './Context'; import LmsApiService from '../../../data/services/LmsApiService'; import { API_CLIENT_DOCUMENTATION } from '../data/constants'; @@ -18,9 +21,10 @@ const RegenerateCredentialWarningModal = ({ const [, setHasError] = useContext(ErrorContext); const [, setData] = useContext(DataContext); const [, setShowSuccessToast] = useContext(ShowSuccessToast); + const enterpriseId = useContext(EnterpriseId); const handleOnClickRegeneration = async () => { try { - const response = await LmsApiService.regenerateAPICredentials(redirectURLs); + const response = await LmsApiService.regenerateAPICredentials(redirectURLs, enterpriseId); const data = { ...response.data, api_client_documentation: API_CLIENT_DOCUMENTATION }; setData(data); setShowSuccessToast(true); diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index 721e7dff43..067b84429f 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -2,7 +2,9 @@ import { Card, Button, Icon } from '@edx/paragon'; import { Add, SpinnerSimple, Error } from '@edx/paragon/icons'; import React, { useState, useContext } from 'react'; import cardImage from '../../../data/images/ZeroState.svg'; -import { ZeroStateHandlerContext, ShowSuccessToast, DataContext } from './Context'; +import { + ZeroStateHandlerContext, ShowSuccessToast, DataContext, EnterpriseId, +} from './Context'; import LmsApiService from '../../../data/services/LmsApiService'; import { API_TERMS_OF_SERVICE } from '../data/constants'; @@ -12,11 +14,11 @@ const ZeroStateCard = () => { const [, setData] = useContext(DataContext); const [isLoading, setIsLoading] = useState(false); const [displayFailureAlert, setFailureAlert] = useState(false); - + const enterpriseId = useContext(EnterpriseId); const handleClick = async () => { setIsLoading(true); try { - const response = await LmsApiService.createNewAPICredentials(); + const response = await LmsApiService.createNewAPICredentials(enterpriseId); setData(response.data); setIsLoading(false); setZeroState(false); diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index bb00b67495..6a04b23b80 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -1,5 +1,6 @@ /* eslint-disable react/jsx-no-constructed-context-values */ import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; import { ActionRow, Toast } from '@edx/paragon'; import ZeroStateCard from './ZeroStateCard'; import APICredentialsPage from './APICredrentialsPage'; @@ -7,18 +8,20 @@ import FailedAlert from './FailedAlert'; import { HELP_CENTER_API_GUIDE, API_CLIENT_DOCUMENTATION } from '../data/constants'; import HelpCenterButton from './HelpCenterButton'; import { - ZeroStateHandlerContext, ErrorContext, ShowSuccessToast, DataContext, + ZeroStateHandlerContext, ErrorContext, ShowSuccessToast, DataContext, EnterpriseId, } from './Context'; import LmsApiService from '../../../data/services/LmsApiService'; -const SettingsApiCredentialsTab = () => { +const SettingsApiCredentialsTab = ({ + enterpriseId, +}) => { const [displayZeroState, setDisplayZeroState] = useState(false); const [isLoading, setIsLoading] = useState(true); const [existingData, setExistingData] = useState(null); const fetchExistingAPICredentials = async () => { setIsLoading(false); try { - const response = await LmsApiService.fetchAPICredentials(); + const response = await LmsApiService.fetchAPICredentials(enterpriseId); const { results } = response.data; const result = results[0]; setExistingData({ @@ -38,43 +41,47 @@ const SettingsApiCredentialsTab = () => { const [showToast, setShowToast] = useState(false); useEffect(() => { fetchExistingAPICredentials(); - }, []); + }); return ( - - - - - { hasRegenerationError && } - -

API credentials

- - - Help Center: EdX Enterprise API Guide - -
-
- {!isLoading + + + + + + { hasRegenerationError && } + +

API credentials

+ + + Help Center: EdX Enterprise API Guide + +
+
+ {!isLoading && ( !displayZeroState ? ( ) : () )} -
-
- { showToast && ( - setShowToast(false)} - show={showToast} - > - API credentials successfully generated - - )} - - - - +
+
+ { showToast && ( + setShowToast(false)} + show={showToast} + > + API credentials successfully generated + + )} + + + + + ); }; - +SettingsApiCredentialsTab.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; export default SettingsApiCredentialsTab; diff --git a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx index 6b9017d5cc..4604ad6d31 100644 --- a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx @@ -16,7 +16,7 @@ jest.mock('../../../../data/services/LmsApiService', () => ({ regenerateAPICredentials: jest.fn(), })); -const name = 'The Whinery Spirits Company'; +const name = "edx's Enterprise Credentials"; const clientId = 'y0TCvOEvvIs6ll95irirzCJ5EaF0RnSbBIIXuNJE'; const clientSecret = '1G896sVeT67jtjHO6FNd5qFqayZPIV7BtnW01P8zaAd4mDfmBVVVsUP33u'; const apiClientDocumentation = API_CLIENT_DOCUMENTATION; @@ -48,6 +48,12 @@ describe('API Credentials Tab', () => { jest.clearAllMocks(); }); + const basicProps = { + enterpriseId: 'test-enterprise-uuid', + }; + + const enterpriseId = 'test-enterprise-uuid'; + test('renders zero state page when having no api credentials', async () => { const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); @@ -56,7 +62,7 @@ describe('API Credentials Tab', () => { render( - + , ); expect(screen.getByText('API credentials')).toBeInTheDocument(); @@ -78,7 +84,7 @@ describe('API Credentials Tab', () => { mockFetchFn.mockResolvedValue({ data: returnResponse }); render( - + , ); @@ -101,7 +107,7 @@ describe('API Credentials Tab', () => { render( - + , ); @@ -130,7 +136,7 @@ describe('API Credentials Tab', () => { render( - + , ); @@ -173,7 +179,7 @@ describe('API Credentials Tab', () => { render( - + , ); @@ -194,7 +200,7 @@ describe('API Credentials Tab', () => { render( - + , ); await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); @@ -209,7 +215,7 @@ describe('API Credentials Tab', () => { const confirmedButton = screen.getByText('Regenerate'); userEvent.click(confirmedButton); await waitFor(() => { - expect(mockPatchFn).toHaveBeenCalledWith(redirectUris); + expect(mockPatchFn).toHaveBeenCalledWith(redirectUris, enterpriseId); }); expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); @@ -228,7 +234,7 @@ describe('API Credentials Tab', () => { render( - + , ); await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); @@ -243,7 +249,7 @@ describe('API Credentials Tab', () => { const confirmedButton = screen.getByText('Regenerate'); userEvent.click(confirmedButton); await waitFor(() => { - expect(mockPatchFn).toHaveBeenCalledWith(redirectUris); + expect(mockPatchFn).toHaveBeenCalledWith(redirectUris, enterpriseId); }); expect(screen.getByRole('heading', { name: 'Allowed URIs:' }).toBeInTheDocument); expect(screen.getByText('Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.')) @@ -257,7 +263,7 @@ describe('API Credentials Tab', () => { render( - + , ); @@ -273,7 +279,7 @@ describe('API Credentials Tab', () => { const cancelButton = screen.getByText('Cancel'); userEvent.click(cancelButton); await waitFor(() => { - expect(mockPatchFn).not.toHaveBeenCalledWith(redirectUris); + expect(mockPatchFn).not.toHaveBeenCalledWith(redirectUris, enterpriseId); }); expect(screen.getByRole('heading', { name: 'Allowed URIs:' }).toBeInTheDocument); }); diff --git a/src/components/settings/tests/SettingsTabs.test.jsx b/src/components/settings/tests/SettingsTabs.test.jsx index 4c91a6d47b..7dff9d0a67 100644 --- a/src/components/settings/tests/SettingsTabs.test.jsx +++ b/src/components/settings/tests/SettingsTabs.test.jsx @@ -20,6 +20,7 @@ import '@testing-library/jest-dom/extend-expect'; const ACCESS_MOCK_CONTENT = 'access'; const LMS_MOCK_CONTENT = 'lms'; const SSO_MOCK_CONTENT = 'sso'; +const API_CREDENTIALS_CONTENT = 'credentials'; jest.mock('../../../data/services/LmsApiService', () => ({ updateEnterpriseCustomerBranding: jest.fn(), @@ -46,6 +47,13 @@ jest.mock( }, ); +jest.mock( + '../SettingsApiCredentialsTab/', + () => function SettingsAccessTab() { + return
{API_CREDENTIALS_CONTENT}
; + }, +); + const enterpriseId = 'test-enterprise'; const initialStore = { portalConfiguration: { @@ -116,4 +124,10 @@ describe('', () => { await act(async () => { userEvent.click(accessTab); }); expect(screen.queryByText(ACCESS_MOCK_CONTENT)).toBeTruthy(); }); + + test('Api credentials tab is not rendered if FEATURE_API_CREDENTIALS_TAB = false', () => { + features.FEATURE_API_CREDENTIALS_TAB = false; + render(); + expect(screen.queryByText(API_CREDENTIALS_CONTENT)).not.toBeInTheDocument(); + }); }); diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js index 2af4d53804..6d8551ae83 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -332,19 +332,19 @@ class LmsApiService { return LmsApiService.apiClient().patch(url, formData); } - static fetchAPICredentials() { - return LmsApiService.apiClient().get(`${LmsApiService.apiCredentialsUrl}`); + static fetchAPICredentials(enterpriseUUID) { + return LmsApiService.apiClient().get(`${LmsApiService.apiCredentialsUrl}${enterpriseUUID}/`); } - static createNewAPICredentials() { - return LmsApiService.apiClient().post(`${LmsApiService.apiCredentialsUrl}`); + static createNewAPICredentials(enterpriseUUID) { + return LmsApiService.apiClient().post(`${LmsApiService.apiCredentialsUrl}${enterpriseUUID}/`); } - static regenerateAPICredentials(redirectURLs) { + static regenerateAPICredentials(redirectURLs, enterpriseUUID) { const requestData = { redirect_uris: redirectURLs, }; - return LmsApiService.apiClient().patch(`${LmsApiService.apiCredentialsUrl}regenerate_credentials/`, requestData); + return LmsApiService.apiClient().patch(`${LmsApiService.apiCredentialsUrl}${enterpriseUUID}/regenerate_credentials/`, requestData); } } From 6a3f4af3dfa61798a4c79aa3dc526220a8a06a5a Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Mon, 14 Aug 2023 17:54:39 +0000 Subject: [PATCH 09/15] fix: remove dependency in useffect --- src/components/settings/SettingsApiCredentialsTab/index.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index 6a04b23b80..06b4e8d9b3 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -41,7 +41,8 @@ const SettingsApiCredentialsTab = ({ const [showToast, setShowToast] = useState(false); useEffect(() => { fetchExistingAPICredentials(); - }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( From 96f5de133ede67cf6f4b4eecf0bc49c34f7ca443 Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Wed, 16 Aug 2023 17:58:54 +0000 Subject: [PATCH 10/15] fix: add api-document url --- .../settings/SettingsApiCredentialsTab/ZeroStateCard.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index 067b84429f..24f5fb14af 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -6,7 +6,7 @@ import { ZeroStateHandlerContext, ShowSuccessToast, DataContext, EnterpriseId, } from './Context'; import LmsApiService from '../../../data/services/LmsApiService'; -import { API_TERMS_OF_SERVICE } from '../data/constants'; +import { API_TERMS_OF_SERVICE, API_CLIENT_DOCUMENTATION } from '../data/constants'; const ZeroStateCard = () => { const [, setZeroState] = useContext(ZeroStateHandlerContext); @@ -19,7 +19,8 @@ const ZeroStateCard = () => { setIsLoading(true); try { const response = await LmsApiService.createNewAPICredentials(enterpriseId); - setData(response.data); + const data = { ...response.data, api_client_documentation: API_CLIENT_DOCUMENTATION }; + setData(data); setIsLoading(false); setZeroState(false); setShowToast(true); From ff0213c823f50defa7cb82f2df9e9e1d64872255 Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Fri, 25 Aug 2023 16:56:44 +0000 Subject: [PATCH 11/15] fix: lots of little fixes --- src/components/forms/FormWorkflow.tsx | 11 +++---- .../HelpCenterButton.jsx | 6 +--- ...entialsPage.jsx => APICredentialsPage.jsx} | 30 +++++++++---------- .../RegenerateCredentialWarningModal.jsx | 11 +++---- .../ZeroStateCard.jsx | 13 ++++---- .../SettingsApiCredentialsTab/index.jsx | 4 +-- .../SettingsAPICredrentialsPage.test.jsx | 4 +-- .../settings/SettingsLMSTab/index.jsx | 11 +++---- src/components/settings/SettingsTabs.jsx | 15 ---------- src/components/settings/settings.scss | 9 +++++- 10 files changed, 50 insertions(+), 64 deletions(-) rename src/components/settings/{SettingsApiCredentialsTab => }/HelpCenterButton.jsx (76%) rename src/components/settings/SettingsApiCredentialsTab/{APICredrentialsPage.jsx => APICredentialsPage.jsx} (72%) diff --git a/src/components/forms/FormWorkflow.tsx b/src/components/forms/FormWorkflow.tsx index e87c12a931..f6c2a9d6da 100644 --- a/src/components/forms/FormWorkflow.tsx +++ b/src/components/forms/FormWorkflow.tsx @@ -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'; @@ -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'; @@ -201,13 +202,9 @@ const FormWorkflow = ({ className="stepper-modal" footerNode={( - + Help Center: Integrations - + {nextButtonConfig && ( diff --git a/src/components/settings/SettingsApiCredentialsTab/HelpCenterButton.jsx b/src/components/settings/HelpCenterButton.jsx similarity index 76% rename from src/components/settings/SettingsApiCredentialsTab/HelpCenterButton.jsx rename to src/components/settings/HelpCenterButton.jsx index ea89801ae8..50b9749abb 100644 --- a/src/components/settings/SettingsApiCredentialsTab/HelpCenterButton.jsx +++ b/src/components/settings/HelpCenterButton.jsx @@ -1,10 +1,8 @@ import React from 'react'; import { Hyperlink } from '@edx/paragon'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; const HelpCenterButton = ({ - variant, url, children, ...rest @@ -15,7 +13,7 @@ const HelpCenterButton = ({ {children} @@ -25,12 +23,10 @@ const HelpCenterButton = ({ HelpCenterButton.defaultProps = { children: 'Help Center', - variant: 'outline-primary', }; HelpCenterButton.propTypes = { children: PropTypes.node, - variant: PropTypes.string, url: PropTypes.string, }; diff --git a/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx b/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx similarity index 72% rename from src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx rename to src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx index ec8b89c8cb..67c2cf19ac 100644 --- a/src/components/settings/SettingsApiCredentialsTab/APICredrentialsPage.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx @@ -20,29 +20,29 @@ const APICredentialsPage = () => { Copy and paste the following credential information and send it to your API developer(s).

-
+

- Application name:  - {data?.name} + Application name: + data?.name

- Allowed URIs:  - {data?.redirect_uris} + Allowed URIs: + data?.redirect_uris

- API client ID:  - {data?.client_id} + API client ID: + {data?.client_id}

- API client secret:  - {data?.client_secret} + API client secret: + {data?.client_secret}

-

API client documentation:  - {data?.api_client_documentation} +

API client documentation: + {data?.api_client_documentation}

- Last generated on:  - {data?.updated} + Last generated on: + {data?.updated}

@@ -61,10 +61,10 @@ const APICredentialsPage = () => { data-testid="form-control" />

- Allowed URI's list, space separated + Allowed URIs list, space separated

diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx index 0a0f43682f..6105d70aed 100644 --- a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx @@ -1,9 +1,10 @@ import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; import { - ActionRow, Button, ModalDialog, useToggle, Icon, + ActionRow, Button, Icon, ModalDialog, useToggle, } from '@edx/paragon'; import { Warning } from '@edx/paragon/icons'; -import PropTypes from 'prop-types'; + import { DataContext, ErrorContext, ShowSuccessToast, EnterpriseId, @@ -14,7 +15,7 @@ import { API_CLIENT_DOCUMENTATION } from '../data/constants'; const RegenerateCredentialWarningModal = ({ modalSize, modalVariant, - redirectURLs, + redirectURIs, setRedirectURIs, }) => { const [isOn, setOn, setOff] = useToggle(false); @@ -24,7 +25,7 @@ const RegenerateCredentialWarningModal = ({ const enterpriseId = useContext(EnterpriseId); const handleOnClickRegeneration = async () => { try { - const response = await LmsApiService.regenerateAPICredentials(redirectURLs, enterpriseId); + const response = await LmsApiService.regenerateAPICredentials(redirectURIs, enterpriseId); const data = { ...response.data, api_client_documentation: API_CLIENT_DOCUMENTATION }; setData(data); setShowSuccessToast(true); @@ -97,7 +98,7 @@ RegenerateCredentialWarningModal.defaultProps = { RegenerateCredentialWarningModal.propTypes = { modalSize: PropTypes.string, modalVariant: PropTypes.string, - redirectURLs: PropTypes.string.isRequired, + redirectURIs: PropTypes.string.isRequired, setRedirectURIs: PropTypes.func.isRequired, }; diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index 24f5fb14af..c13d404501 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -1,6 +1,9 @@ -import { Card, Button, Icon } from '@edx/paragon'; -import { Add, SpinnerSimple, Error } from '@edx/paragon/icons'; import React, { useState, useContext } from 'react'; +import { + Button, Card, Icon, Spinner, +} from '@edx/paragon'; +import { Add, Error } from '@edx/paragon/icons'; + import cardImage from '../../../data/images/ZeroState.svg'; import { ZeroStateHandlerContext, ShowSuccessToast, DataContext, EnterpriseId, @@ -26,7 +29,6 @@ const ZeroStateCard = () => { setShowToast(true); } catch (err) { setFailureAlert(true); - setIsLoading(false); setZeroState(true); setIsLoading(false); } @@ -40,7 +42,7 @@ const ZeroStateCard = () => { srcAlt="Card image" /> -

You don't hava API credentials yet.

+

You don't have API credentials yet.

{ !displayFailureAlert && (

This page allows you to generate API credentials to send to  @@ -68,10 +70,11 @@ const ZeroStateCard = () => { diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index 06b4e8d9b3..f99ed619e3 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -3,10 +3,10 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { ActionRow, Toast } from '@edx/paragon'; import ZeroStateCard from './ZeroStateCard'; -import APICredentialsPage from './APICredrentialsPage'; +import APICredentialsPage from './APICredentialsPage'; import FailedAlert from './FailedAlert'; import { HELP_CENTER_API_GUIDE, API_CLIENT_DOCUMENTATION } from '../data/constants'; -import HelpCenterButton from './HelpCenterButton'; +import HelpCenterButton from '../HelpCenterButton'; import { ZeroStateHandlerContext, ErrorContext, ShowSuccessToast, DataContext, EnterpriseId, } from './Context'; diff --git a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx index 4604ad6d31..b762b56af5 100644 --- a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredrentialsPage.test.jsx @@ -66,9 +66,9 @@ describe('API Credentials Tab', () => { , ); expect(screen.getByText('API credentials')).toBeInTheDocument(); - expect(screen.queryByText("You don't hava API credentials yet.")).toBeNull(); + expect(screen.queryByText("You don't have API credentials yet.")).toBeNull(); await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); - expect(screen.getByText("You don't hava API credentials yet.")).toBeInTheDocument(); + expect(screen.getByText("You don't have API credentials yet.")).toBeInTheDocument(); expect(screen.queryByText('Help Center: EdX Enterprise API Guide')).toBeInTheDocument(); const helpLink = screen.getByText('Help Center: EdX Enterprise API Guide'); expect(helpLink.getAttribute('href')).toBe(HELP_CENTER_API_GUIDE); diff --git a/src/components/settings/SettingsLMSTab/index.jsx b/src/components/settings/SettingsLMSTab/index.jsx index 54c3c3fa1d..1da30d6a09 100644 --- a/src/components/settings/SettingsLMSTab/index.jsx +++ b/src/components/settings/SettingsLMSTab/index.jsx @@ -7,11 +7,12 @@ import PropTypes from 'prop-types'; import { camelCaseObject } from '@edx/frontend-platform/utils'; import { - Alert, Button, Hyperlink, Toast, Skeleton, useToggle, + Alert, Button, Toast, Skeleton, useToggle, } from '@edx/paragon'; import { Add, Info } from '@edx/paragon/icons'; import { logError } from '@edx/frontend-platform/logging'; +import HelpCenterButton from '../HelpCenterButton'; import { camelCaseDictArray, getChannelMap } from '../../../utils'; import LMSConfigPage from './LMSConfigPage'; import ExistingLMSCardDeck from './ExistingLMSCardDeck'; @@ -149,13 +150,9 @@ const SettingsLMSTab = ({ return (

Learning Platform Integrations - + Help Center: Integrations - +
{!configsLoading && !config && (

API client documentation: - {data?.api_client_documentation} + {API_CLIENT_DOCUMENTATION}

Last generated on: {data?.updated}

- +
@@ -64,7 +64,8 @@ const APICredentialsPage = ({ data }) => {

@@ -94,6 +95,7 @@ APICredentialsPage.propTypes = { api_client_documentation: PropTypes.string, updated: PropTypes.bool, }), + setData: PropTypes.func.isRequired, }; export default APICredentialsPage; diff --git a/src/components/settings/SettingsApiCredentialsTab/Context.jsx b/src/components/settings/SettingsApiCredentialsTab/Context.jsx index e464b8eadd..4682f99ef7 100644 --- a/src/components/settings/SettingsApiCredentialsTab/Context.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/Context.jsx @@ -1,7 +1,5 @@ import { createContext } from 'react'; -export const ZeroStateHandlerContext = createContext(null); export const ErrorContext = createContext(null); export const ShowSuccessToast = createContext(null); -export const DataContext = createContext(null); export const EnterpriseId = createContext(null); diff --git a/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx b/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx similarity index 70% rename from src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx rename to src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx index 78e8d9309c..c15351d621 100644 --- a/src/components/settings/SettingsApiCredentialsTab/CopiedButton.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx @@ -1,12 +1,12 @@ -import React, { useContext, useState } from 'react'; +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 { DataContext } from './Context'; -const CopiedButton = () => { +const CopyButton = ({ data }) => { const [isCopyLinkToastOpen, setIsCopyLinkToastOpen] = useState(false); - const [data] = useContext(DataContext); const [copiedError, setCopiedError] = useState(false); const handleCopyLink = async () => { @@ -40,4 +40,15 @@ const CopiedButton = () => { ); }; -export default CopiedButton; +CopyButton.propTypes = { + data: PropTypes.shape({ + name: PropTypes.string, + redirect_uris: PropTypes.string, + client_id: PropTypes.string, + client_secret: PropTypes.string, + api_client_documentation: PropTypes.string, + updated: PropTypes.bool, + }), +}; + +export default CopyButton; diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx index 6105d70aed..ab2b535ec1 100644 --- a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx @@ -6,36 +6,35 @@ import { import { Warning } from '@edx/paragon/icons'; import { - DataContext, ErrorContext, + ErrorContext, ShowSuccessToast, EnterpriseId, } from './Context'; import LmsApiService from '../../../data/services/LmsApiService'; -import { API_CLIENT_DOCUMENTATION } from '../data/constants'; const RegenerateCredentialWarningModal = ({ - modalSize, - modalVariant, redirectURIs, - setRedirectURIs, + data, + setData, }) => { const [isOn, setOn, setOff] = useToggle(false); const [, setHasError] = useContext(ErrorContext); - const [, setData] = useContext(DataContext); const [, setShowSuccessToast] = useContext(ShowSuccessToast); const enterpriseId = useContext(EnterpriseId); const handleOnClickRegeneration = async () => { try { const response = await LmsApiService.regenerateAPICredentials(redirectURIs, enterpriseId); - const data = { ...response.data, api_client_documentation: API_CLIENT_DOCUMENTATION }; - setData(data); + const newURIs = response.data.redirect_uris; setShowSuccessToast(true); - setRedirectURIs(''); + const updatedData = data; + updatedData.redirect_uris = newURIs; + setData(updatedData); } catch (error) { setHasError(true); } finally { setOff(true); } }; + return ( <>