diff --git a/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx b/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx
index 688ca79c6..6224f86ce 100644
--- a/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx
+++ b/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx
@@ -1,4 +1,3 @@
-import { useState } from 'react';
import {
Hyperlink,
Icon,
@@ -8,22 +7,19 @@ import { getConfig } from '@edx/frontend-platform';
import { Check, ContentCopy } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import ROUTES from '../../../data/constants/routes';
+import { useCopyToClipboard } from '../data/utils';
const { HOME } = ROUTES.CONFIGURATION.SUB_DIRECTORY.CUSTOMERS;
export const CustomerDetailLink = ({ row }) => {
- const [showToast, setShowToast] = useState(false);
- const copyToClipboard = (id) => {
- navigator.clipboard.writeText(id);
- setShowToast(true);
- };
+ const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();
const { ADMIN_PORTAL_BASE_URL } = getConfig();
return (
({
getConfig: jest.fn(),
}));
-Object.assign(navigator, {
- clipboard: {
- writeText: () => {},
- },
-});
+jest.mock('../../data/utils', () => ({
+ useCopyToClipboard: jest.fn(() => ({
+ showToast: true,
+ copyToClipboard: jest.fn(),
+ setShowToast: jest.fn(),
+ })),
+}));
describe('CustomerDetails', () => {
const row = {
@@ -107,7 +109,7 @@ describe('CustomerDetails', () => {
,
);
- expect(screen.getByRole('link', { name: 'Ash Ketchum' })).toHaveAttribute('href', '/enterprise-configuration/customers/ash-ketchum/view');
+ expect(screen.getByRole('link', { name: 'Ash Ketchum' })).toHaveAttribute('href', '/enterprise-configuration/customers/123456789/view');
expect(screen.getByRole('link', { name: '/ash-ketchum/ in a new tab' })).toHaveAttribute('href', 'http://www.testportal.com/ash-ketchum/admin/learners');
expect(screen.getByText('123456789')).toBeInTheDocument();
const copy = screen.getByTestId('copy');
diff --git a/src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx b/src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx
new file mode 100644
index 000000000..cf3df68f3
--- /dev/null
+++ b/src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx
@@ -0,0 +1,95 @@
+import PropTypes from 'prop-types';
+import {
+ ActionRow,
+ Button,
+ Card,
+ Icon,
+ Hyperlink,
+ Toast,
+} from '@openedx/paragon';
+import { Launch, ContentCopy } from '@openedx/paragon/icons';
+import { getConfig } from '@edx/frontend-platform';
+import { formatDate, useCopyToClipboard } from '../data/utils';
+
+const CustomerCard = ({ enterpriseCustomer }) => {
+ const { ADMIN_PORTAL_BASE_URL, LMS_BASE_URL } = getConfig();
+ const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();
+
+ return (
+
+
+
+
+
+
+ )}
+ >
+
+ CUSTOMER RECORD
+
+
+ {enterpriseCustomer.name}
+
+
+ /{enterpriseCustomer.slug}/
+
+
+
+ {enterpriseCustomer.uuid}
+
+
copyToClipboard(enterpriseCustomer.uuid)}
+ />
+
+
+ Created {formatDate(enterpriseCustomer.created)} • Last modified {formatDate(enterpriseCustomer.modified)}
+
+
+
+
setShowToast(false)}
+ show={showToast}
+ delay={2000}
+ >
+ Copied to clipboard
+
+
+
+ );
+};
+
+CustomerCard.propTypes = {
+ enterpriseCustomer: PropTypes.shape({
+ created: PropTypes.string,
+ modified: PropTypes.string,
+ slug: PropTypes.string,
+ name: PropTypes.string,
+ uuid: PropTypes.string,
+ }).isRequired,
+};
+
+export default CustomerCard;
diff --git a/src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx b/src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx
new file mode 100644
index 000000000..ee4fc809c
--- /dev/null
+++ b/src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx
@@ -0,0 +1,67 @@
+import { useState, useEffect, useCallback } from 'react';
+import { useParams } from 'react-router-dom';
+import { logError } from '@edx/frontend-platform/logging';
+import {
+ Breadcrumb,
+ Container,
+ Skeleton,
+ Stack,
+} from '@openedx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import CustomerCard from './CustomerCard';
+import { getEnterpriseCustomer } from '../data/utils';
+
+const CustomerViewContainer = () => {
+ const { id } = useParams();
+ const [enterpriseCustomer, setEnterpriseCustomer] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const intl = useIntl();
+
+ const fetchData = useCallback(
+ async () => {
+ try {
+ const response = await getEnterpriseCustomer({ uuid: id });
+ setEnterpriseCustomer(response[0]);
+ } catch (error) {
+ logError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [],
+ );
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ return (
+
+ {!isLoading ? (
+
+
+
+ ) : }
+
+
+ {!isLoading ? : }
+
+
+
+ );
+};
+
+export default CustomerViewContainer;
diff --git a/src/Configuration/Customers/CustomerDetailView/tests/CustomerCard.test.jsx b/src/Configuration/Customers/CustomerDetailView/tests/CustomerCard.test.jsx
new file mode 100644
index 000000000..82dbeceea
--- /dev/null
+++ b/src/Configuration/Customers/CustomerDetailView/tests/CustomerCard.test.jsx
@@ -0,0 +1,40 @@
+/* eslint-disable react/prop-types */
+import { screen, render } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { formatDate } from '../../data/utils';
+import CustomerCard from '../CustomerCard';
+
+jest.mock('../../data/utils', () => ({
+ getEnterpriseCustomer: jest.fn(),
+ formatDate: jest.fn(),
+ useCopyToClipboard: jest.fn(() => ({
+ showToast: true,
+ copyToClipboard: jest.fn(),
+ setShowToast: jest.fn(),
+ })),
+}));
+
+const mockData = {
+ uuid: 'test-id',
+ name: 'Test Customer Name',
+ slug: 'customer-6',
+ created: '2024-07-23T20:02:57.651943Z',
+ modified: '2024-07-23T20:02:57.651943Z',
+};
+
+describe('CustomerCard', () => {
+ it('renders customer card data', () => {
+ formatDate.mockReturnValue('July 23, 2024');
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText('test-id')).toBeInTheDocument();
+ expect(screen.getByText('/customer-6/')).toBeInTheDocument();
+ expect(screen.getByText('Created July 23, 2024 • Last modified July 23, 2024')).toBeInTheDocument();
+ expect(screen.getByText('Test Customer Name'));
+ });
+});
diff --git a/src/Configuration/Customers/CustomerDetailView/tests/CustomerViewContainer.test.jsx b/src/Configuration/Customers/CustomerDetailView/tests/CustomerViewContainer.test.jsx
new file mode 100644
index 000000000..23f7a360b
--- /dev/null
+++ b/src/Configuration/Customers/CustomerDetailView/tests/CustomerViewContainer.test.jsx
@@ -0,0 +1,49 @@
+/* eslint-disable react/prop-types */
+import { screen, render, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { getEnterpriseCustomer, formatDate } from '../../data/utils';
+import CustomerViewContainer from '../CustomerViewContainer';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({ id: 'test-id' }),
+}));
+
+jest.mock('../../data/utils', () => ({
+ getEnterpriseCustomer: jest.fn(),
+ formatDate: jest.fn(),
+ useCopyToClipboard: jest.fn(() => ({
+ showToast: true,
+ copyToClipboard: jest.fn(),
+ setShowToast: jest.fn(),
+ })),
+}));
+
+describe('CustomerViewContainer', () => {
+ it('renders data', async () => {
+ getEnterpriseCustomer.mockReturnValue([{
+ uuid: 'test-id',
+ name: 'Test Customer Name',
+ slug: 'customer-6',
+ created: '2024-07-23T20:02:57.651943Z',
+ modified: '2024-07-23T20:02:57.651943Z',
+ }]);
+ formatDate.mockReturnValue('July 23, 2024');
+ render(
+
+
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.getByText('test-id')).toBeInTheDocument();
+ expect(screen.getByText('/customer-6/')).toBeInTheDocument();
+ expect(screen.getByText('Created July 23, 2024 • Last modified July 23, 2024')).toBeInTheDocument();
+ const customerNameText = screen.getAllByText('Test Customer Name');
+ customerNameText.forEach(customerName => {
+ expect(customerName).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/src/Configuration/Customers/data/utils.js b/src/Configuration/Customers/data/utils.js
index 9252e9864..0ff8fe380 100644
--- a/src/Configuration/Customers/data/utils.js
+++ b/src/Configuration/Customers/data/utils.js
@@ -1,7 +1,10 @@
+import { useState } from 'react';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import EcommerceApiService from '../../../data/services/EcommerceApiService';
import LicenseManagerApiService from '../../../data/services/LicenseManagerApiService';
import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService';
+import LmsApiService from '../../../data/services/EnterpriseApiService';
+import dayjs from '../../Provisioning/data/dayjs';
export const getEnterpriseOffers = async (enterpriseId) => {
const response = await EcommerceApiService.fetchEnterpriseOffers(enterpriseId);
@@ -26,3 +29,24 @@ export const getSubsidyAccessPolicies = async (enterpriseId) => {
const subsidyAccessPolicies = camelCaseObject(response.data);
return subsidyAccessPolicies;
};
+
+export const getEnterpriseCustomer = async (options) => {
+ const response = await LmsApiService.fetchEnterpriseCustomerSupportTool(options);
+ const enterpriseCustomer = camelCaseObject(response.data);
+ return enterpriseCustomer;
+};
+
+export const formatDate = (date) => dayjs(date).utc().format('MMMM DD, YYYY');
+
+export const useCopyToClipboard = (id) => {
+ const [showToast, setShowToast] = useState(false);
+ const copyToClipboard = () => {
+ navigator.clipboard.writeText(id);
+ setShowToast(true);
+ };
+ return {
+ showToast,
+ copyToClipboard,
+ setShowToast,
+ };
+};
diff --git a/src/Configuration/Customers/data/utils.test.js b/src/Configuration/Customers/data/utils.test.js
index f5bcfe4b5..3ba7998f4 100644
--- a/src/Configuration/Customers/data/utils.test.js
+++ b/src/Configuration/Customers/data/utils.test.js
@@ -4,6 +4,8 @@ import {
getCouponOrders,
getCustomerAgreements,
getSubsidyAccessPolicies,
+ getEnterpriseCustomer,
+ formatDate,
} from './utils';
jest.mock('@edx/frontend-platform/auth', () => ({
@@ -100,3 +102,30 @@ describe('getCustomerAgreements', () => {
expect(results).toEqual(agreementsResults.data);
});
});
+
+describe('getEnterpriseCustomer', () => {
+ it('returns the correct data', async () => {
+ const enterpriseCustomer = {
+ data: [{
+ uuid: '0b466242-75ff-4c27-8237-680dac3737f7',
+ name: 'customer-6',
+ slug: 'customer-6',
+ active: true,
+ }],
+ };
+ getAuthenticatedHttpClient.mockImplementation(() => ({
+ get: jest.fn().mockResolvedValue(enterpriseCustomer),
+ }));
+ const results = await getEnterpriseCustomer(TEST_ENTERPRISE_UUID);
+ expect(results).toEqual(enterpriseCustomer.data);
+ });
+});
+
+describe('formatDate', () => {
+ it('returns the formatted date', async () => {
+ const date = '2024-07-23T20:02:57.651943Z';
+ const formattedDate = formatDate(date);
+ const expectedFormattedDate = 'July 23, 2024';
+ expect(expectedFormattedDate).toEqual(formattedDate);
+ });
+});
diff --git a/src/Configuration/index.scss b/src/Configuration/index.scss
index e7ede0f54..c3780b47c 100644
--- a/src/Configuration/index.scss
+++ b/src/Configuration/index.scss
@@ -7,7 +7,6 @@
.pgn__icon {
align-self: center;
margin-left: 8px;
- opacity: .3;
width: 1rem;
height: 1rem;
}
@@ -25,4 +24,4 @@
font-size: 15px;
color: black !important;
font-weight: bold;
-}
\ No newline at end of file
+}
diff --git a/src/index.jsx b/src/index.jsx
index 1bed2bec7..007efb559 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -29,6 +29,7 @@ import ProvisioningFormContainer from './Configuration/Provisioning/Provisioning
import SubsidyDetailViewContainer from './Configuration/Provisioning/SubsidyDetailView/SubsidyDetailViewContainer';
import ErrorPageContainer from './Configuration/Provisioning/ErrorPage';
import SubsidyEditViewContainer from './Configuration/Provisioning/SubsidyEditView/SubsidyEditViewContainer';
+import CustomerViewContainer from './Configuration/Customers/CustomerDetailView/CustomerViewContainer';
const { CONFIGURATION, SUPPORT_TOOLS_TABS } = ROUTES;
@@ -76,6 +77,11 @@ subscribe(APP_READY, () => {
path={CONFIGURATION.SUB_DIRECTORY.CUSTOMERS.HOME}
element={}
/>,
+ }
+ />,
];
ReactDOM.render(