Skip to content

Commit 89a3569

Browse files
authored
Merge pull request #406 from openedx/knguyen2/ent-9166
feat: add users table
2 parents c6efe80 + 5456ed3 commit 89a3569

12 files changed

+452
-6
lines changed

src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useCopyToClipboard } from '../data/utils';
1212
const { HOME } = ROUTES.CONFIGURATION.SUB_DIRECTORY.CUSTOMERS;
1313

1414
export const CustomerDetailLink = ({ row }) => {
15-
const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();
15+
const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard(row.original.uuid);
1616
const { ADMIN_PORTAL_BASE_URL } = getConfig();
1717

1818
return (

src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import {
55
import { Launch, ContentCopy } from '@openedx/paragon/icons';
66
import { getConfig } from '@edx/frontend-platform';
77
import { formatDate, useCopyToClipboard } from '../data/utils';
8-
import DJANGO_ADMIN_BASE_URL from '../data/constants';
98
import CustomerDetailModal from './CustomerDetailModal';
109

1110
const CustomerCard = ({ enterpriseCustomer }) => {
12-
const { ADMIN_PORTAL_BASE_URL } = getConfig();
11+
const { ADMIN_PORTAL_BASE_URL, DJANGO_ADMIN_LMS_BASE_URL } = getConfig();
1312
const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();
1413
const [isDetailsOpen, openDetails, closeDetails] = useToggle(false);
1514

@@ -28,7 +27,7 @@ const CustomerCard = ({ enterpriseCustomer }) => {
2827
<Button
2928
className="text-dark-500"
3029
as="a"
31-
href={`${DJANGO_ADMIN_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
30+
href={`${DJANGO_ADMIN_LMS_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
3231
variant="inverse-primary"
3332
target="_blank"
3433
rel="noopener noreferrer"

src/Configuration/Customers/CustomerDetailView/CustomerIntegrations.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const CustomerIntegrations = ({
1616
<div>
1717
{(activeSSO || activeIntegrations || apiCredentialsEnabled) && (
1818
<div>
19-
<h2>Associated Integrations</h2>
19+
<h2>Associated integrations</h2>
20+
<hr />
2021
{activeSSO && activeSSO.map((sso) => (
2122
<CustomerViewCard
2223
slug={slug}

src/Configuration/Customers/CustomerDetailView/CustomerPlanContainer.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const CustomerPlanContainer = ({ slug }) => {
4848
Show inactive
4949
</Form.Switch>
5050
</div>
51+
<hr />
5152
{renderActivePoliciesCard}
5253
{renderActiveSubscriptions}
5354
{showInactive ? (

src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
1111
import CustomerCard from './CustomerCard';
1212
import { getEnterpriseCustomer } from '../data/utils';
1313
import CustomerIntegrations from './CustomerIntegrations';
14+
import EnterpriseCustomerUsersTable from './EnterpriseCustomerUsersTable';
1415
import CustomerPlanContainer from './CustomerPlanContainer';
1516

1617
const CustomerViewContainer = () => {
@@ -75,6 +76,7 @@ const CustomerViewContainer = () => {
7576
activeSSO={enterpriseCustomer.activeSsoConfigurations}
7677
apiCredentialsEnabled={enterpriseCustomer.enableGenerationOfApiCredentials}
7778
/>
79+
<EnterpriseCustomerUsersTable />
7880
</Stack>
7981
</Container>
8082
</div>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {
4+
Icon, IconButton, Stack, Chip,
5+
} from '@openedx/paragon';
6+
import { Person, Check, Timelapse } from '@openedx/paragon/icons';
7+
8+
export const EnterpriseCustomerUserDetail = ({
9+
row,
10+
}) => {
11+
let memberDetails;
12+
const memberDetailIcon = (
13+
<IconButton
14+
isActive
15+
invertColors
16+
src={Person}
17+
iconAs={Icon}
18+
className="border rounded-circle mr-3"
19+
alt="members detail column icon"
20+
style={{ opacity: 1, flexShrink: 0 }}
21+
/>
22+
);
23+
24+
if (row.original.enterpriseCustomerUser?.username) {
25+
memberDetails = (
26+
<div className="mb-n3">
27+
<p className="font-weight-bold mb-0">
28+
{row.original.enterpriseCustomerUser?.username}
29+
</p>
30+
<p>{row.original.enterpriseCustomerUser?.email}</p>
31+
</div>
32+
);
33+
} else {
34+
memberDetails = (
35+
<p className="align-middle mb-0">
36+
{row.original.pendingEnterpriseCustomerUser?.userEmail}
37+
</p>
38+
);
39+
}
40+
return (
41+
<Stack gap={0} direction="horizontal">
42+
{memberDetailIcon}
43+
{memberDetails}
44+
</Stack>
45+
);
46+
};
47+
48+
export const AdministratorCell = ({ row }) => {
49+
if (row.original?.pendingEnterpriseCustomerUser?.isPendingAdmin) {
50+
return (
51+
<Chip
52+
iconBefore={Timelapse}
53+
>
54+
Pending
55+
</Chip>
56+
);
57+
}
58+
return (
59+
<div>
60+
{row.original?.roleAssignments?.includes('enterprise_admin') ? <Check data-testid="admin check" aria-label="admin check" /> : null}
61+
</div>
62+
);
63+
};
64+
65+
export const LearnerCell = ({ row }) => {
66+
if (!row.original?.pendingEnterpriseCustomerUser?.isPendingLearner) {
67+
return (
68+
<div>
69+
{row.original?.roleAssignments?.includes('enterprise_learner') ? <Check data-testid="learner check" aria-label="learner check" /> : null}
70+
</div>
71+
);
72+
}
73+
74+
return (
75+
<Chip
76+
iconBefore={Timelapse}
77+
>
78+
Pending
79+
</Chip>
80+
);
81+
};
82+
83+
EnterpriseCustomerUserDetail.propTypes = {
84+
row: PropTypes.shape({
85+
original: PropTypes.shape({
86+
enterpriseCustomerUser: PropTypes.shape({
87+
email: PropTypes.string.isRequired,
88+
username: PropTypes.string,
89+
}),
90+
pendingEnterpriseCustomerUser: PropTypes.shape({
91+
isPendingAdmin: PropTypes.bool,
92+
userEmail: PropTypes.string,
93+
}),
94+
roleAssignments: PropTypes.arrayOf(PropTypes.string),
95+
}).isRequired,
96+
}).isRequired,
97+
};
98+
99+
AdministratorCell.propTypes = {
100+
row: PropTypes.shape({
101+
original: PropTypes.shape({
102+
pendingEnterpriseCustomerUser: PropTypes.shape({
103+
isPendingAdmin: PropTypes.bool,
104+
}),
105+
roleAssignments: PropTypes.arrayOf(PropTypes.string),
106+
}).isRequired,
107+
}).isRequired,
108+
};
109+
110+
LearnerCell.propTypes = {
111+
row: PropTypes.shape({
112+
original: PropTypes.shape({
113+
pendingEnterpriseCustomerUser: PropTypes.shape({
114+
isPendingLearner: PropTypes.bool,
115+
}),
116+
roleAssignments: PropTypes.arrayOf(PropTypes.string),
117+
}).isRequired,
118+
}).isRequired,
119+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useParams } from 'react-router-dom';
2+
import { DataTable, TextFilter } from '@openedx/paragon';
3+
import { EnterpriseCustomerUserDetail, LearnerCell, AdministratorCell } from './EnterpriseCustomerUserDetail';
4+
import useCustomerUsersTableData from '../data/hooks/useCustomerUsersTableData';
5+
6+
const EnterpriseCustomerUsersTable = () => {
7+
const { id } = useParams();
8+
const {
9+
isLoading,
10+
enterpriseUsersTableData,
11+
fetchEnterpriseUsersData,
12+
} = useCustomerUsersTableData(id);
13+
return (
14+
<div>
15+
<h2>Associated users ({enterpriseUsersTableData.itemCount})</h2>
16+
<hr />
17+
<DataTable
18+
isLoading={isLoading}
19+
isExpandable
20+
isPaginated
21+
manualPagination
22+
isFilterable
23+
manualFilters
24+
initialState={{
25+
pageSize: 8,
26+
pageIndex: 0,
27+
sortBy: [],
28+
filters: [],
29+
}}
30+
defaultColumnValues={{ Filter: TextFilter }}
31+
fetchData={fetchEnterpriseUsersData}
32+
data={enterpriseUsersTableData.results}
33+
itemCount={enterpriseUsersTableData.itemCount}
34+
pageCount={enterpriseUsersTableData.pageCount}
35+
columns={[
36+
{
37+
id: 'details',
38+
Header: 'User details',
39+
accessor: 'details',
40+
Cell: EnterpriseCustomerUserDetail,
41+
},
42+
{
43+
id: 'administrator',
44+
Header: 'Administrator',
45+
accessor: 'administrator',
46+
disableFilters: true,
47+
Cell: AdministratorCell,
48+
},
49+
{
50+
id: 'learner',
51+
Header: 'Learner',
52+
accessor: 'learner',
53+
disableFilters: true,
54+
Cell: LearnerCell,
55+
},
56+
]}
57+
/>
58+
</div>
59+
);
60+
};
61+
62+
export default EnterpriseCustomerUsersTable;

src/Configuration/Customers/CustomerDetailView/tests/CustomerViewIntegrations.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('CustomerViewIntegrations', () => {
4949
</IntlProvider>,
5050
);
5151
await waitFor(() => {
52-
expect(screen.getByText('Associated Integrations')).toBeInTheDocument();
52+
expect(screen.getByText('Associated integrations')).toBeInTheDocument();
5353

5454
expect(screen.getByText('SSO')).toBeInTheDocument();
5555
expect(screen.getByText('Orange cats rule')).toBeInTheDocument();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* eslint-disable react/prop-types */
2+
import {
3+
screen,
4+
render,
5+
} from '@testing-library/react';
6+
import '@testing-library/jest-dom';
7+
import {
8+
EnterpriseCustomerUserDetail,
9+
AdministratorCell,
10+
LearnerCell,
11+
} from '../EnterpriseCustomerUserDetail';
12+
13+
describe('EnterpriseCustomerUserDetail', () => {
14+
it('renders enterprise customer detail', () => {
15+
const enterpriseCustomerUser = {
16+
original: {
17+
enterpriseCustomerUser: {
18+
username: 'ash ketchum',
19+
20+
},
21+
},
22+
};
23+
render(<EnterpriseCustomerUserDetail row={enterpriseCustomerUser} />);
24+
expect(screen.getByText('ash ketchum')).toBeInTheDocument();
25+
expect(screen.getByText('[email protected]')).toBeInTheDocument();
26+
});
27+
28+
it('renders pending enterprise customer detail', () => {
29+
const pendingEnterpriseCustomerUser = {
30+
original: {
31+
pendingEnterpriseCustomerUser: {
32+
userEmail: '[email protected]',
33+
},
34+
},
35+
};
36+
render(<EnterpriseCustomerUserDetail row={pendingEnterpriseCustomerUser} />);
37+
expect(screen.getByText('[email protected]')).toBeInTheDocument();
38+
});
39+
40+
it('renders AdministratorCell there is a pending admin', () => {
41+
const pendingAdmin = {
42+
original: {
43+
pendingEnterpriseCustomerUser: {
44+
isPendingAdmin: true,
45+
},
46+
roleAssignments: ['enterprise_learner'],
47+
},
48+
};
49+
render(<AdministratorCell row={pendingAdmin} />);
50+
expect(screen.getByText('Pending')).toBeInTheDocument();
51+
});
52+
53+
it('renders AdministratorCell there is a registered admin', () => {
54+
const adminRow = {
55+
original: {
56+
pendingEnterpriseCustomerUser: {
57+
isPendingAdmin: false,
58+
},
59+
roleAssignments: ['enterprise_admin'],
60+
},
61+
};
62+
render(<AdministratorCell row={adminRow} />);
63+
expect(screen.queryByText('Pending')).not.toBeInTheDocument();
64+
});
65+
66+
it('renders LearnerCell when there is a registered learner and not pending', () => {
67+
const learnerRow = {
68+
original: {
69+
pendingEnterpriseCustomerUser: null,
70+
enterpriseCustomerUser: {
71+
username: 'ash ketchum',
72+
73+
},
74+
roleAssignments: ['enterprise_learner'],
75+
},
76+
};
77+
render(<LearnerCell row={learnerRow} />);
78+
expect(screen.queryByText('Pending')).not.toBeInTheDocument();
79+
});
80+
81+
it('renders LearnerCell for pending user', () => {
82+
const pendingLearnerRow = {
83+
original: {
84+
pendingEnterpriseCustomerUser: {
85+
isPendingLearner: true,
86+
userEmail: '[email protected]',
87+
},
88+
enterpriseCustomerUser: null,
89+
},
90+
};
91+
render(<LearnerCell row={pendingLearnerRow} />);
92+
expect(screen.queryByText('Pending')).toBeInTheDocument();
93+
});
94+
});

0 commit comments

Comments
 (0)