Skip to content

Commit 9ddfb37

Browse files
authored
Merge pull request #397 from openedx/kiram15/ENT-9164
Adding integrations to the Customer view page
2 parents 978a690 + 97c718d commit 9ddfb37

File tree

8 files changed

+227
-2
lines changed

8 files changed

+227
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"build": "fedx-scripts webpack",
1414
"i18n_extract": "fedx-scripts formatjs extract",
1515
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
16+
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
1617
"start": "fedx-scripts webpack-dev-server --progress",
1718
"debug-test": "node --inspect-brk node_modules/.bin/jest --coverage --runInBand",
1819
"test": "TZ=UTC fedx-scripts jest --coverage --maxWorkers=2",

src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import {
1010
import { Launch, ContentCopy } from '@openedx/paragon/icons';
1111
import { getConfig } from '@edx/frontend-platform';
1212
import { formatDate, useCopyToClipboard } from '../data/utils';
13+
import DJANGO_ADMIN_BASE_URL from '../data/constants';
1314

1415
const CustomerCard = ({ enterpriseCustomer }) => {
15-
const { ADMIN_PORTAL_BASE_URL, LMS_BASE_URL } = getConfig();
16+
const { ADMIN_PORTAL_BASE_URL } = getConfig();
1617
const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();
1718

1819
return (
@@ -25,7 +26,7 @@ const CustomerCard = ({ enterpriseCustomer }) => {
2526
<Button
2627
className="text-dark-500"
2728
as="a"
28-
href={`${LMS_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
29+
href={`${DJANGO_ADMIN_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
2930
variant="inverse-primary"
3031
target="_blank"
3132
rel="noopener noreferrer"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Container } from '@openedx/paragon';
2+
import { getConfig } from '@edx/frontend-platform';
3+
import PropTypes from 'prop-types';
4+
5+
import CustomerViewCard from './CustomerViewCard';
6+
import { formatDate } from '../data/utils';
7+
import DJANGO_ADMIN_BASE_URL from '../data/constants';
8+
9+
const CustomerIntegrations = ({
10+
slug, activeIntegrations, activeSSO, apiCredentialsEnabled,
11+
}) => {
12+
const { ADMIN_PORTAL_BASE_URL } = getConfig();
13+
const ssoDateText = ({ sso }) => (`Created ${formatDate(sso?.created)} • Last modified ${formatDate(sso?.modifed)}`);
14+
const configDateText = ({ config }) => (`Created ${formatDate(config?.created)} • Last modified ${formatDate(config?.lastModifiedAt)}`);
15+
16+
return (
17+
<Container className="mt-3 pr-6 mb-5">
18+
{(activeSSO || activeIntegrations || apiCredentialsEnabled) && (
19+
<div>
20+
<h2 className="pt-4">Associated Integrations</h2>
21+
{activeSSO && activeSSO.map((sso) => (
22+
<CustomerViewCard
23+
slug={slug}
24+
header="SSO"
25+
title={sso.displayName}
26+
subtext={ssoDateText(sso)}
27+
buttonText="Open in Admin Portal"
28+
buttonLink={`${ADMIN_PORTAL_BASE_URL}/${slug}/admin/settings/sso`}
29+
/>
30+
))}
31+
{activeIntegrations && activeIntegrations.map((config) => (
32+
<CustomerViewCard
33+
slug={slug}
34+
header="Learning platform"
35+
title={config.channelCode[0].toUpperCase() + config.channelCode.substr(1).toLowerCase()}
36+
subtext={configDateText(config)}
37+
buttonText="Open in Admin Portal"
38+
buttonLink={`${ADMIN_PORTAL_BASE_URL}/${slug}/admin/settings/lms`}
39+
/>
40+
))}
41+
{apiCredentialsEnabled && (
42+
<CustomerViewCard
43+
slug={slug}
44+
header="Integration"
45+
title="API"
46+
buttonText="Open in Django"
47+
buttonLink={`${DJANGO_ADMIN_BASE_URL}/admin/enterprise/enterprisecustomerinvitekey/`}
48+
/>
49+
)}
50+
</div>
51+
)}
52+
</Container>
53+
);
54+
};
55+
56+
CustomerIntegrations.defaultProps = {
57+
slug: null,
58+
activeIntegrations: null,
59+
activeSSO: null,
60+
apiCredentialsEnabled: false,
61+
};
62+
63+
CustomerIntegrations.propTypes = {
64+
slug: PropTypes.string,
65+
activeIntegrations: PropTypes.arrayOf(
66+
PropTypes.shape({
67+
channelCode: PropTypes.string,
68+
lastModifiedAt: PropTypes.string,
69+
}),
70+
),
71+
activeSSO: PropTypes.arrayOf(
72+
PropTypes.shape({
73+
created: PropTypes.string,
74+
modified: PropTypes.string,
75+
displayName: PropTypes.string,
76+
}),
77+
),
78+
apiCredentialsEnabled: PropTypes.bool,
79+
};
80+
81+
export default CustomerIntegrations;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
Button, Card, Hyperlink,
3+
} from '@openedx/paragon';
4+
import PropTypes from 'prop-types';
5+
6+
const CustomerViewCard = (
7+
{
8+
header, title, subtext, buttonText, buttonLink,
9+
},
10+
) => (
11+
<Card className="mt-4">
12+
<Card.Section className="pb-0">
13+
<h6 className="mb-0">{header.toUpperCase()}</h6>
14+
<h3 className="mt-0">{title}</h3>
15+
</Card.Section>
16+
<Card.Section className="pt-0 x-small text-gray-400">
17+
{subtext && <div>{subtext}</div>}
18+
</Card.Section>
19+
<Card.Footer>
20+
<Button>
21+
<Hyperlink
22+
destination={buttonLink}
23+
rel="noopener noreferrer"
24+
target="_blank"
25+
className="text-white"
26+
showLaunchIcon
27+
>
28+
{buttonText}
29+
</Hyperlink>
30+
</Button>
31+
</Card.Footer>
32+
</Card>
33+
);
34+
35+
CustomerViewCard.propTypes = {
36+
header: PropTypes.string.isRequired,
37+
title: PropTypes.string.isRequired,
38+
subtext: PropTypes.string.isRequired,
39+
buttonText: PropTypes.string.isRequired,
40+
buttonLink: PropTypes.string.isRequired,
41+
};
42+
43+
export default CustomerViewCard;

src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { useIntl } from '@edx/frontend-platform/i18n';
1111
import CustomerCard from './CustomerCard';
1212
import { getEnterpriseCustomer } from '../data/utils';
13+
import CustomerIntegrations from './CustomerIntegrations';
1314

1415
const CustomerViewContainer = () => {
1516
const { id } = useParams();
@@ -58,6 +59,12 @@ const CustomerViewContainer = () => {
5859
<Container className="mt-4">
5960
<Stack gap={2}>
6061
{!isLoading ? <CustomerCard enterpriseCustomer={enterpriseCustomer} /> : <Skeleton height={230} />}
62+
<CustomerIntegrations
63+
slug={enterpriseCustomer.slug}
64+
activeIntegrations={enterpriseCustomer.activeIntegrations}
65+
activeSSO={enterpriseCustomer.activeSsoConfigurations}
66+
apiCredentialsEnabled={enterpriseCustomer.enableGenerationOfApiCredentials}
67+
/>
6168
</Stack>
6269
</Container>
6370
</div>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable react/prop-types */
2+
import { screen, render, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
5+
import { IntlProvider } from '@edx/frontend-platform/i18n';
6+
import { formatDate } from '../../data/utils';
7+
import CustomerIntegrations from '../CustomerIntegrations';
8+
9+
jest.mock('react-router-dom', () => ({
10+
...jest.requireActual('react-router-dom'),
11+
useParams: () => ({ id: 'test-id' }),
12+
}));
13+
14+
jest.mock('../../data/utils', () => ({
15+
formatDate: jest.fn(),
16+
}));
17+
18+
const mockSSOData = [{
19+
enterpriseCustomer: '18005882300',
20+
created: '2024-09-15T11:01:04.501365Z',
21+
modified: '2024-09-15T11:01:04.501365Z',
22+
displayName: 'Orange cats rule',
23+
}];
24+
25+
const mockIntegratedChannelData = [
26+
{
27+
channelCode: 'MOODLE',
28+
enterpriseCustomer: '18005882300',
29+
lastModifiedAt: '2024-09-15T11:01:04.501365Z',
30+
},
31+
{
32+
channelCode: 'CANVAS',
33+
enterpriseCustomer: '18005882300',
34+
lastModifiedAt: '2024-09-15T11:01:04.501365Z',
35+
},
36+
];
37+
38+
describe('CustomerViewIntegrations', () => {
39+
it('renders cards', async () => {
40+
formatDate.mockReturnValue('September 15, 2024');
41+
render(
42+
<IntlProvider locale="en">
43+
<CustomerIntegrations
44+
slug="marcel-the-shell"
45+
activeIntegrations={mockIntegratedChannelData}
46+
activeSSO={mockSSOData}
47+
apiCredentialsEnabled
48+
/>
49+
</IntlProvider>,
50+
);
51+
await waitFor(() => {
52+
expect(screen.getByText('Associated Integrations')).toBeInTheDocument();
53+
54+
expect(screen.getByText('SSO')).toBeInTheDocument();
55+
expect(screen.getByText('Orange cats rule')).toBeInTheDocument();
56+
expect(screen.getAllByText('Created September 15, 2024 • Last modified September 15, 2024')).toHaveLength(3);
57+
expect(screen.getAllByText('Open in Admin Portal')).toHaveLength(3);
58+
59+
expect(screen.getAllByText('LEARNING PLATFORM')).toHaveLength(2);
60+
expect(screen.getByText('Moodle')).toBeInTheDocument();
61+
expect(screen.getByText('Canvas')).toBeInTheDocument();
62+
63+
expect(screen.getByText('INTEGRATION')).toBeInTheDocument();
64+
expect(screen.getByText('API')).toBeInTheDocument();
65+
});
66+
});
67+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const DJANGO_ADMIN_BASE_URL = 'https://internal.courses.edx.org';
2+
3+
export default DJANGO_ADMIN_BASE_URL;

src/data/services/EnterpriseApiService.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ class LmsApiService {
1818

1919
static enterpriseCatalogsUrl = `${LmsApiService.enterpriseAPIBaseUrl}enterprise_catalogs/`;
2020

21+
static enterpriseSSOConfigurations = `${LmsApiService.enterpriseAPIBaseUrl}enterprise_customer_sso_configuration/`;
22+
23+
static integratedChannelsUrl = `${LmsApiService.baseUrl}/integrated_channels/api/v1/configs/`;
24+
2125
static fetchEnterpriseCatalogQueries = () => LmsApiService.apiClient().get(LmsApiService.enterpriseCatalogQueriesUrl);
2226

2327
static fetchEnterpriseCustomersBasicList = (enterpriseNameOrUuid) => LmsApiService.apiClient().get(`${LmsApiService.enterpriseCustomersBasicListUrl}${enterpriseNameOrUuid !== undefined ? `?name_or_uuid=${enterpriseNameOrUuid}` : ''}`);
@@ -121,6 +125,24 @@ class LmsApiService {
121125
static fetchSubsidyAccessPolicies = async (enterpriseCustomerUuid) => LmsApiService.apiClient().get(
122126
`${getConfig().ENTERPRISE_ACCESS_BASE_URL}/api/v1/subsidy-access-policies/?enterprise_customer_uuid=${enterpriseCustomerUuid}`,
123127
);
128+
129+
static fetchEnterpriseCustomerSSOConfigs = (options) => {
130+
const queryParams = new URLSearchParams({
131+
...options,
132+
});
133+
return LmsApiService.apiClient().get(
134+
`${LmsApiService.enterpriseSSOConfigurations}?${queryParams.toString()}`,
135+
);
136+
};
137+
138+
static fetchIntegratedChannels = (options) => {
139+
const queryParams = new URLSearchParams({
140+
...options,
141+
});
142+
return LmsApiService.apiClient().get(
143+
`${LmsApiService.integratedChannelsUrl}?${queryParams.toString()}`,
144+
);
145+
};
124146
}
125147

126148
export default LmsApiService;

0 commit comments

Comments
 (0)