Skip to content

Commit

Permalink
Merge pull request #397 from openedx/kiram15/ENT-9164
Browse files Browse the repository at this point in the history
Adding integrations to the Customer view page
  • Loading branch information
kiram15 authored Aug 15, 2024
2 parents 978a690 + 97c718d commit 9ddfb37
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"start": "fedx-scripts webpack-dev-server --progress",
"debug-test": "node --inspect-brk node_modules/.bin/jest --coverage --runInBand",
"test": "TZ=UTC fedx-scripts jest --coverage --maxWorkers=2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
import { Launch, ContentCopy } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import { formatDate, useCopyToClipboard } from '../data/utils';
import DJANGO_ADMIN_BASE_URL from '../data/constants';

const CustomerCard = ({ enterpriseCustomer }) => {
const { ADMIN_PORTAL_BASE_URL, LMS_BASE_URL } = getConfig();
const { ADMIN_PORTAL_BASE_URL } = getConfig();
const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();

return (
Expand All @@ -25,7 +26,7 @@ const CustomerCard = ({ enterpriseCustomer }) => {
<Button
className="text-dark-500"
as="a"
href={`${LMS_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
href={`${DJANGO_ADMIN_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
variant="inverse-primary"
target="_blank"
rel="noopener noreferrer"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Container } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';

import CustomerViewCard from './CustomerViewCard';
import { formatDate } from '../data/utils';
import DJANGO_ADMIN_BASE_URL from '../data/constants';

const CustomerIntegrations = ({
slug, activeIntegrations, activeSSO, apiCredentialsEnabled,
}) => {
const { ADMIN_PORTAL_BASE_URL } = getConfig();
const ssoDateText = ({ sso }) => (`Created ${formatDate(sso?.created)} • Last modified ${formatDate(sso?.modifed)}`);
const configDateText = ({ config }) => (`Created ${formatDate(config?.created)} • Last modified ${formatDate(config?.lastModifiedAt)}`);

return (
<Container className="mt-3 pr-6 mb-5">
{(activeSSO || activeIntegrations || apiCredentialsEnabled) && (
<div>
<h2 className="pt-4">Associated Integrations</h2>
{activeSSO && activeSSO.map((sso) => (
<CustomerViewCard
slug={slug}
header="SSO"
title={sso.displayName}
subtext={ssoDateText(sso)}
buttonText="Open in Admin Portal"
buttonLink={`${ADMIN_PORTAL_BASE_URL}/${slug}/admin/settings/sso`}
/>
))}
{activeIntegrations && activeIntegrations.map((config) => (
<CustomerViewCard
slug={slug}
header="Learning platform"
title={config.channelCode[0].toUpperCase() + config.channelCode.substr(1).toLowerCase()}
subtext={configDateText(config)}
buttonText="Open in Admin Portal"
buttonLink={`${ADMIN_PORTAL_BASE_URL}/${slug}/admin/settings/lms`}
/>
))}
{apiCredentialsEnabled && (
<CustomerViewCard
slug={slug}
header="Integration"
title="API"
buttonText="Open in Django"
buttonLink={`${DJANGO_ADMIN_BASE_URL}/admin/enterprise/enterprisecustomerinvitekey/`}
/>
)}
</div>
)}
</Container>
);
};

CustomerIntegrations.defaultProps = {
slug: null,
activeIntegrations: null,
activeSSO: null,
apiCredentialsEnabled: false,
};

CustomerIntegrations.propTypes = {
slug: PropTypes.string,
activeIntegrations: PropTypes.arrayOf(
PropTypes.shape({
channelCode: PropTypes.string,
lastModifiedAt: PropTypes.string,
}),
),
activeSSO: PropTypes.arrayOf(
PropTypes.shape({
created: PropTypes.string,
modified: PropTypes.string,
displayName: PropTypes.string,
}),
),
apiCredentialsEnabled: PropTypes.bool,
};

export default CustomerIntegrations;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
Button, Card, Hyperlink,
} from '@openedx/paragon';
import PropTypes from 'prop-types';

const CustomerViewCard = (
{
header, title, subtext, buttonText, buttonLink,
},
) => (
<Card className="mt-4">
<Card.Section className="pb-0">
<h6 className="mb-0">{header.toUpperCase()}</h6>
<h3 className="mt-0">{title}</h3>
</Card.Section>
<Card.Section className="pt-0 x-small text-gray-400">
{subtext && <div>{subtext}</div>}
</Card.Section>
<Card.Footer>
<Button>
<Hyperlink
destination={buttonLink}
rel="noopener noreferrer"
target="_blank"
className="text-white"
showLaunchIcon
>
{buttonText}
</Hyperlink>
</Button>
</Card.Footer>
</Card>
);

CustomerViewCard.propTypes = {
header: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
subtext: PropTypes.string.isRequired,
buttonText: PropTypes.string.isRequired,
buttonLink: PropTypes.string.isRequired,
};

export default CustomerViewCard;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { useIntl } from '@edx/frontend-platform/i18n';
import CustomerCard from './CustomerCard';
import { getEnterpriseCustomer } from '../data/utils';
import CustomerIntegrations from './CustomerIntegrations';

const CustomerViewContainer = () => {
const { id } = useParams();
Expand Down Expand Up @@ -58,6 +59,12 @@ const CustomerViewContainer = () => {
<Container className="mt-4">
<Stack gap={2}>
{!isLoading ? <CustomerCard enterpriseCustomer={enterpriseCustomer} /> : <Skeleton height={230} />}
<CustomerIntegrations
slug={enterpriseCustomer.slug}
activeIntegrations={enterpriseCustomer.activeIntegrations}
activeSSO={enterpriseCustomer.activeSsoConfigurations}
apiCredentialsEnabled={enterpriseCustomer.enableGenerationOfApiCredentials}
/>
</Stack>
</Container>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* 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 { formatDate } from '../../data/utils';
import CustomerIntegrations from '../CustomerIntegrations';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({ id: 'test-id' }),
}));

jest.mock('../../data/utils', () => ({
formatDate: jest.fn(),
}));

const mockSSOData = [{
enterpriseCustomer: '18005882300',
created: '2024-09-15T11:01:04.501365Z',
modified: '2024-09-15T11:01:04.501365Z',
displayName: 'Orange cats rule',
}];

const mockIntegratedChannelData = [
{
channelCode: 'MOODLE',
enterpriseCustomer: '18005882300',
lastModifiedAt: '2024-09-15T11:01:04.501365Z',
},
{
channelCode: 'CANVAS',
enterpriseCustomer: '18005882300',
lastModifiedAt: '2024-09-15T11:01:04.501365Z',
},
];

describe('CustomerViewIntegrations', () => {
it('renders cards', async () => {
formatDate.mockReturnValue('September 15, 2024');
render(
<IntlProvider locale="en">
<CustomerIntegrations
slug="marcel-the-shell"
activeIntegrations={mockIntegratedChannelData}
activeSSO={mockSSOData}
apiCredentialsEnabled
/>
</IntlProvider>,
);
await waitFor(() => {
expect(screen.getByText('Associated Integrations')).toBeInTheDocument();

expect(screen.getByText('SSO')).toBeInTheDocument();
expect(screen.getByText('Orange cats rule')).toBeInTheDocument();
expect(screen.getAllByText('Created September 15, 2024 • Last modified September 15, 2024')).toHaveLength(3);
expect(screen.getAllByText('Open in Admin Portal')).toHaveLength(3);

expect(screen.getAllByText('LEARNING PLATFORM')).toHaveLength(2);
expect(screen.getByText('Moodle')).toBeInTheDocument();
expect(screen.getByText('Canvas')).toBeInTheDocument();

expect(screen.getByText('INTEGRATION')).toBeInTheDocument();
expect(screen.getByText('API')).toBeInTheDocument();
});
});
});
3 changes: 3 additions & 0 deletions src/Configuration/Customers/data/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const DJANGO_ADMIN_BASE_URL = 'https://internal.courses.edx.org';

export default DJANGO_ADMIN_BASE_URL;
22 changes: 22 additions & 0 deletions src/data/services/EnterpriseApiService.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class LmsApiService {

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

static enterpriseSSOConfigurations = `${LmsApiService.enterpriseAPIBaseUrl}enterprise_customer_sso_configuration/`;

static integratedChannelsUrl = `${LmsApiService.baseUrl}/integrated_channels/api/v1/configs/`;

static fetchEnterpriseCatalogQueries = () => LmsApiService.apiClient().get(LmsApiService.enterpriseCatalogQueriesUrl);

static fetchEnterpriseCustomersBasicList = (enterpriseNameOrUuid) => LmsApiService.apiClient().get(`${LmsApiService.enterpriseCustomersBasicListUrl}${enterpriseNameOrUuid !== undefined ? `?name_or_uuid=${enterpriseNameOrUuid}` : ''}`);
Expand Down Expand Up @@ -121,6 +125,24 @@ class LmsApiService {
static fetchSubsidyAccessPolicies = async (enterpriseCustomerUuid) => LmsApiService.apiClient().get(
`${getConfig().ENTERPRISE_ACCESS_BASE_URL}/api/v1/subsidy-access-policies/?enterprise_customer_uuid=${enterpriseCustomerUuid}`,
);

static fetchEnterpriseCustomerSSOConfigs = (options) => {
const queryParams = new URLSearchParams({
...options,
});
return LmsApiService.apiClient().get(
`${LmsApiService.enterpriseSSOConfigurations}?${queryParams.toString()}`,
);
};

static fetchIntegratedChannels = (options) => {
const queryParams = new URLSearchParams({
...options,
});
return LmsApiService.apiClient().get(
`${LmsApiService.integratedChannelsUrl}?${queryParams.toString()}`,
);
};
}

export default LmsApiService;

0 comments on commit 9ddfb37

Please sign in to comment.