Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding integrations to the Customer view page #397

Merged
merged 4 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this value can be configured in .env?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we're pulling from import { getConfig } from '@edx/frontend-platform' so I was talking to Katrina about maybe putting in a ticket to add it in there?


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 @@

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 @@
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({

Check warning on line 130 in src/data/services/EnterpriseApiService.js

View check run for this annotation

Codecov / codecov/patch

src/data/services/EnterpriseApiService.js#L130

Added line #L130 was not covered by tests
...options,
});
return LmsApiService.apiClient().get(

Check warning on line 133 in src/data/services/EnterpriseApiService.js

View check run for this annotation

Codecov / codecov/patch

src/data/services/EnterpriseApiService.js#L133

Added line #L133 was not covered by tests
`${LmsApiService.enterpriseSSOConfigurations}?${queryParams.toString()}`,
);
};

static fetchIntegratedChannels = (options) => {
const queryParams = new URLSearchParams({

Check warning on line 139 in src/data/services/EnterpriseApiService.js

View check run for this annotation

Codecov / codecov/patch

src/data/services/EnterpriseApiService.js#L139

Added line #L139 was not covered by tests
...options,
});
return LmsApiService.apiClient().get(

Check warning on line 142 in src/data/services/EnterpriseApiService.js

View check run for this annotation

Codecov / codecov/patch

src/data/services/EnterpriseApiService.js#L142

Added line #L142 was not covered by tests
`${LmsApiService.integratedChannelsUrl}?${queryParams.toString()}`,
);
};
}

export default LmsApiService;