Skip to content

Commit

Permalink
feat: show transparent overlay over rendered chart while re-fetching …
Browse files Browse the repository at this point in the history
…data
  • Loading branch information
muhammad-ammar committed Sep 5, 2024
1 parent 7b5e690 commit 3f786ef
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 75 deletions.
2 changes: 1 addition & 1 deletion src/components/AdvanceAnalyticsV2/ProgressOverlay.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const ProgressOverlay = ({ isError, message }) => (

ProgressOverlay.propTypes = {
isError: PropTypes.bool.isRequired,
message: PropTypes.string.isRequired,
message: PropTypes.string,
};

export default ProgressOverlay;
42 changes: 23 additions & 19 deletions src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import ProgressOverlay from '../ProgressOverlay';
import classNames from 'classnames';
import {
Spinner,
} from '@openedx/paragon';
import ScatterChart from './ScatterChart';
import LineChart from './LineChart';
import BarChart from './BarChart';
import EmptyChart from './EmptyChart';

const ChartWrapper = ({
isLoading,
isFetching,
isError,
chartType,
chartProps,
loadingMessage,
}) => {
if (isLoading || isError) {
return (
<ProgressOverlay
isError={isError}
message={loadingMessage}
/>
);
if (isError) {
return <EmptyChart />;

Check warning on line 20 in src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx#L20

Added line #L20 was not covered by tests
}

const renderChart = () => {
const chartMap = {
ScatterChart: <ScatterChart {...chartProps} />,
LineChart: <LineChart {...chartProps} />,
BarChart: <BarChart {...chartProps} />,
};

return chartMap[chartType];
const chartMap = {
ScatterChart: <ScatterChart {...chartProps} />,
LineChart: <LineChart {...chartProps} />,
BarChart: <BarChart {...chartProps} />,
};

return renderChart();
return (
<div className={classNames('analytics-chart-container', { chartType }, { 'is-fetching': isFetching })}>
{isFetching && (
<div className="spinner-centered">
<Spinner animation="border" screenReaderText={loadingMessage} />
</div>
)}
{chartProps.data && chartMap[chartType]}
</div>
);
};

ChartWrapper.propTypes = {
isLoading: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isError: PropTypes.bool.isRequired,
chartType: PropTypes.oneOf(['ScatterChart', 'LineChart', 'BarChart']).isRequired,
chartProps: PropTypes.object.isRequired,

Check failure on line 45 in src/components/AdvanceAnalyticsV2/charts/ChartWrapper.jsx

View workflow job for this annotation

GitHub Actions / tests

Prop type "object" is forbidden
Expand Down
4 changes: 2 additions & 2 deletions src/components/AdvanceAnalyticsV2/data/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export const useEnterpriseAnalyticsData = ({
key,
requestOptions,
),
staleTime: 0.5 * (1000 * 60 * 60), // 30 minutes. Length of time before your data becomes stale
cacheTime: 0.75 * (1000 * 60 * 60), // 45 minutes. Length of time before inactive data gets removed from the cache
staleTime: 0.5 * (1000 * 60 * 60), // 30 minutes. The time in milliseconds after data is considered stale.
cacheTime: 0.75 * (1000 * 60 * 60), // 45 minutes. Cache data will be garbage collected after this duration.
keepPreviousData: true,
...queryOptions,
});
Expand Down
8 changes: 4 additions & 4 deletions src/components/AdvanceAnalyticsV2/data/hooks.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('useEnterpriseAnalyticsData', () => {

expect(result.current).toEqual(
expect.objectContaining({
isLoading: true,
isFetching: true,
error: null,
data: undefined,
}),
Expand All @@ -89,7 +89,7 @@ describe('useEnterpriseAnalyticsData', () => {
},
);
expect(result.current).toEqual(expect.objectContaining({
isLoading: false,
isFetching: false,
error: null,
data: camelCaseObject(mockAnalyticsCompletionsChartsData),
}));
Expand All @@ -115,7 +115,7 @@ describe('useEnterpriseAnalyticsData', () => {

expect(result.current).toEqual(
expect.objectContaining({
isLoading: true,
isFetching: true,
error: null,
data: undefined,
}),
Expand All @@ -136,7 +136,7 @@ describe('useEnterpriseAnalyticsData', () => {
},
);
expect(result.current).toEqual(expect.objectContaining({
isLoading: false,
isFetching: false,
error: null,
data: camelCaseObject(mockAnalyticsLeaderboardTableData),
}));
Expand Down
24 changes: 24 additions & 0 deletions src/components/AdvanceAnalyticsV2/styles/index.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
.analytics-stat-number {
font-size: 2.5rem;
}

.analytics-chart-container {
position: relative;
min-height: 40vh;

&.is-fetching::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba($white, .7);
z-index: 1;
}

.spinner-centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
}
4 changes: 2 additions & 2 deletions src/components/AdvanceAnalyticsV2/tabs/AnalyticsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const AnalyticsTable = ({
const [currentPage, setCurrentPage] = useState(0);

const {
isLoading, data, isPreviousData,
isFetching, data,
} = useEnterpriseAnalyticsData({
enterpriseCustomerUUID: enterpriseId,
key: analyticsDataTableKeys[name],
Expand Down Expand Up @@ -55,7 +55,7 @@ const AnalyticsTable = ({
isDownloadCSV={enableCSVDownload}
/>
<DataTable
isLoading={isLoading || isPreviousData}
isLoading={isFetching}
isPaginated
manualPagination
initialState={{
Expand Down
8 changes: 4 additions & 4 deletions src/components/AdvanceAnalyticsV2/tabs/Completions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const Completions = ({
const intl = useIntl();

const {
isLoading, isError, data,
isFetching, isError, data,
} = useEnterpriseAnalyticsData({
enterpriseCustomerUUID: enterpriseId,
key: ANALYTICS_TABS.COMPLETIONS,
Expand Down Expand Up @@ -47,7 +47,7 @@ const Completions = ({
isDownloadCSV
/>
<ChartWrapper
isLoading={isLoading}
isFetching={isFetching}
isError={isError}
chartType="LineChart"
chartProps={{
Expand Down Expand Up @@ -89,7 +89,7 @@ const Completions = ({
isDownloadCSV
/>
<ChartWrapper
isLoading={isLoading}
isFetching={isFetching}
isError={isError}
chartType="BarChart"
chartProps={{
Expand Down Expand Up @@ -134,7 +134,7 @@ const Completions = ({
isDownloadCSV
/>
<ChartWrapper
isLoading={isLoading}
isFetching={isFetching}
isError={isError}
chartType="BarChart"
chartProps={{
Expand Down
23 changes: 16 additions & 7 deletions src/components/AdvanceAnalyticsV2/tabs/Completions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Completions from './Completions';
import { queryClient } from '../../test/testUtils';
import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiService';

const mockAnalyticsData = {
const mockAnalyticsTableData = {
next: null,
previous: null,
count: 2,
Expand All @@ -32,12 +32,15 @@ const mockAnalyticsData = {
},
],
};
const mockAnalyticsChartsData = {
completionsOverTime: [],
topCoursesByCompletions: [],
topSubjectsByCompletions: [],
};

jest.spyOn(EnterpriseDataApiService, 'fetchAdminAnalyticsData');
const axiosMock = new MockAdapter(axios);
getAuthenticatedHttpClient.mockReturnValue(axios);
axiosMock.onAny().reply(200);
axios.get = jest.fn(() => Promise.resolve({ data: mockAnalyticsData }));

jest.mock('../charts/LineChart', () => {
const MockedLineChart = () => <div>Mocked LineChart</div>;
Expand All @@ -50,7 +53,14 @@ jest.mock('../charts/BarChart', () => {
});

describe('Completions Component', () => {
afterEach(() => {
axiosMock.reset();
});

test('renders all charts correctly', async () => {
axiosMock.onGet(/\/completions\/stats(\?.*)/).reply(200, mockAnalyticsChartsData);
axiosMock.onGet(/\/completions(\?.*)/).reply(200, mockAnalyticsTableData);

const { container } = render(
<QueryClientProvider client={queryClient()}>
<IntlProvider locale="en">
Expand Down Expand Up @@ -103,7 +113,7 @@ describe('Completions Component', () => {

// ensure the correct number of rows are rendered (including header row)
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(mockAnalyticsData.count + 1); // +1 for header row
expect(rows).toHaveLength(mockAnalyticsTableData.count + 1); // +1 for header row

// validate header row
const columnHeaders = within(rows[0]).getAllByRole('columnheader');
Expand All @@ -112,7 +122,7 @@ describe('Completions Component', () => {
});

// validate content of each data row
mockAnalyticsData.results.forEach((user, index) => {
mockAnalyticsTableData.results.forEach((user, index) => {
const rowCells = within(rows[index + 1]).getAllByRole('cell'); // Skip header row
expect(rowCells[0]).toHaveTextContent(user.email);
expect(rowCells[1]).toHaveTextContent(user.course_title);
Expand All @@ -124,10 +134,9 @@ describe('Completions Component', () => {
test('renders charts with correct loading messages', () => {
jest.mock('../data/hooks', () => ({
useEnterpriseAnalyticsTableData: jest.fn().mockReturnValue({
isLoading: true,
isFetching: true,
data: null,
isError: false,
isFetching: false,
error: null,
}),
}));
Expand Down
10 changes: 5 additions & 5 deletions src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const Engagements = ({
}) => {
const intl = useIntl();
const {
isLoading, isError, data,
isFetching, isError, data,
} = useEnterpriseAnalyticsData({
enterpriseCustomerUUID: enterpriseId,
key: ANALYTICS_TABS.ENGAGEMENTS,
Expand Down Expand Up @@ -44,7 +44,7 @@ const Engagements = ({
isDownloadCSV
/>
<ChartWrapper
isLoading={isLoading}
isFetching={isFetching}
isError={isError}
chartType="LineChart"
chartProps={{
Expand Down Expand Up @@ -84,7 +84,7 @@ const Engagements = ({
isDownloadCSV
/>
<ChartWrapper
isLoading={isLoading}
isFetching={isFetching}
isError={isError}
chartType="BarChart"
chartProps={{
Expand Down Expand Up @@ -127,11 +127,11 @@ const Engagements = ({
isDownloadCSV
/>
<ChartWrapper
isLoading={isLoading}
isFetching={isFetching}
isError={isError}
chartType="BarChart"
chartProps={{
data: data?.topCoursesByEngagement,
data: data?.topSubjectsByEngagement,
xKey: 'courseSubject',
yKey: 'count',
colorKey: 'enrollType',
Expand Down
25 changes: 17 additions & 8 deletions src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Engagements from './Engagements';
import { queryClient } from '../../test/testUtils';
import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiService';

const mockAnalyticsData = {
const mockAnalyticsTableData = {
next: null,
previous: null,
count: 2,
Expand All @@ -34,12 +34,15 @@ const mockAnalyticsData = {
},
],
};
const mockAnalyticsChartsData = {
engagementsOverTime: [],
topCoursesByEngagement: [],
topSubjectsByEngagement: [],
};

jest.spyOn(EnterpriseDataApiService, 'fetchAdminAnalyticsData');
const axiosMock = new MockAdapter(axios);
getAuthenticatedHttpClient.mockReturnValue(axios);
axiosMock.onAny().reply(200);
axios.get = jest.fn(() => Promise.resolve({ data: mockAnalyticsData }));

jest.mock('../charts/LineChart', () => {
const MockedLineChart = () => <div>Mocked LineChart</div>;
Expand All @@ -52,7 +55,14 @@ jest.mock('../charts/BarChart', () => {
});

describe('Engagements Component', () => {
test('renders all sections with correct classes and content', async () => {
afterEach(() => {
axiosMock.reset();
});

test('renders all charts correctly', async () => {
axiosMock.onGet(/\/engagements\/stats(\?.*)/).reply(200, mockAnalyticsChartsData);
axiosMock.onGet(/\/engagements(\?.*)/).reply(200, mockAnalyticsTableData);

const { container } = render(
<QueryClientProvider client={queryClient()}>
<IntlProvider locale="en">
Expand Down Expand Up @@ -105,7 +115,7 @@ describe('Engagements Component', () => {

// ensure the correct number of rows are rendered (including header row)
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(mockAnalyticsData.count + 1); // +1 for header row
expect(rows).toHaveLength(mockAnalyticsTableData.count + 1); // +1 for header row

// validate header row
const columnHeaders = within(rows[0]).getAllByRole('columnheader');
Expand All @@ -114,7 +124,7 @@ describe('Engagements Component', () => {
});

// validate content of each data row
mockAnalyticsData.results.forEach((user, index) => {
mockAnalyticsTableData.results.forEach((user, index) => {
const rowCells = within(rows[index + 1]).getAllByRole('cell'); // Skip header row
expect(rowCells[0]).toHaveTextContent(user.email);
expect(rowCells[1]).toHaveTextContent(user.course_title);
Expand All @@ -127,10 +137,9 @@ describe('Engagements Component', () => {
test('renders charts with correct loading messages', () => {
jest.mock('../data/hooks', () => ({
useEnterpriseAnalyticsTableData: jest.fn().mockReturnValue({
isLoading: true,
isFetching: true,
data: null,
isError: false,
isFetching: false,
error: null,
}),
}));
Expand Down
Loading

0 comments on commit 3f786ef

Please sign in to comment.