Skip to content

Commit

Permalink
Merge pull request #393 from openedx/knguyen2/ent-9156
Browse files Browse the repository at this point in the history
feat: add enterprise customer list view
  • Loading branch information
katrinan029 authored Aug 6, 2024
2 parents 4382569 + 31aa077 commit e541f84
Show file tree
Hide file tree
Showing 25 changed files with 1,116 additions and 5 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ BASE_URL=null
FEATURE_CONFIGURATION_MANAGEMENT=''
FEATURE_CONFIGURATION_ENTERPRISE_PROVISION=''
FEATURE_CONFIGURATION_EDIT_ENTERPRISE_PROVISION=''
FEATURE_CUSTOMER_SUPPORT_VIEW=''
ADMIN_PORTAL_BASE_URL=''
COMMERCE_COORDINATOR_ORDER_DETAILS_URL=''
CREDENTIALS_BASE_URL=null
CSRF_TOKEN_API_PATH=null
Expand Down
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ BASE_URL='http://localhost:18450'
FEATURE_CONFIGURATION_MANAGEMENT='true'
FEATURE_CONFIGURATION_ENTERPRISE_PROVISION='true'
FEATURE_CONFIGURATION_EDIT_ENTERPRISE_PROVISION='true'
FEATURE_CUSTOMER_SUPPORT_VIEW='true'
ADMIN_PORTAL_BASE_URL='http://localhost:1991'
COMMERCE_COORDINATOR_ORDER_DETAILS_URL='http://localhost:8140/lms/order_details_page_redirect'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import PropTypes from 'prop-types';
import {
DataTable, Icon, OverlayTrigger, Stack, Tooltip,
} from '@openedx/paragon';
import { Check, InfoOutline } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import useActiveAssociatedPlans from '../data/hooks/useActiveAssociatedPlans';

const SubscriptionCheckIcon = ({ row }) => {
if (row.original.hasActiveAgreements) {
return <Icon src={Check} screenReaderText="subscription check" />;
}
return null;
};

const PolicyCheckIcon = ({ row }) => {
if (row.original.hasActivePolicies) {
return <Icon src={Check} screenReaderText="policy check" />;
}
return null;
};

const OtherSubsidiesCheckIcon = ({ row }) => {
if (row.original.hasActiveOtherSubsidies) {
return <Icon src={Check} screenReaderText="other subsidies check" />;
}
return null;
};

export const OtherSubsidies = () => (
<Stack gap={1} direction="horizontal">
<span data-testid="members-table-status-column-header">
<FormattedMessage
id="configuration.customersPage.otherSubsidiesColumn"
defaultMessage="Other Subsidies"
description="Other subsidies column header in the Customers table"
/>
</span>
<OverlayTrigger
key="other-subsidies-tooltip"
placement="top"
overlay={(
<Tooltip id="other-subsidies-tooltip">
<div>
<FormattedMessage
id="configuration.customersPage.otherSubsidiesColumn.tooltip"
defaultMessage="Includes offers and codes"
description="Tooltip for the Other Subsidies column header in the Customers table"
/>
</div>
</Tooltip>
)}
>
<Icon size="xs" src={InfoOutline} className="ml-1 d-inline-flex" />
</OverlayTrigger>
</Stack>
);

const CustomerDetailRowSubComponent = ({ row }) => {
const enterpriseId = row.original.uuid;
const { data, isLoading } = useActiveAssociatedPlans(enterpriseId);
return (
<div className="sub-component w-50">
<DataTable
itemCount={1}
data={[data] || []}
isLoading={isLoading}
columns={[
{
Header: 'Subscription',
accessor: 'hasActiveSubscription',
Cell: SubscriptionCheckIcon,
},
{
Header: 'Learner Credit',
accessor: 'hasActivePolicies',
Cell: PolicyCheckIcon,
},
{
Header: OtherSubsidies,
accessor: 'hasActiveOtherSubsidies',
Cell: OtherSubsidiesCheckIcon,
},
]}
>
<DataTable.Table />
</DataTable>
</div>
);
};

CustomerDetailRowSubComponent.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
uuid: PropTypes.string,
}),
}).isRequired,
};

SubscriptionCheckIcon.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
hasActiveAgreements: PropTypes.bool,
}),
}).isRequired,
};

PolicyCheckIcon.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
hasActivePolicies: PropTypes.bool,
}),
}).isRequired,
};

OtherSubsidiesCheckIcon.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
hasActiveOtherSubsidies: PropTypes.bool,
}),
}).isRequired,
};

export default CustomerDetailRowSubComponent;
142 changes: 142 additions & 0 deletions src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useState } from 'react';
import {
Hyperlink,
Icon,
Toast,
} from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { Check, ContentCopy } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import ROUTES from '../../../data/constants/routes';

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 { ADMIN_PORTAL_BASE_URL } = getConfig();

return (
<div>
<div>
<Hyperlink
destination={`${HOME}/${row.original.slug}/view`}
key={row.original.uuid}
rel="noopener noreferrer"
variant="muted"
target="_blank"
showLaunchIcon={false}
className="customer-name"
>
{row.original.name}
</Hyperlink>
</div>
<div>
<Hyperlink
destination={`${ADMIN_PORTAL_BASE_URL}/${row.original.slug}/admin/learners`}
key={row.original.uuid}
rel="noopener noreferrer"
variant="muted"
target="_blank"
showLaunchIcon
>
/{row.original.slug}/
</Hyperlink>
<div
role="presentation"
className="pgn-doc__icons-table__preview-footer"
>
<div>{row.original.uuid}</div>
<Icon
key="ContentCopy"
src={ContentCopy}
data-testid="copy"
onClick={() => copyToClipboard(row.original.uuid)}
/>
</div>
</div>
<Toast
onClose={() => setShowToast(false)}
show={showToast}
delay={2000}
>
Copied to clipboard
</Toast>
</div>
);
};

export const LmsCheck = ({ row }) => {
if (row.original.activeIntegrations.length) {
return (
<Icon src={Check} screenReaderText="Lms Check" />
);
}
return null;
};

export const SSOCheck = ({ row }) => {
if (row.original.activeSsoConfigurations.length) {
return (
<Icon src={Check} screenReaderText="SSO Check" />
);
}
return null;
};

export const ApiCheck = ({ row }) => {
if (row.original.enableGenerationOfApiCredentials) {
return (
<Icon src={Check} screenReaderText="API Check" />
);
}
return null;
};

LmsCheck.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
activeIntegrations: PropTypes.arrayOf(PropTypes.shape({
channelCode: PropTypes.string,
created: PropTypes.string,
modified: PropTypes.string,
displayName: PropTypes.string,
active: PropTypes.bool,
})),
}),
}).isRequired,
};

SSOCheck.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
activeSsoConfigurations: PropTypes.arrayOf(PropTypes.shape({
created: PropTypes.string,
modified: PropTypes.string,
active: PropTypes.bool,
displayName: PropTypes.string,
})),
}),
}).isRequired,
};

ApiCheck.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
enableGenerationOfApiCredentials: PropTypes.bool,
}),
}).isRequired,
};

CustomerDetailLink.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
name: PropTypes.string,
uuid: PropTypes.string,
slug: PropTypes.string,
}),
}).isRequired,
};
107 changes: 107 additions & 0 deletions src/Configuration/Customers/CustomerDataTable/CustomersPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, {
useMemo,
useState,
useCallback,
useEffect,
} from 'react';
import debounce from 'lodash.debounce';
import {
Container, DataTable, TextFilter,
} from '@openedx/paragon';
import { camelCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';

import {
CustomerDetailLink,
SSOCheck,
LmsCheck,
ApiCheck,
} from './CustomerDetails';
import LmsApiService from '../../../data/services/EnterpriseApiService';
import CustomerDetailRowSubComponent from './CustomerDetailSubComponent';

const CustomersPage = () => {
const [enterpriseList, setEnterpriseList] = useState([]);
const [isLoading, setIsLoading] = useState(true);

const fetchData = useCallback(
async () => {
try {
const { data } = await LmsApiService.fetchEnterpriseCustomerSupportTool();
const result = camelCaseObject(data);
setEnterpriseList(result);
} catch (error) {
logError(error);
} finally {
setIsLoading(false);
}
},
[],
);

const debouncedFetchData = useMemo(() => debounce(
fetchData,
300,
), [fetchData]);

useEffect(() => {
debouncedFetchData();
}, [debouncedFetchData]);

return (
<Container className="mt-5">
<h1>Customers</h1>
<section className="mt-5">
<DataTable
isLoading={isLoading}
isExpandable
initialState={{
pageSize: 12,
}}
renderRowSubComponent={({ row }) => <CustomerDetailRowSubComponent row={row} />}
isPaginated
isFilterable
defaultColumnValues={{ Filter: TextFilter }}
itemCount={enterpriseList?.length || 0}
data={enterpriseList || []}
columns={[
{
id: 'expander',
Header: DataTable.ExpandAll,
Cell: DataTable.ExpandRow,
},
{
id: 'customer details',
Header: 'Customer details',
accessor: 'name',
Cell: CustomerDetailLink,
},
{
id: 'sso',
Header: 'SSO',
accessor: 'activeSsoConfigurations',
disableFilters: true,
Cell: SSOCheck,
},
{
id: 'lms',
Header: 'LMS',
accessor: 'activeIntegrations',
disableFilters: true,
Cell: LmsCheck,
},
{
id: 'api',
Header: 'API',
accessor: 'enableGenerationOfApiCredentials',
disableFilters: true,
Cell: ApiCheck,
},
]}
/>
</section>
</Container>
);
};

export default CustomersPage;
3 changes: 3 additions & 0 deletions src/Configuration/Customers/CustomerDataTable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import CustomersPage from './CustomersPage';

export default CustomersPage;
Loading

0 comments on commit e541f84

Please sign in to comment.