-
Notifications
You must be signed in to change notification settings - Fork 23
PROD - Reports portal deploy and lots of small fixes #1538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f0839d4
78a9b48
1bd0f02
61fdca1
eee8edf
166144b
4db3c1b
39388d9
000b697
394ce8a
dcca61b
866d600
5b57cfb
6478d10
b5917f0
3acb31e
a5f8a12
53181db
afe9c36
06cc95c
0574d01
67d03b7
cd5f83c
a83ce50
04f6157
cf47e7c
8feee49
682e619
cf5d97e
60125ea
64332d7
15e3c42
2bc2aff
4ebc8e4
9c05037
c151b62
b04348e
2a2921a
022306f
6bc93a4
145bca3
1882714
9caa8c1
b7ff0e5
6294433
63e2e6b
8019000
698747a
10f5899
a5aede1
90eea56
8b762a5
6383f1e
4854149
b347d15
a64c13b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { FC } from 'react' | ||
| import { Navigate } from 'react-router-dom' | ||
|
|
||
| import { reportsRootRoute } from '~/apps/reports' | ||
| import { ProfileContextData, useProfileContext } from '~/libs/core' | ||
|
|
||
| import { manageChallengeRouteId } from './config/routes.config' | ||
| import { isAdministrator } from './lib/utils' | ||
|
|
||
| /** | ||
| * Redirects authenticated admin-app users to the first route they can access. | ||
| */ | ||
| const AdminHomeRedirect: FC = () => { | ||
| const { profile }: ProfileContextData = useProfileContext() | ||
| const defaultRoute: string = isAdministrator(profile?.roles) | ||
| ? manageChallengeRouteId | ||
| : reportsRootRoute | ||
|
|
||
| return <Navigate replace to={defaultRoute} /> | ||
| } | ||
|
|
||
| export default AdminHomeRedirect | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,6 @@ import { | |
| lazyLoad, | ||
| LazyLoadedComponent, | ||
| PlatformRoute, | ||
| Rewrite, | ||
| UserRole, | ||
| } from '~/libs/core' | ||
|
|
||
| import { | ||
|
|
@@ -17,12 +15,13 @@ import { | |
| paymentsRouteId, | ||
| permissionManagementRouteId, | ||
| platformRouteId, | ||
| reportsRouteId, | ||
| rootRoute, | ||
| termsRouteId, | ||
| userManagementRouteId, | ||
| } from './config/routes.config' | ||
| import { administratorOnlyRoles, adminReportsAccessRoles } from './lib/utils' | ||
| import { platformSkillRouteId } from './platform/routes.config' | ||
| import AdminHomeRedirect from './AdminHomeRedirect' | ||
|
|
||
| const AdminApp: LazyLoadedComponent = lazyLoad(() => import('./AdminApp')) | ||
|
|
||
|
|
@@ -173,10 +172,6 @@ const PaymentsPage: LazyLoadedComponent = lazyLoad( | |
| () => import('./payments/PaymentsPage'), | ||
| 'PaymentsPage', | ||
| ) | ||
| const ReportsPage: LazyLoadedComponent = lazyLoad( | ||
| () => import('./reports/ReportsPage'), | ||
| 'ReportsPage', | ||
| ) | ||
|
|
||
| export const toolTitle: string = ToolTitle.admin | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
|
|
||
|
|
@@ -186,7 +181,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [ | |
| authRequired: true, | ||
| children: [ | ||
| { | ||
| element: <Rewrite to={manageChallengeRouteId} />, | ||
| element: <AdminHomeRedirect />, | ||
| route: '', | ||
| }, | ||
| // Challenge Management Module | ||
|
|
@@ -220,12 +215,14 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [ | |
| ], | ||
| element: <ChallengeManagement />, | ||
| id: manageChallengeRouteId, | ||
| rolesRequired: administratorOnlyRoles, | ||
| route: manageChallengeRouteId, | ||
| }, | ||
| // User Management Module | ||
| { | ||
| element: <UserManagementPage />, | ||
| id: userManagementRouteId, | ||
| rolesRequired: administratorOnlyRoles, | ||
| route: userManagementRouteId, | ||
| }, | ||
| // Reviewer Management Module | ||
|
|
@@ -244,6 +241,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [ | |
| ], | ||
| element: <ReviewManagement />, | ||
| id: manageReviewRouteId, | ||
| rolesRequired: administratorOnlyRoles, | ||
| route: manageReviewRouteId, | ||
| }, | ||
| // Billing Account Module | ||
|
|
@@ -297,6 +295,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [ | |
| ], | ||
| element: <BillingAccount />, | ||
| id: billingAccountRouteId, | ||
| rolesRequired: administratorOnlyRoles, | ||
| route: billingAccountRouteId, | ||
| }, | ||
| // Permission Management Module | ||
|
|
@@ -335,6 +334,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [ | |
| ], | ||
| element: <PermissionManagement />, | ||
| id: permissionManagementRouteId, | ||
| rolesRequired: administratorOnlyRoles, | ||
| route: permissionManagementRouteId, | ||
| }, | ||
|
|
||
|
|
@@ -408,25 +408,21 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [ | |
| ], | ||
| element: <Platform />, | ||
| id: platformRouteId, | ||
| rolesRequired: administratorOnlyRoles, | ||
| route: platformRouteId, | ||
| }, | ||
| // Payments Module | ||
| { | ||
| element: <PaymentsPage />, | ||
| id: paymentsRouteId, | ||
| rolesRequired: administratorOnlyRoles, | ||
| route: paymentsRouteId, | ||
| }, | ||
| // Reports Module | ||
| { | ||
| element: <ReportsPage />, | ||
| id: reportsRouteId, | ||
| route: reportsRouteId, | ||
| }, | ||
| ], | ||
| domain: AppSubdomain.admin, | ||
| element: <AdminApp />, | ||
| id: toolTitle, | ||
| rolesRequired: [UserRole.administrator], | ||
| rolesRequired: adminReportsAccessRoles, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| route: rootRoute, | ||
| title: toolTitle, | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { | ||
| ChangeEvent, | ||
| FC, | ||
| MouseEvent, | ||
| useEffect, | ||
| useMemo, | ||
| useState, | ||
|
|
@@ -32,6 +33,10 @@ import { | |
| getResourceRoles, | ||
| updateChallengeById, | ||
| } from '../../lib/services' | ||
| import { | ||
| downloadBlobFile, | ||
| downloadReportAsCsv, | ||
| } from '../../lib/services/reports.service' | ||
| import { createChallengeQueryString, handleError } from '../../lib/utils' | ||
|
|
||
| import styles from './ChallengeDetailsPage.module.scss' | ||
|
|
@@ -62,6 +67,22 @@ type RouteState = { | |
|
|
||
| type WinnerUpdate = Pick<ChallengeWinner, 'handle' | 'placement' | 'userId'> | ||
|
|
||
| type ChallengeExportReportKey = | ||
| | 'registered-users' | ||
| | 'submitters' | ||
| | 'valid-submitters' | ||
| | 'winners' | ||
|
|
||
| const CHALLENGE_EXPORT_REPORTS: Array<{ | ||
| key: ChallengeExportReportKey | ||
| label: string | ||
| }> = [ | ||
| { key: 'registered-users', label: 'Registered Users' }, | ||
| { key: 'submitters', label: 'Submitters' }, | ||
| { key: 'valid-submitters', label: 'Valid Submitters' }, | ||
| { key: 'winners', label: 'Winners' }, | ||
| ] | ||
|
|
||
| function formatStatusLabel(rawStatus: string): string { | ||
| const normalized = rawStatus | ||
| .trim() | ||
|
|
@@ -188,6 +209,8 @@ export const ChallengeDetailsPage: FC = () => { | |
| const [isLoading, setIsLoading] = useState(false) | ||
| const [isSavingStatus, setIsSavingStatus] = useState(false) | ||
| const [isSavingWinners, setIsSavingWinners] = useState(false) | ||
| const [downloadingReportKey, setDownloadingReportKey] | ||
| = useState<ChallengeExportReportKey | undefined>() | ||
| const [isLoadingSubmitters, setIsLoadingSubmitters] = useState(false) | ||
| const [submitterOptions, setSubmitterOptions] = useState<InputSelectOption[]>([ | ||
| { label: 'Select submitter', value: '' }, | ||
|
|
@@ -328,6 +351,7 @@ export const ChallengeDetailsPage: FC = () => { | |
| }, [routeState.previousChallengeListFilter]) | ||
|
|
||
| const pageTitle = challengeInfo?.name || 'Challenge Details' | ||
| const isMarathonMatch = challengeInfo?.type?.name === 'Marathon Match' | ||
| const currentWinnerHandleByUserId = useMemo( | ||
| () => Object.fromEntries( | ||
| (challengeInfo?.winners ?? []).map(winner => [`${winner.userId}`, winner.handle]), | ||
|
|
@@ -414,6 +438,37 @@ export const ChallengeDetailsPage: FC = () => { | |
| } | ||
| }) | ||
|
|
||
| const handleExportReport = useEventCallback(async (reportKey: ChallengeExportReportKey) => { | ||
| if (!challengeId) { | ||
| return | ||
| } | ||
|
|
||
| setDownloadingReportKey(reportKey) | ||
|
|
||
| try { | ||
| const path = `/challenges/${encodeURIComponent(challengeId)}/${reportKey}` | ||
| const blob = await downloadReportAsCsv(path) | ||
| const fileName = `challenge-${reportKey}_${challengeId}.csv` | ||
|
|
||
| downloadBlobFile(blob, fileName) | ||
| } catch (error) { | ||
| handleError(error) | ||
| } finally { | ||
| setDownloadingReportKey(undefined) | ||
| } | ||
| }) | ||
|
|
||
| const handleExportButtonClick = useEventCallback( | ||
| (event: MouseEvent<HTMLButtonElement>) => { | ||
| const reportKey = event.currentTarget.value as ChallengeExportReportKey | ||
| if (!reportKey) { | ||
| return | ||
| } | ||
|
|
||
| handleExportReport(reportKey) | ||
| }, | ||
| ) | ||
|
|
||
| return ( | ||
| <PageWrapper | ||
| pageTitle={pageTitle} | ||
|
|
@@ -457,6 +512,34 @@ export const ChallengeDetailsPage: FC = () => { | |
| )} | ||
| {!isLoading && challengeInfo && ( | ||
| <> | ||
| <section className={styles.section}> | ||
| <h4 className={styles.sectionTitle}>Exports</h4> | ||
| <p className={styles.exportDescription}> | ||
| Download challenge detail reports as CSV. | ||
| {isMarathonMatch && ( | ||
| ' Marathon Match submission-based exports include provisional ' | ||
| + 'score and final rank.' | ||
| )} | ||
| </p> | ||
| <div className={styles.exportActions}> | ||
| {CHALLENGE_EXPORT_REPORTS.map(report => ( | ||
| <Button | ||
| key={report.key} | ||
| secondary | ||
| size='lg' | ||
| className={styles.exportButton} | ||
| value={report.key} | ||
| disabled={downloadingReportKey !== undefined} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| onClick={handleExportButtonClick} | ||
| > | ||
| {downloadingReportKey === report.key | ||
| ? `Downloading ${report.label}…` | ||
| : `Export ${report.label}`} | ||
| </Button> | ||
| ))} | ||
| </div> | ||
| </section> | ||
|
|
||
| <section className={styles.section}> | ||
| <h4 className={styles.sectionTitle}>Status</h4> | ||
| <div className={styles.statusRow}> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,22 @@ | ||
| import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'react' | ||
| import { NavigateFunction, useLocation, useNavigate } from 'react-router-dom' | ||
|
|
||
| import { ProfileContextData, useProfileContext } from '~/libs/core' | ||
| import { TabsNavbar } from '~/libs/ui' | ||
|
|
||
| import { getTabIdFromPathName, SystemAdminTabsConfig } from './config' | ||
| import { getSystemAdminTabs, getTabIdFromPathName } from './config' | ||
| import styles from './SystemAdminTabs.module.scss' | ||
|
|
||
| const SystemAdminTabs: FC = () => { | ||
| const navigate: NavigateFunction = useNavigate() | ||
| const { profile }: ProfileContextData = useProfileContext() | ||
|
|
||
| const { pathname }: { pathname: string } = useLocation() | ||
| const activeTabPathName: string = useMemo<string>(() => getTabIdFromPathName(pathname), [pathname]) | ||
| const tabs = useMemo(() => getSystemAdminTabs(profile?.roles), [profile?.roles]) | ||
| const activeTabPathName: string = useMemo<string>( | ||
| () => getTabIdFromPathName(pathname, tabs), | ||
| [pathname, tabs], | ||
| ) | ||
| const [activeTab, setActiveTab]: [string, Dispatch<SetStateAction<string>>] | ||
| = useState<string>(activeTabPathName) | ||
|
|
||
|
|
@@ -26,17 +32,21 @@ const SystemAdminTabs: FC = () => { | |
|
|
||
| // If url is changed by navigator on different tabs, we need set activeTab | ||
| useEffect(() => { | ||
| const pathTabId = getTabIdFromPathName(pathname) | ||
| const pathTabId = getTabIdFromPathName(pathname, tabs) | ||
| if (pathTabId !== activeTab) { | ||
| setActiveTab(pathTabId) | ||
| } | ||
| }, [pathname]) // eslint-disable-line react-hooks/exhaustive-deps | ||
| }, [activeTab, pathname, tabs]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
|
|
||
| if (!tabs.length) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| return <></> | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.container}> | ||
| <TabsNavbar | ||
| defaultActive={activeTab} | ||
| tabs={SystemAdminTabsConfig} | ||
| tabs={tabs} | ||
| onChange={handleTabChange} | ||
| onChildChange={handleChildTabChange} | ||
| /> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[❗❗
correctness]Consider adding a null check for
profilebefore accessingprofile?.rolesto prevent potential runtime errors ifprofileisnullorundefined.