-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #393 from openedx/knguyen2/ent-9156
feat: add enterprise customer list view
- Loading branch information
Showing
25 changed files
with
1,116 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
src/Configuration/Customers/CustomerDataTable/CustomerDetailSubComponent.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
142
src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
107
src/Configuration/Customers/CustomerDataTable/CustomersPage.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import CustomersPage from './CustomersPage'; | ||
|
||
export default CustomersPage; |
Oops, something went wrong.