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 6, 2023
1 parent 6a47c98 commit 320bdd1
Show file tree
Hide file tree
Showing 16 changed files with 2,627 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);
} finally {
setIsLoading(false);
}
};

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

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

export default useAIAnalyticsSummary;
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)}
/>
)}
{showTrackProgressCard && (
<AnalyticsDetailCard
data={analyticsSummary?.data?.learner_progress}
onClose={() => setShowTrackProgressCard(false)}
/>
)}
</>
);
};

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

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

export default connect(mapStateToProps)(AIAnalyticsSummary);
87 changes: 87 additions & 0 deletions src/components/Admin/AIAnalyticsSummary.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 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();
});
});
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}>
<Skeleton height={40} width={250} />
<Skeleton height={40} width={250} />
</Stack>
);

export default AIAnalyticsSummarySkeleton;
67 changes: 67 additions & 0 deletions src/components/Admin/Admin.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const store = mockStore({
number_of_users: 3,
course_completions: 1,
},
dashboardInsights: {
loading: null,
insights: null,
},
});

const AdminWrapper = props => (
Expand All @@ -61,6 +65,8 @@ const AdminWrapper = props => (
url: '/',
}}
{...props}
fetchDashboardInsights={() => {}}
clearDashboardInsights={() => {}}
/>
</IntlProvider>
</Provider>
Expand All @@ -77,6 +83,7 @@ describe('<Admin />', () => {
courseCompletions: 1,
lastUpdatedDate: '2018-07-31T23:14:35Z',
numberOfUsers: 3,
insights: null,
};

describe('renders correctly', () => {
Expand Down Expand Up @@ -290,6 +297,66 @@ describe('<Admin />', () => {
.toJSON();
expect(tree).toMatchSnapshot();
});

it('with no dashboard insights data', () => {
const insights = null;
const tree = renderer
.create((
<AdminWrapper
{...baseProps}
insights={insights}
/>
))
.toJSON();

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

describe('with dashboard insights data', () => {
it('renders dashboard insights data correctly', () => {
const insights = {
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 tree = renderer
.create((
<AdminWrapper
{...baseProps}
insights={insights}
/>
))
.toJSON();

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

describe('handle changes to enterpriseId prop', () => {
Expand Down
Loading

0 comments on commit 320bdd1

Please sign in to comment.