-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #989 from parlemonde/dashboard-to-staging
Dashboard to staging
- Loading branch information
Showing
36 changed files
with
1,189 additions
and
361 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,27 @@ | ||
services: | ||
mysql: | ||
image: mysql:8 | ||
environment: | ||
- MYSQL_ROOT_PASSWORD=my-secret-pw | ||
healthcheck: | ||
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost'] | ||
timeout: 20s | ||
retries: 10 | ||
ports: | ||
- '3306:3306' | ||
- '33060:33060' | ||
volumes: | ||
- ./.mysql-data:/var/lib/mysql | ||
minio: | ||
image: minio/minio | ||
ports: | ||
- '9000:9000' | ||
- '9090:9090' | ||
environment: | ||
- MINIO_ROOT_USER=minioadmin | ||
- MINIO_ROOT_PASSWORD=minioadmin | ||
- MINIO_ACCESS_KEY=minio | ||
- MINIO_SECRET_KEY=minio123 | ||
volumes: | ||
- ./.minio-data:/data | ||
command: server --console-address ":9090" /data | ||
# services: | ||
# mysql: | ||
# image: mysql:8 | ||
# environment: | ||
# - MYSQL_ROOT_PASSWORD=my-secret-pw | ||
# healthcheck: | ||
# test: [ 'CMD', 'mysqladmin', 'ping', '-h', 'localhost' ] | ||
# timeout: 20s | ||
# retries: 10 | ||
# ports: | ||
# - '3306:3306' | ||
# - '33060:33060' | ||
# volumes: | ||
# - ./.mysql-data:/var/lib/mysql | ||
# minio: | ||
# image: minio/minio | ||
# ports: | ||
# - '9000:9000' | ||
# - '9090:9090' | ||
# environment: | ||
# - MINIO_ROOT_USER=minioadmin | ||
# - MINIO_ROOT_PASSWORD=minioadmin | ||
# - MINIO_ACCESS_KEY=minio | ||
# - MINIO_SECRET_KEY=minio123 | ||
# volumes: | ||
# - ./.minio-data:/data | ||
# command: server --console-address ":9090" /data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,110 +1,45 @@ | ||
import { AnalyticSession } from '../entities/analytic'; | ||
import { Classroom } from '../entities/classroom'; | ||
import { AppDataSource } from '../utils/data-source'; | ||
import type { Request } from 'express'; | ||
|
||
import { | ||
getClassroomsInfos, | ||
getConnectedClassroomsCount, | ||
getContributedClassroomsCount, | ||
getRegisteredClassroomsCount, | ||
} from '../stats/classroomStats'; | ||
import { | ||
getAverageConnections, | ||
getAverageDuration, | ||
getMaxConnections, | ||
getMaxDuration, | ||
getMedianConnections, | ||
getMedianDuration, | ||
getMinConnections, | ||
getMinDuration, | ||
} from '../stats/sessionStats'; | ||
import { Controller } from './controller'; | ||
|
||
export const statisticsController = new Controller('/statistics'); | ||
|
||
const classroomRepository = AppDataSource.getRepository(Classroom); | ||
const analyticSessionRepository = AppDataSource.getRepository(AnalyticSession); | ||
|
||
statisticsController.get({ path: '/classrooms' }, async (_req, res) => { | ||
const classroomsData = await classroomRepository | ||
.createQueryBuilder('classroom') | ||
.leftJoin('classroom.village', 'village') | ||
.leftJoin('classroom.user', 'user') | ||
.addSelect('classroom.id', 'classroomId') | ||
.addSelect('classroom.name', 'classroomName') | ||
.addSelect('classroom.countryCode', 'classroomCountryCode') | ||
.addSelect('village.id', 'villageId') | ||
.addSelect('village.name', 'villageName') | ||
.addSelect('user.id', 'userId') | ||
.addSelect('user.firstname', 'userFirstname') | ||
.addSelect('user.lastname', 'userLastname') | ||
.addSelect( | ||
`(SELECT COUNT(comment.id) | ||
FROM comment | ||
WHERE comment.userId = user.id) AS commentsCount`, | ||
) | ||
.addSelect( | ||
`(SELECT COUNT(video.id) | ||
FROM video | ||
WHERE video.userId = user.id) AS videosCount`, | ||
) | ||
.addSelect( | ||
`(SELECT JSON_ARRAYAGG( | ||
JSON_OBJECT( | ||
'phase', ac.phase, | ||
'activities', ac.activities | ||
) | ||
) | ||
FROM ( | ||
SELECT | ||
activity.phase, | ||
JSON_ARRAYAGG( | ||
JSON_OBJECT( | ||
'type', activity.type, | ||
'count', activity.totalActivities | ||
) | ||
) AS activities | ||
FROM ( | ||
SELECT | ||
activity.phase, | ||
activity.type, | ||
COUNT(activity.id) AS totalActivities | ||
FROM activity | ||
WHERE activity.userId = user.id | ||
AND activity.villageId = classroom.villageId | ||
AND activity.deleteDate IS NULL | ||
GROUP BY activity.phase, activity.type | ||
) AS activity | ||
GROUP BY activity.phase | ||
) AS ac | ||
) AS activitiesCount`, | ||
) | ||
.groupBy('classroom.id') | ||
.addGroupBy('user.id') | ||
.getRawMany(); | ||
statisticsController.get({ path: '/sessions/:phase' }, async (req: Request, res) => { | ||
const phase = req.params.phase ? parseInt(req.params.phase) : null; | ||
|
||
res.sendJSON( | ||
classroomsData.map((classroom) => ({ | ||
...classroom, | ||
commentsCount: parseInt(classroom.commentsCount, 10), | ||
videosCount: parseInt(classroom.videosCount, 10), | ||
})), | ||
); | ||
res.sendJSON({ | ||
minDuration: await getMinDuration(), // TODO - add phase | ||
maxDuration: await getMaxDuration(), // TODO - add phase | ||
averageDuration: await getAverageDuration(), // TODO - add phase | ||
medianDuration: await getMedianDuration(), // TODO - add phase | ||
minConnections: await getMinConnections(), // TODO - add phase | ||
maxConnections: await getMaxConnections(), // TODO - add phase | ||
averageConnections: await getAverageConnections(), // TODO - add phase | ||
medianConnections: await getMedianConnections(), // TODO - add phase | ||
registeredClassroomsCount: await getRegisteredClassroomsCount(), | ||
connectedClassroomsCount: await getConnectedClassroomsCount(), // TODO - add phase | ||
contributedClassroomsCount: await getContributedClassroomsCount(phase), | ||
}); | ||
}); | ||
|
||
statisticsController.get({ path: '/connections' }, async (_req, res) => { | ||
const durationThreshold = 60; | ||
const connectionsStats = await analyticSessionRepository | ||
.createQueryBuilder('analytic_session') | ||
.select('MIN(DISTINCT(analytic_session.duration))', 'minDuration') | ||
.addSelect('COUNT(DISTINCT uniqueId)', 'connectedClassroomsCount') | ||
.addSelect('(SELECT COUNT(DISTINCT id) FROM classroom)', 'registeredClassroomsCount') | ||
.addSelect('(SELECT COUNT(DISTINCT userId) FROM activity)', 'contributedClassroomsCount') | ||
.addSelect('MAX(DISTINCT(analytic_session.duration))', 'maxDuration') | ||
.addSelect('ROUND(AVG(CASE WHEN analytic_session.duration >= :minDuration THEN analytic_session.duration ELSE NULL END), 0)', 'averageDuration') | ||
.addSelect( | ||
`(SELECT duration FROM (SELECT duration, ROW_NUMBER() OVER (ORDER BY duration) AS medianIdx FROM (SELECT DISTINCT duration FROM analytic_session WHERE duration IS NOT NULL AND duration >= :minDuration) AS sub) AS d, (SELECT COUNT(DISTINCT duration) AS cnt FROM analytic_session WHERE duration IS NOT NULL AND duration >= :minDuration) AS total_count WHERE d.medianIdx = (total_count.cnt DIV 2))`, | ||
'medianDuration', | ||
) | ||
.addSelect('MIN(sub.occurrence_count)', 'minConnections') | ||
.addSelect('MAX(sub.occurrence_count)', 'maxConnections') | ||
.addSelect('ROUND(AVG(sub.occurrence_count), 0)', 'averageConnections') | ||
.addSelect( | ||
`(SELECT connection_count FROM (SELECT connection_count, ROW_NUMBER() OVER (ORDER BY connection_count) AS medianIdx FROM (SELECT COUNT(*) AS connection_count FROM analytic_session GROUP BY uniqueId) AS sub) AS d, (SELECT COUNT(*) AS cnt FROM (SELECT COUNT(*) AS connection_count FROM analytic_session GROUP BY uniqueId) AS sub_count) AS total_count WHERE d.medianIdx = (total_count.cnt DIV 2))`, | ||
'medianConnections', | ||
) | ||
.from((subQuery) => { | ||
return subQuery.subQuery().select('COUNT(*)', 'occurrence_count').from(AnalyticSession, 'analytic_session').groupBy('uniqueId'); | ||
}, 'sub') | ||
.where('analytic_session.duration >= :minDuration', { minDuration: durationThreshold }) | ||
.getRawOne(); | ||
|
||
for (const property in connectionsStats) { | ||
connectionsStats[property] = typeof connectionsStats[property] === 'string' ? parseInt(connectionsStats[property]) : connectionsStats[property]; | ||
} | ||
|
||
res.sendJSON(connectionsStats); | ||
statisticsController.get({ path: '/classrooms' }, async (_req, res) => { | ||
res.sendJSON({ | ||
classrooms: await getClassroomsInfos(), | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { NextFunction, Request, Response } from 'express'; | ||
|
||
import { TeamComment } from '../entities/teamComment'; | ||
import { UserType } from '../entities/user'; | ||
import { AppDataSource } from '../utils/data-source'; | ||
import { Controller } from './controller'; | ||
|
||
const teamCommentController = new Controller('/team-comments'); | ||
|
||
// --- Get all comments. --- | ||
teamCommentController.get({ path: '', userType: UserType.ADMIN }, async (req: Request, res: Response) => { | ||
const teamComments = await AppDataSource.getRepository(TeamComment).find(); | ||
res.sendJSON(teamComments); | ||
}); | ||
|
||
// --- Edit one comment. --- | ||
teamCommentController.put({ path: '/:commentId', userType: UserType.ADMIN }, async (req: Request, res: Response, next: NextFunction) => { | ||
const data = req.body; | ||
const id = parseInt(req.params.commentId, 10) ?? 0; | ||
const teamComment = await AppDataSource.getRepository(TeamComment).findOne({ where: { id } }); | ||
if (!teamComment) { | ||
next(); | ||
return; | ||
} | ||
|
||
const updatedTeamComment = new TeamComment(); | ||
updatedTeamComment.id = id; | ||
updatedTeamComment.text = data.text; | ||
await AppDataSource.getRepository(TeamComment).save(updatedTeamComment); | ||
res.sendJSON(updatedTeamComment); | ||
}); | ||
|
||
export { teamCommentController }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; | ||
|
||
import type { TeamCommentInterface, TeamCommentType } from '../../types/teamComment.type'; | ||
|
||
@Entity() | ||
export class TeamComment implements TeamCommentInterface { | ||
@PrimaryGeneratedColumn() | ||
public id: number; | ||
|
||
@Column({ type: 'tinyint' }) | ||
public type: TeamCommentType; | ||
|
||
@CreateDateColumn() | ||
public createDate: Date; | ||
|
||
@UpdateDateColumn() | ||
public updateDate: Date; | ||
|
||
@Column({ type: 'text' }) | ||
public text: string; | ||
} |
2 changes: 1 addition & 1 deletion
2
...s/1710427662589-AlterGameResponseTable.ts → ...s/1724946964958-AlterGameResponseTable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.