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

feat: populates datatable with subsidies #343

Merged
merged 25 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e3e7cea
feat: Removes perLearnerEnrollmentLimit
brobro10000 Jun 12, 2023
a8f145c
feat: populates datatable with subsidies
brobro10000 Jun 13, 2023
4666299
feat: Populate datatable
brobro10000 Jun 21, 2023
fa5ff42
fix: PR updates (#346)
adamstankiewicz Jun 22, 2023
6a94221
feat: Updates paragon and @edx/brand packages
brobro10000 Jun 26, 2023
0ab500e
chore: merge
brobro10000 Jun 26, 2023
5f58c03
chore: npm i
brobro10000 Jun 26, 2023
3e0f0b8
chore: Normalized tests to passing state
brobro10000 Jun 26, 2023
5eaebfb
feat: Supports pageCount from subsidy API
brobro10000 Jun 26, 2023
e27f9c7
feat: allow graceful fail if no results returned
brobro10000 Jun 26, 2023
744ef78
feat: Working datatable sort
brobro10000 Jun 27, 2023
5db02c0
feat: Filter by enterprise uuid through basic list
brobro10000 Jun 27, 2023
bdb56fd
feat: filter subsidy uuid and debounce
brobro10000 Jun 27, 2023
e5ed3da
feat: removed unused package, newrelic
brobro10000 Jun 27, 2023
862cd42
fix: Updates financial identifier validation to meet 00k criteria
brobro10000 Jun 27, 2023
bf212f8
feat: provides the length of characters typed for opportunity product
brobro10000 Jun 27, 2023
35e0136
feat: feature flags edit button on datatable
brobro10000 Jun 28, 2023
e57189d
feat: abstraction for greater test coverage
brobro10000 Jun 28, 2023
0eb755e
feat: testing pt 1
brobro10000 Jun 28, 2023
4e119bc
chore: testing 2
brobro10000 Jun 28, 2023
9d2973f
chore: testing 3
brobro10000 Jun 28, 2023
38b040b
chore: testing final
brobro10000 Jun 28, 2023
db1fb40
chore: PR fixes
brobro10000 Jul 11, 2023
4558167
chore: PR fixes 2
brobro10000 Jul 12, 2023
8d81730
chore: PR fixes 2
brobro10000 Jul 13, 2023
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 .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
FEATURE_CONFIGURATION_MANAGEMENT=''
FEATURE_CONFIGURATION_ENTERPRISE_PROVISION=''
FEATURE_CONFIGURATION_EDIT_ENTERPRISE_PROVISION=''
CREDENTIALS_BASE_URL=null
CSRF_TOKEN_API_PATH=null
ECOMMERCE_BASE_URL=null
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:18450'
FEATURE_CONFIGURATION_MANAGEMENT='true'
FEATURE_CONFIGURATION_ENTERPRISE_PROVISION='true'
FEATURE_CONFIGURATION_EDIT_ENTERPRISE_PROVISION='true'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
Expand Down
13,283 changes: 5,209 additions & 8,074 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
"url": "https://github.com/openedx/frontend-app-support-tools/issues"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^1.4.2",
"@edx/brand": "npm:@edx/brand-[email protected]",
Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to handle "~@edx/brand/paragon/overrides" import for datatable styles

Copy link
Member

Choose a reason for hiding this comment

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

[inform] Additional context: because this is an open-source repo in the openedx Github organization, the brand-openedx theme should be the default theme installed. Should a consumer want to use the brand-edx.org theme for an MFE, that should be configured outside of committed code. For 2U/edX, the @edx/brand package may be overridden at build+deploy time via configuration (example).

"@edx/frontend-enterprise-utils": "^3.0.0",
"@edx/frontend-platform": "^4.2.0",
"@edx/paragon": "^20.26.0",
"@edx/paragon": "20.45.0",
Copy link
Member Author

Choose a reason for hiding this comment

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

Upgraded to handle new DjangoShort icon
Screenshot 2023-06-26 at 11 38 34 AM

"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.15.1",
"@fortawesome/free-regular-svg-icons": "5.15.1",
Expand All @@ -41,8 +41,8 @@
"babel-polyfill": "6.26.0",
"classnames": "2.2.6",
"lodash.debounce": "4.0.8",
"lodash.snakecase": "4.1.1",
"moment": "2.29.1",
"newrelic": "5.13.1",
Copy link
Member

Choose a reason for hiding this comment

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

[inform] Context: newrelic is installed, but seemingly unused by this repo. All New Relic related things for MFEs currently comes from @edx/frontend-build and @edx/frontend-platform, without consumers needing to install newrelic themselves.

Having newrelic in package.json causes issues such as:

❯ npm i
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: '[email protected]',
npm WARN EBADENGINE   required: { node: '>=6.0.0 <13.0.0', npm: '>=3.0.0' },
npm WARN EBADENGINE   current: { node: 'v18.16.0', npm: '9.5.1' }
npm WARN EBADENGINE }
npm WARN deprecated [email protected]: This version of the New Relic Node Agent has reached the end of life.

"prop-types": "15.7.2",
"react": "16.14.0",
"react-dom": "16.14.0",
Expand Down
24 changes: 2 additions & 22 deletions src/Configuration/Provisioning/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,19 @@
import React, { useEffect, useState } from 'react';

import { Icon, IconButton } from '@edx/paragon';
import { EditOutline } from '@edx/paragon/icons';
import { useHistory } from 'react-router';
import { v4 as uuidv4 } from 'uuid';

import DashboardHeader from './DashboardHeader';
import DashboardDatatable from './DashboardDatatable';
import DashboardToast from './DashboardToast';

import DashboardDataTable from './DashboardDataTable';
import { toastText } from './data/constants';
import { useDashboardContext } from './data/hooks';

// TODO: Create a new item header, search box and datatable
const Dashboard = () => {
const { hydrateEnterpriseSubsidies } = useDashboardContext();
const history = useHistory();
const { location } = history;
const { state: locationState } = location;
const [toasts, setToasts] = useState([]);

const editLearnerCreditPlan = (uuid) => {
// TODO: Navigate to the edit page for the selected learner credit plan based on UUID
history.push(`/enterprise-configuration/learner-credit/${uuid}/edit`);
};

const editAction = (onIconInteraction) => (
<IconButton
src={EditOutline}
iconAs={Icon}
onClick={onIconInteraction}
/>
);

useEffect(() => {
if (locationState?.planSuccessfullyCreated) {
setToasts((prevState) => [...prevState, {
Expand All @@ -43,13 +24,12 @@ const Dashboard = () => {
delete newState.planSuccessfullyCreated;
history.replace({ ...location, state: newState });
}
hydrateEnterpriseSubsidies(25, editAction, editLearnerCreditPlan);
}, [toastText.successfulPlanCreation, history, location, locationState]);

return (
<>
<DashboardHeader />
<DashboardDatatable />
<DashboardDataTable />
{toasts.map(({ text, uuid }) => (<DashboardToast toastText={text} key={uuid} />))}
</>
);
Expand Down
2 changes: 1 addition & 1 deletion src/Configuration/Provisioning/DashboardContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createContext } from 'use-context-selector';
export const DashboardContext = createContext(null);
const DashboardContextProvider = ({ children }) => {
const contextValue = useState({
enterpriseSubsidies: [],
enterpriseSubsidies: { results: [] },
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { DataTable, TextFilter } from '@edx/paragon';
import React, { useCallback, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import debounce from 'lodash.debounce';
import { logError } from '@edx/frontend-platform/logging';
import { DashboardContext } from '../DashboardContext';
import { MAX_PAGE_SIZE } from '../data/constants';
import { useDashboardContext } from '../data/hooks';
import DashboardTableActions from './DashboardTableActions';
import DashboardTableBadges from './DashboardTableBadges';
import { sortDataTableData, transformDataTableData, transformDatatableDate } from '../data/utils';

const FilterStatus = (rest) => <DataTable.FilterStatus showFilteredFields={false} {...rest} />;

const DashboardDataTable = () => {
const { enterpriseSubsidies } = useContextSelector(DashboardContext, v => v[0]);
const { hydrateEnterpriseSubsidies } = useDashboardContext();
const [isLoading, setIsLoading] = useState(true);

// Implementation due to filterText value displaying accessor value customerName as opposed to Customer Name
Copy link
Member

Choose a reason for hiding this comment

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

[clarification] Is this comment dangling from the now-moved FilterStatus above?

Copy link
Member Author

Choose a reason for hiding this comment

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

lol yeah it is my bad


const fetchData = useCallback(async (datatableProps) => {
setIsLoading(true);
try {
await hydrateEnterpriseSubsidies({
pageIndex: datatableProps.pageIndex + 1,
sortBy: sortDataTableData(datatableProps),
filterBy: transformDataTableData(datatableProps),
});
} catch (e) {
logError(e);

Check warning on line 31 in src/Configuration/Provisioning/DashboardDataTable/DashboardDataTable.jsx

View check run for this annotation

Codecov / codecov/patch

src/Configuration/Provisioning/DashboardDataTable/DashboardDataTable.jsx#L31

Added line #L31 was not covered by tests
} finally {
setIsLoading(false);
}
}, [hydrateEnterpriseSubsidies]);

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

return (
<section className="mt-5">
<DataTable
isLoading={isLoading}
isPaginated
manualPagination
isSortable
manualSortBy
isFilterable
manualFilters
defaultColumnValues={{ Filter: TextFilter }}
pageCount={enterpriseSubsidies.pageCount || 0}
initialState={{
pageSize: MAX_PAGE_SIZE,
pageIndex: 0,
}}
itemCount={enterpriseSubsidies?.count || 0}
data={enterpriseSubsidies.results}
fetchData={debouncedFetchData}
FilterStatusComponent={FilterStatus}
columns={[
{
Header: 'Plan ID',
accessor: 'uuid',
},
{
Header: 'Plan name',
accessor: 'title',
},
{
Header: 'Plan Status',
accessor: 'isActive',
disableFilters: true,
Cell: DashboardTableBadges,
},
{
Header: 'Customer name',
accessor: 'enterpriseCustomerName',
disableSortBy: true,
},
{
Header: 'Start date',
accessor: 'activeDatetime',
disableFilters: true,
Cell: ({ row }) => transformDatatableDate(row.values.activeDatetime),
Copy link
Member

Choose a reason for hiding this comment

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

nit: just a caution that if/when these functions were to return JSX, this line and line 94 below would be going against the no-unstable-nested-components ESLint rule. Not suggesting you need to change anything here, but just wanted to inform.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, will leave open for reference 👍🏽

Copy link
Member

Choose a reason for hiding this comment

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

nitty nit: transformDataTableDate

Copy link
Member Author

Choose a reason for hiding this comment

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

Same as above, 👍🏽

},
{
Header: 'End date',
accessor: 'expirationDatetime',
disableFilters: true,
Cell: ({ row }) => transformDatatableDate(row.values.expirationDatetime),
},
{
Header: '',
accessor: 'actions',
disableFilters: true,
disableSortBy: true,
Cell: DashboardTableActions,
},
]}
/>
</section>
);
};

export default DashboardDataTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Hyperlink, Icon, IconButton } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useHistory } from 'react-router';
import { DjangoShort, EditOutline } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import ROUTES from '../../../data/constants/routes';

const DashboardTableActions = ({ row }) => {
const rowUuid = row.values.uuid;
const { DJANGO_ADMIN_SUBSIDY_BASE_URL } = getConfig();
const history = useHistory();
const { HOME } = ROUTES.CONFIGURATION.SUB_DIRECTORY.PROVISIONING;

const actionsArray = [];
if (getConfig().FEATURE_CONFIGURATION_EDIT_ENTERPRISE_PROVISION) {
actionsArray.push((
<IconButton
key="edit-icon"
size="sm"
src={EditOutline}
iconAs={Icon}
onClick={() => history.push(`${HOME}/${rowUuid}/edit`)}
alt="Edit Subsidy Icon Button"
data-testid={`Edit-${rowUuid}`}
/>
));
}
actionsArray.push(
<Hyperlink
key="django-icon"
destination={`${DJANGO_ADMIN_SUBSIDY_BASE_URL}/admin/subsidy/subsidy/?uuid=${rowUuid}`}
target="_blank"
showLaunchIcon={false}
data-testid="django-admin-link"
>
<IconButton
size="sm"
src={DjangoShort}
iconAs={Icon}
alt="Django Admin Icon Button"
data-testid={`Django-Admin-Page-${rowUuid}`}
/>
</Hyperlink>,
);
return actionsArray;
};
DashboardTableActions.propTypes = {
row: PropTypes.shape({
values: PropTypes.shape({
uuid: PropTypes.string,
}),
}).isRequired,
};

export default DashboardTableActions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
Badge,
} from '@edx/paragon';
import PropTypes from 'prop-types';

const DashboardTableBadges = ({ row }) => {
const { isActive } = row.values;
return (
<Badge
variant={isActive ? 'success' : 'danger'}
>
{isActive ? 'Active' : 'Inactive'}
</Badge>
);
};

DashboardTableBadges.propTypes = {
row: PropTypes.shape({
values: PropTypes.shape({
isActive: PropTypes.bool,
}),
}).isRequired,
};

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

export default DashboardDataTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable react/prop-types */
import { renderWithRouter } from '@edx/frontend-enterprise-utils';
import { screen, waitFor } from '@testing-library/react';
import { camelCaseObject } from '@edx/frontend-platform';
import { DashboardContext, initialStateValue } from '../../../testData/Dashboard';
import DashboardDataTable from '../DashboardDataTable';
import { sampleDataTableData } from '../../../testData/constants';

// Mock the debounce function
jest.mock('lodash.debounce', () => jest.fn((fn) => fn));

// Mock the subsidy list
const mockGetAllSubsidiesData = sampleDataTableData(10);
jest.mock('../../../../data/services/SubsidyApiService', () => ({
getAllSubsidies: () => Promise.resolve({
data: mockGetAllSubsidiesData,
}),
}));

// Mock the enterprise customers list
const mockCustomerData = camelCaseObject(mockGetAllSubsidiesData).results.map((subsidy) => ({
id: subsidy.enterpriseCustomerUuid,
name: subsidy.customerName,
}));
jest.mock('../../../../data/services/EnterpriseApiService', () => ({
fetchEnterpriseCustomersBasicList: () => Promise.resolve({
data: mockCustomerData,
}),
}));

const DashboardDatatableWrapper = ({
value = initialStateValue,
}) => (
<DashboardContext value={value}>
<DashboardDataTable />
</DashboardContext>
);

describe('DashboardDatatable', () => {
it('renders the datatable', () => {
renderWithRouter(<DashboardDatatableWrapper />);
expect(screen.getByText('loading')).toBeTruthy();
});
it('renders the datatable with data', async () => {
renderWithRouter(<DashboardDatatableWrapper />);
expect(screen.getByText('loading')).toBeTruthy();
await waitFor(() => expect(screen.getByText('Enterprise Customer 1')).toBeTruthy());
});
});
Loading
Loading