Skip to content

Commit e8fc93c

Browse files
authored
feat: analytics view (#202)
* feat: create member versions view * fix: update version of apps query client * feat: create a general statistic analytics component * feat: display analytic view at analytic context * test: add tests for analytics general statistics * fix: put usestates togother and remove force for dev run * fix: refactor * fix: fix default values operator * fix: use type alias instead of interfaces * fix: trnaslations use i18 namespace
1 parent c0f1e2f commit e8fc93c

File tree

17 files changed

+1236
-106
lines changed

17 files changed

+1236
-106
lines changed
Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { Context, PermissionLevel } from '@graasp/sdk';
22

3-
import { ANALYTICS_VIEW_CY, buildDataCy } from '../../../src/config/selectors';
3+
import {
4+
ANALYTICS_VIEW_CY,
5+
STATISTIC_RUNNING_VERSIONS_KEY,
6+
STATISTIC_SAVED_VERSIONS_KEY,
7+
STATISTIC_TOTAL_USERS_KEY,
8+
buildDataCy,
9+
buildStatisticCardID,
10+
} from '../../../src/config/selectors';
11+
import { MOCK_APP_ACTIONS } from '../../fixtures/appActions';
412

513
describe('Analytics View', () => {
614
beforeEach(() => {
@@ -9,13 +17,30 @@ describe('Analytics View', () => {
917
context: Context.Analytics,
1018
permission: PermissionLevel.Admin,
1119
},
20+
database: { appActions: MOCK_APP_ACTIONS },
1221
});
1322
cy.visit('/');
1423
});
1524

1625
it('should open Analytics view', () => {
17-
cy.get(buildDataCy(ANALYTICS_VIEW_CY))
26+
cy.get(buildDataCy(ANALYTICS_VIEW_CY)).should('be.visible');
27+
});
28+
29+
it('should have total users general statistic', () => {
30+
cy.get(`#${buildStatisticCardID(STATISTIC_TOTAL_USERS_KEY)}`)
31+
.should('be.visible')
32+
.and('have.text', '2');
33+
});
34+
35+
it('should have average saved versions general statistic', () => {
36+
cy.get(`#${buildStatisticCardID(STATISTIC_SAVED_VERSIONS_KEY)}`)
37+
.should('be.visible')
38+
.and('have.text', '1');
39+
});
40+
41+
it('should have average running versions general statistic', () => {
42+
cy.get(`#${buildStatisticCardID(STATISTIC_RUNNING_VERSIONS_KEY)}`)
1843
.should('be.visible')
19-
.and('have.text', 'Analytics View');
44+
.and('have.text', '1');
2045
});
2146
});

package.json

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
"@fortawesome/free-regular-svg-icons": "6.5.1",
2424
"@fortawesome/free-solid-svg-icons": "6.5.1",
2525
"@fortawesome/react-fontawesome": "0.2.0",
26-
"@graasp/apps-query-client": "3.4.3",
26+
"@graasp/apps-query-client": "3.4.8",
2727
"@graasp/pyodide": "github:graasp/graasp-pyodide",
28-
"@graasp/sdk": "3.3.0",
28+
"@graasp/sdk": "4.1.0",
2929
"@graasp/ui": "4.1.1",
3030
"@mui/icons-material": "5.15.1",
3131
"@mui/lab": "5.0.0-alpha.134",
@@ -53,14 +53,19 @@
5353
"lodash.isequal": "4.5.0",
5454
"lodash.isobject": "3.0.2",
5555
"lodash.isstring": "4.0.1",
56+
"lodash.orderby": "4.6.0",
57+
"lodash.sumby": "4.6.0",
5658
"prism-react-renderer": "2.3.1",
5759
"react": "18.2.0",
60+
"react-csv": "2.2.2",
5861
"react-diff-viewer": "3.1.1",
5962
"react-dom": "18.2.0",
6063
"react-i18next": "13.5.0",
6164
"react-markdown": "9.0.1",
6265
"react-mde": "12.0.8",
6366
"react-router-dom": "6.21.0",
67+
"react-sparklines": "1.7.0",
68+
"react-syntax-highlighter": "15.5.0",
6469
"react-toastify": "9.1.3",
6570
"remark-breaks": "4.0.0",
6671
"remark-gfm": "4.0.0",
@@ -98,10 +103,15 @@
98103
"@trivago/prettier-plugin-sort-imports": "4.3.0",
99104
"@types/file-saver": "^2.0.7",
100105
"@types/i18n": "0.13.10",
101-
"@types/lodash.countby": "4.6.9",
106+
"@types/lodash.countby": "^4",
102107
"@types/lodash.isequal": "4.5.8",
103108
"@types/lodash.isobject": "3.0.9",
104109
"@types/lodash.isstring": "4.0.9",
110+
"@types/lodash.orderby": "^4",
111+
"@types/lodash.sumby": "4.6.9",
112+
"@types/react-csv": "1.1.10",
113+
"@types/react-sparklines": "^1",
114+
"@types/react-syntax-highlighter": "^15",
105115
"@types/uuid": "9.0.7",
106116
"@typescript-eslint/eslint-plugin": "6.15.0",
107117
"@typescript-eslint/parser": "6.15.0",
@@ -151,7 +161,11 @@
151161
]
152162
},
153163
"resolutions": {
154-
"@codemirror/state": "6.3.3"
164+
"@codemirror/state": "6.3.3",
165+
"react": "18.2.0",
166+
"react-dom": "18.2.0",
167+
"@types/react": "18.2.60",
168+
"@types/react-dom": "18.2.19"
155169
},
156170
"packageManager": "[email protected]",
157171
"msw": {

src/config/selectors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,15 @@ export const buildCommitFieldCypress = (selector: string): string =>
182182
`[data-cy=${selector}]`;
183183
export const settingKeyDataCy = (key: string): string => `setting-${key}`;
184184

185+
export const buildStatisticCardID = (key: string): string => `statistic-${key}`;
186+
185187
// keys for save buttons and tests
186188
export const EXECUTION_MODE_SETTINGS_KEY = 'EXECUTION_MODE_SETTINGS_KEY';
187189
export const REVIEW_MODE_SETTINGS_KEY = 'REVIEW_MODE_SETTINGS_KEY';
188190
export const EXPLAIN_MODE_SETTINGS_KEY = 'EXPLAIN_MODE_SETTINGS_KEY';
189191
export const SETTING_NEW_CHATBOT_PROMPT_KEY = 'NEW_CHATBOT_PROMPT_KEY';
192+
193+
export const STATISTIC_TIME_SPENT_KEY = 'timeSpent';
194+
export const STATISTIC_TOTAL_USERS_KEY = 'totalUsers';
195+
export const STATISTIC_SAVED_VERSIONS_KEY = 'savedVersions';
196+
export const STATISTIC_RUNNING_VERSIONS_KEY = 'runningVersions';

src/interfaces/analytics.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface GeneralMemberStatistic {
2+
memberId: string;
3+
endTime: string;
4+
startTime: string;
5+
memberName: string;
6+
savedVersions: number;
7+
runningVersions: number;
8+
spentTimeInSeconds: number;
9+
}

src/langs/en.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,24 @@
157157
"APP_MODE_COLLABORATE_DESCRIPTION": "Coming soon !",
158158
"PUBLIC_ALERT": "You are not authenticated. You cannot save any data.",
159159
"MAX_COMMENT_LENGTH_SETTING": "Maximum comment length (default is {{default}} character)",
160-
"MAX_THREAD_LENGTH_SETTING": "Maximum thread length (default is {{default}} messages per user)"
160+
"MAX_THREAD_LENGTH_SETTING": "Maximum thread length (default is {{default}} messages per user)",
161+
"Statistics for running code": "Statistics for running code",
162+
"Search by member name": "Search by member name",
163+
"versions": "versions",
164+
"No Results Match Member": "No Results Match Member",
165+
"General Statistics": "General Statistics",
166+
"Average Time Spent By Users": "Average Time Spent By Users",
167+
"Total Users": "Total Users",
168+
"Average Saved Versions": "Average Saved Versions",
169+
"Average Running Versions": "Average Running Versions",
170+
"Export": "Export",
171+
"Time Spent": "Time Spent",
172+
"GET_ITEM_ERROR": "Sorry, There was an error retrieving this item actions.",
173+
"No Records Yet": "No Records Yet",
174+
"hours": "hours",
175+
"minutes": "minutes",
176+
"VERSION_COUNT_one": "{{count}} version",
177+
"VERSION_COUNT_other": "{{count}} versions",
178+
"LAST_VERSION": "Last version: {{date}}"
161179
}
162180
}

src/modules/main/App.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { useLocalContext } from '@graasp/apps-query-client';
44
import { Context } from '@graasp/sdk';
55

66
import i18n from '../../config/i18n';
7-
import { ANALYTICS_VIEW_CY } from '../../config/selectors';
87
import { DEFAULT_CONTEXT_LANGUAGE } from '../../config/settings';
98
import { AppDataProvider } from '../context/AppDataContext';
109
import { MembersProvider } from '../context/MembersContext';
1110
import { SettingsProvider } from '../context/SettingsContext';
1211
import BuilderView from '../views/admin/BuilderView';
12+
import AnalyticsView from '../views/analytics/AnalyticsView';
1313
import PlayerView from '../views/read/PlayerView';
1414

1515
const App: FC = () => {
@@ -25,12 +25,11 @@ const App: FC = () => {
2525

2626
const renderContent = (): ReactElement => {
2727
switch (context.context) {
28-
// eslint-disable-next-line default-case-last
2928
case Context.Builder:
3029
return <BuilderView />;
3130

3231
case Context.Analytics:
33-
return <div data-cy={ANALYTICS_VIEW_CY}>Analytics View</div>;
32+
return <AnalyticsView />;
3433

3534
case Context.Player:
3635
default:
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useTranslation } from 'react-i18next';
2+
3+
import { Alert, Box } from '@mui/material';
4+
5+
import { AppAction } from '@graasp/sdk';
6+
import { Loader } from '@graasp/ui';
7+
8+
import { differenceInSeconds } from 'date-fns';
9+
import groupBy from 'lodash.groupby';
10+
import orderBy from 'lodash.orderby';
11+
12+
import { APP_ACTIONS_TYPES } from '@/config/appActionsTypes';
13+
import { hooks } from '@/config/queryClient';
14+
import { CodeVersionType } from '@/interfaces/codeVersions';
15+
16+
import { ANALYTICS_VIEW_CY } from '../../../config/selectors';
17+
import GeneralStatistics from './GeneralStatistics';
18+
import UsersRunningCodeVersions from './UsersRunningCodeVersions';
19+
20+
const AnalyticsView = (): JSX.Element => {
21+
const { data, isLoading } = hooks.useAppActions();
22+
23+
const { t } = useTranslation();
24+
25+
if (data) {
26+
const actionsOrdersByCreatedDate = orderBy(data, 'createdAt');
27+
const actionsByType = groupBy(actionsOrdersByCreatedDate, 'type');
28+
const actionsByMember = groupBy(actionsOrdersByCreatedDate, 'member.id');
29+
30+
const generalStatistic = Object.entries(actionsByMember).map(
31+
([memberId, memberActions]) => {
32+
const startTime = memberActions[0].createdAt;
33+
const endTime = memberActions[memberActions.length - 1].createdAt;
34+
return {
35+
memberId,
36+
endTime,
37+
startTime,
38+
memberName: memberActions[0].member.name,
39+
savedVersions: memberActions.filter(
40+
(version) => version.type === APP_ACTIONS_TYPES.SAVE_CODE,
41+
).length,
42+
runningVersions: memberActions.filter(
43+
(version) => version.type === APP_ACTIONS_TYPES.RUN_CODE,
44+
).length,
45+
spentTimeInSeconds: differenceInSeconds(endTime, startTime),
46+
};
47+
},
48+
);
49+
50+
return (
51+
<div data-cy={ANALYTICS_VIEW_CY}>
52+
<GeneralStatistics generalStatistics={generalStatistic} />
53+
<UsersRunningCodeVersions
54+
runningVersions={
55+
actionsByType[
56+
APP_ACTIONS_TYPES.RUN_CODE
57+
] as AppAction<CodeVersionType>[]
58+
}
59+
generalStatistics={generalStatistic}
60+
/>
61+
</div>
62+
);
63+
}
64+
65+
if (isLoading) {
66+
return <Loader />;
67+
}
68+
69+
return (
70+
<Box pl={2} pr={2} mb={2} flexGrow={1}>
71+
<Alert severity="error">{t('GET_ITEM_ERROR')}</Alert>
72+
</Box>
73+
);
74+
};
75+
76+
export default AnalyticsView;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
4+
import { AccessTime, People, RunCircleSharp, Save } from '@mui/icons-material';
5+
import { Box, Grid, Typography } from '@mui/material';
6+
7+
import { formatDuration } from 'date-fns';
8+
import sumBy from 'lodash.sumby';
9+
10+
import {
11+
STATISTIC_RUNNING_VERSIONS_KEY,
12+
STATISTIC_SAVED_VERSIONS_KEY,
13+
STATISTIC_TIME_SPENT_KEY,
14+
STATISTIC_TOTAL_USERS_KEY,
15+
} from '@/config/selectors';
16+
import { GeneralMemberStatistic } from '@/interfaces/analytics';
17+
import { formatSeconds } from '@/utils/chart';
18+
19+
import StatisticCard from './StatisticCard';
20+
21+
const AccessTimeIcon = <AccessTime fontSize="large" color="primary" />;
22+
const PeopleIcon = <People fontSize="large" color="primary" />;
23+
const SaveIcon = <Save fontSize="large" color="primary" />;
24+
const RunIcon = <RunCircleSharp fontSize="large" color="primary" />;
25+
26+
type Props = {
27+
generalStatistics: GeneralMemberStatistic[];
28+
};
29+
30+
const GeneralStatistics = ({ generalStatistics }: Props): JSX.Element => {
31+
const { t } = useTranslation();
32+
const savedVersions = sumBy(generalStatistics, 'savedVersions');
33+
const runningVersions = sumBy(generalStatistics, 'runningVersions');
34+
35+
const averageTime = generalStatistics.reduce(
36+
(acc, curr) => acc + curr.spentTimeInSeconds,
37+
0,
38+
);
39+
40+
const { hours, minutes } = formatSeconds(
41+
averageTime / generalStatistics.length,
42+
);
43+
44+
return (
45+
<Box marginY={4}>
46+
<Typography variant="h6" align="center">
47+
{t('General Statistics')}
48+
</Typography>
49+
<Grid container spacing={2} marginTop={1} justifyContent="center">
50+
<StatisticCard
51+
icon={AccessTimeIcon}
52+
title={t('Average Time Spent By Users')}
53+
stat={formatDuration({ hours, minutes })}
54+
key={STATISTIC_TIME_SPENT_KEY}
55+
cardId={STATISTIC_TIME_SPENT_KEY}
56+
/>
57+
<StatisticCard
58+
icon={PeopleIcon}
59+
title={t('Total Users')}
60+
stat={generalStatistics.length}
61+
key={STATISTIC_TOTAL_USERS_KEY}
62+
cardId={STATISTIC_TOTAL_USERS_KEY}
63+
/>
64+
<StatisticCard
65+
icon={SaveIcon}
66+
title={t('Average Saved Versions')}
67+
stat={(savedVersions / generalStatistics.length || 0).toFixed()}
68+
key={STATISTIC_SAVED_VERSIONS_KEY}
69+
cardId={STATISTIC_SAVED_VERSIONS_KEY}
70+
/>
71+
<StatisticCard
72+
icon={RunIcon}
73+
title={t('Average Running Versions')}
74+
stat={(runningVersions / generalStatistics.length || 0).toFixed()}
75+
key={STATISTIC_RUNNING_VERSIONS_KEY}
76+
cardId={STATISTIC_RUNNING_VERSIONS_KEY}
77+
/>
78+
</Grid>
79+
</Box>
80+
);
81+
};
82+
83+
export default GeneralStatistics;

0 commit comments

Comments
 (0)