Skip to content

Commit

Permalink
feat: display AI analytics section on LPR page
Browse files Browse the repository at this point in the history
  • Loading branch information
mahamakifdar19 committed Oct 9, 2023
1 parent 6a47c98 commit 6bee69b
Show file tree
Hide file tree
Showing 19 changed files with 2,944 additions and 198 deletions.
35 changes: 35 additions & 0 deletions src/components/AIAnalyticsSummary/data/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import { logError } from '@edx/frontend-platform/logging';
import LmsApiService from '../../../data/services/LmsApiService';

const useAIAnalyticsSummary = (enterpriseId, insights) => {
const [isLoading, setIsLoading] = useState(true);
const [analyticsSummaryData, setAnalyticsSummaryData] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
const response = await LmsApiService.generateAIAnalyticsSummary(enterpriseId, insights);
setAnalyticsSummaryData(response.data);
} catch (error) {
logError(error);

Check warning on line 16 in src/components/AIAnalyticsSummary/data/hooks.js

View check run for this annotation

Codecov / codecov/patch

src/components/AIAnalyticsSummary/data/hooks.js#L16

Added line #L16 was not covered by tests
} finally {
setIsLoading(false);
}
};

if (enterpriseId && insights) {
fetchData();
} else {
setIsLoading(false);
}
}, [enterpriseId, insights]);

return {
isLoading,
data: analyticsSummaryData,
};
};

export default useAIAnalyticsSummary;
78 changes: 78 additions & 0 deletions src/components/AIAnalyticsSummary/tests/hooks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { renderHook } from '@testing-library/react-hooks';
import useAIAnalyticsSummary from '../data/hooks';
import LmsApiService from '../../../data/services/LmsApiService';

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

jest.mock('../../../data/services/LmsApiService', () => ({
generateAIAnalyticsSummary: jest.fn(),
}));

const TEST_ENTERPRISE_ID = 'test-enterprise-uuid';

const mockInsightsData = {
learner_progress: {
enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9',
enterprise_customer_name: 'Microsoft Corporation',
active_subscription_plan: true,
assigned_licenses: 0,
activated_licenses: 0,
assigned_licenses_percentage: 0.0,
activated_licenses_percentage: 0.0,
active_enrollments: 1026,
at_risk_enrollment_less_than_one_hour: 26,
at_risk_enrollment_end_date_soon: 15,
at_risk_enrollment_dormant: 918,
created_at: '2023-10-02T03:24:17Z',
},
learner_engagement: {
enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9',
enterprise_customer_name: 'Microsoft Corporation',
enrolls: 49,
enrolls_prior: 45,
passed: 2,
passed_prior: 0,
engage: 67,
engage_prior: 50,
hours: 62,
hours_prior: 49,
contract_end_date: '2022-06-13T00:00:00Z',
active_contract: false,
created_at: '2023-10-02T03:24:40Z',
},
};
const mockAnalyticsData = {
learner_progress: 'As an administrator running an online learning program on edX For Business, currently, none of the licenses are active',
learner_engagement: 'In the last 30 days, your online learning program on edX For Business has seen positive growth.',
};

describe('useAIAnalyticsSummary', () => {
it('should fetch AI analytics summary data', async () => {
LmsApiService.generateAIAnalyticsSummary.mockResolvedValueOnce({ data: mockAnalyticsData });
const { result, waitForNextUpdate } = renderHook(() => useAIAnalyticsSummary(TEST_ENTERPRISE_ID, mockInsightsData));

expect(result.current).toEqual({
isLoading: true,
data: null,
});

await waitForNextUpdate();

expect(LmsApiService.generateAIAnalyticsSummary).toHaveBeenCalled();
expect(result.current).toEqual({
isLoading: false,
data: mockAnalyticsData,
});
});

it('should not fetch data if enterpriseId or insights is missing', async () => {
const { result } = renderHook(() => useAIAnalyticsSummary(TEST_ENTERPRISE_ID));

expect(result.current).toEqual({
isLoading: false,
data: null,
});
});
});
100 changes: 100 additions & 0 deletions src/components/Admin/AIAnalyticsSummary.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Button, Card, Stack, Badge,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { AutoFixHigh, Groups } from '@edx/paragon/icons';
import useAIAnalyticsSummary from '../AIAnalyticsSummary/data/hooks';

const AnalyticsDetailCard = ({ onClose, data }) => (
<Card className="mt-3 mb-4">
<Card.Section>
<Badge variant="light" className="mb-3 font-weight-semibold">
Beta
</Badge>
<Stack gap={1} direction="horizontal">
<p className="card-text text-justify small">{data || 'No insights found..'}</p>
<Button variant="link" className="mb-4 ml-3" onClick={onClose}>
<span className="small font-weight-bold text-gray-800">Dismiss</span>
</Button>
</Stack>
<label className="x-small" htmlFor="poweredBylabel">Powered by OpenAI</label>
</Card.Section>
</Card>
);

AnalyticsDetailCard.propTypes = {
onClose: PropTypes.func.isRequired,
data: PropTypes.string,
};

const AIAnalyticsSummary = ({ enterpriseId, insights }) => {
const [showSummarizeCard, setShowSummarizeCard] = useState(false);
const [showTrackProgressCard, setShowTrackProgressCard] = useState(false);

const analyticsSummary = useAIAnalyticsSummary(enterpriseId, insights);

const toggleSummarizeCard = () => {
setShowSummarizeCard(!showSummarizeCard);
setShowTrackProgressCard(false);
};

const toggleTrackProgressCard = () => {
setShowTrackProgressCard(!showTrackProgressCard);
setShowSummarizeCard(false);
};

return (
<>
<Stack gap={3} direction="horizontal">
<Button
variant="outline-primary"
className="d-sm-inline"
onClick={toggleSummarizeCard}
data-testid="summarize-analytics"
>
<>
<AutoFixHigh className="mr-2" />
<FormattedMessage id="adminPortal.summarizeAnalytics" defaultMessage="Summarize Analytics" />
</>
</Button>
<Button
variant="outline-primary"
className="d-sm-inline"
onClick={toggleTrackProgressCard}
data-testid="track-progress"
>
<>
<Groups className="mr-2" />
<FormattedMessage id="adminPortal.trackProgress" defaultMessage="Track Progress" />
</>
</Button>
</Stack>
{showSummarizeCard && (
<AnalyticsDetailCard
data={analyticsSummary?.data?.learner_engagement}
onClose={() => setShowSummarizeCard(false)}

Check warning on line 78 in src/components/Admin/AIAnalyticsSummary.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/Admin/AIAnalyticsSummary.jsx#L78

Added line #L78 was not covered by tests
/>
)}
{showTrackProgressCard && (
<AnalyticsDetailCard
data={analyticsSummary?.data?.learner_progress}
onClose={() => setShowTrackProgressCard(false)}

Check warning on line 84 in src/components/Admin/AIAnalyticsSummary.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/Admin/AIAnalyticsSummary.jsx#L84

Added line #L84 was not covered by tests
/>
)}
</>
);
};

const mapStateToProps = state => ({
insights: state.dashboardInsights.insights,
});

AIAnalyticsSummary.propTypes = {
enterpriseId: PropTypes.string.isRequired,
insights: PropTypes.objectOf(PropTypes.shape),
};

export default connect(mapStateToProps)(AIAnalyticsSummary);
110 changes: 110 additions & 0 deletions src/components/Admin/AIAnalyticsSummary.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import configureMockStore from 'redux-mock-store';
import { MemoryRouter } from 'react-router-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import thunk from 'redux-thunk';
import AIAnalyticsSummary from './AIAnalyticsSummary';

const mockedInsights = {
learner_progress: {
enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9',
enterprise_customer_name: 'Microsoft Corporation',
active_subscription_plan: true,
assigned_licenses: 0,
activated_licenses: 0,
assigned_licenses_percentage: 0.0,
activated_licenses_percentage: 0.0,
active_enrollments: 1026,
at_risk_enrollment_less_than_one_hour: 26,
at_risk_enrollment_end_date_soon: 15,
at_risk_enrollment_dormant: 918,
created_at: '2023-10-02T03:24:17Z',
},
learner_engagement: {
enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9',
enterprise_customer_name: 'Microsoft Corporation',
enrolls: 49,
enrolls_prior: 45,
passed: 2,
passed_prior: 0,
engage: 67,
engage_prior: 50,
hours: 62,
hours_prior: 49,
contract_end_date: '2022-06-13T00:00:00Z',
active_contract: false,
created_at: '2023-10-02T03:24:40Z',
},
};
const mockStore = configureMockStore([thunk]);
const store = mockStore({
portalConfiguration: {
enterpriseId: 'test-enterprise-id',
},
dashboardInsights: mockedInsights,
});

const AIAnalyticsSummaryWrapper = props => (
<MemoryRouter>
<Provider store={store}>
<IntlProvider locale="en">
<AIAnalyticsSummary
enterpriseId="test-enterprise-id"
insights={mockedInsights}
{...props}
/>,
</IntlProvider>
</Provider>
</MemoryRouter>
);

describe('<AIAnalyticsSummary />', () => {
it('should render action buttons correctly', () => {
const tree = renderer
.create((
<AIAnalyticsSummaryWrapper
insights={mockedInsights}
/>
))
.toJSON();

expect(tree).toMatchSnapshot();
});

it('should display AnalyticsDetailCard with learner_engagement data when Summarize Analytics button is clicked', () => {
const wrapper = mount(<AIAnalyticsSummaryWrapper insights={mockedInsights} />);
wrapper.find('[data-testid="summarize-analytics"]').first().simulate('click');

const tree = renderer
.create(<AIAnalyticsSummaryWrapper insights={mockedInsights} />)
.toJSON();

expect(tree).toMatchSnapshot();
});

it('should display AnalyticsDetailCard with learner_progress data when Track Progress button is clicked', () => {
const wrapper = mount(<AIAnalyticsSummaryWrapper insights={mockedInsights} />);
wrapper.find('[data-testid="track-progress"]').first().simulate('click');

const tree = renderer
.create(<AIAnalyticsSummaryWrapper insights={mockedInsights} />)
.toJSON();

expect(tree).toMatchSnapshot();
});

it('should handle null analytics data', () => {
const insightsData = { ...mockedInsights, learner_engagement: null };
const wrapper = mount(<AIAnalyticsSummaryWrapper insights={insightsData} />);
wrapper.find('[data-testid="summarize-analytics"]').first().simulate('click');

const tree = renderer
.create(<AIAnalyticsSummaryWrapper insights={insightsData} />)
.toJSON();

expect(tree).toMatchSnapshot();
});
});
11 changes: 11 additions & 0 deletions src/components/Admin/AIAnalyticsSummarySkeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { Skeleton, Stack } from '@edx/paragon';

const AIAnalyticsSummarySkeleton = () => (
<Stack direction="horizontal" gap={2}>

Check warning on line 5 in src/components/Admin/AIAnalyticsSummarySkeleton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/Admin/AIAnalyticsSummarySkeleton.jsx#L5

Added line #L5 was not covered by tests
<Skeleton height={40} width={250} />
<Skeleton height={40} width={250} />
</Stack>
);

export default AIAnalyticsSummarySkeleton;
Loading

0 comments on commit 6bee69b

Please sign in to comment.