Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f0839d4
Initial reports portal
jmgasper Mar 5, 2026
78a9b48
Deploy reports
jmgasper Mar 5, 2026
1bd0f02
Remove project manager rights from reports portal
jmgasper Mar 5, 2026
61fdca1
PM-4075 Fix manager comment access
himaniraghav3 Mar 10, 2026
eee8edf
PM-3707
hentrymartin Mar 10, 2026
166144b
Merge pull request #1521 from topcoder-platform/reports
jmgasper Mar 10, 2026
4db3c1b
Better RBAC for reports access for PM / TM roles
jmgasper Mar 11, 2026
39388d9
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into dev
jmgasper Mar 11, 2026
000b697
Fix up for copilot issue
jmgasper Mar 11, 2026
394ce8a
Merge pull request #1518 from topcoder-platform/PM-4075
kkartunov Mar 11, 2026
dcca61b
PM-4075 Don't allow copilots to add/edit manager comments
himaniraghav3 Mar 11, 2026
866d600
PM-4182 #time 15min qa tweaks
vas3a Mar 11, 2026
5b57cfb
Merge pull request #1524 from topcoder-platform/PM-4075
himaniraghav3 Mar 11, 2026
6478d10
PM-4198 #time 15m qa feedback
vas3a Mar 11, 2026
b5917f0
Merge pull request #1525 from topcoder-platform/PM-4182_open-to-work-…
vas3a Mar 11, 2026
3acb31e
lint
vas3a Mar 11, 2026
a5f8a12
Merge pull request #1526 from topcoder-platform/PM-4198_customer-port…
vas3a Mar 11, 2026
53181db
fix: lint
hentrymartin Mar 11, 2026
afe9c36
fix: review comments
hentrymartin Mar 11, 2026
06cc95c
PM-4288 #time 3h implemented open to work filter and showing it in th…
hentrymartin Mar 11, 2026
0574d01
rearranged the columns
hentrymartin Mar 11, 2026
67d03b7
fix: lint
hentrymartin Mar 11, 2026
cd5f83c
Date validation
jmgasper Mar 12, 2026
a83ce50
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into dev
jmgasper Mar 12, 2026
04f6157
Merge pull request #1527 from topcoder-platform/pm-4288
kkartunov Mar 12, 2026
cf47e7c
Merge pull request #1520 from topcoder-platform/pm-3707
kkartunov Mar 12, 2026
8feee49
PM-3907 - ai review status
vas3a Mar 12, 2026
682e619
lint
vas3a Mar 12, 2026
cf5d97e
Merge pull request #1528 from topcoder-platform/PM-3907_ai-review-sta…
kkartunov Mar 12, 2026
60125ea
Update Trivy action to use latest version
kkartunov Mar 12, 2026
64332d7
Update trivy.yaml
kkartunov Mar 12, 2026
15e3c42
PM-3907 #time 2h refactor layout, move "rerun" tooltip to button,
vas3a Mar 12, 2026
2bc2aff
lint
vas3a Mar 12, 2026
4ebc8e4
PM-3907 #time 15m pr feedback
vas3a Mar 12, 2026
9c05037
PM-4203 #time 4h implemented filter by skills in customer app
hentrymartin Mar 12, 2026
c151b62
fix: lint
hentrymartin Mar 12, 2026
b04348e
fix: lint
hentrymartin Mar 12, 2026
2a2921a
Merge pull request #1529 from topcoder-platform/PM-3907_ai-review-sta…
vas3a Mar 13, 2026
022306f
PM-4265 Add score info and gating indicator
himaniraghav3 Mar 13, 2026
6bc93a4
Merge branch 'dev' into PM-4265
himaniraghav3 Mar 13, 2026
145bca3
Show score info only if ai review config
himaniraghav3 Mar 13, 2026
1882714
PM-4265 Fix linting
himaniraghav3 Mar 13, 2026
9caa8c1
Merge pull request #1531 from topcoder-platform/PM-4265
kkartunov Mar 13, 2026
b7ff0e5
fix: modified the logic to display open to work column
hentrymartin Mar 13, 2026
6294433
Merge pull request #1532 from topcoder-platform/pm-4288_1
hentrymartin Mar 13, 2026
63e2e6b
fix: review comment
hentrymartin Mar 14, 2026
8019000
PM-4179 #time 1h find root cause and fix pagination in permission man…
hentrymartin Mar 14, 2026
698747a
PM-4267 #time 10m modified copy in engagements app
hentrymartin Mar 14, 2026
10f5899
Merge pull request #1534 from topcoder-platform/pm-4267
hentrymartin Mar 14, 2026
a5aede1
Merge pull request #1533 from topcoder-platform/pm-4179
kkartunov Mar 15, 2026
90eea56
Merge pull request #1530 from topcoder-platform/pm-4203
kkartunov Mar 15, 2026
8b762a5
Hotfix for PS-536 bad scorecard display
jmgasper Mar 15, 2026
6383f1e
Merge pull request #1536 from topcoder-platform/PS-536
jmgasper Mar 15, 2026
4854149
PM-4265 Add overall score, fix css
himaniraghav3 Mar 15, 2026
b347d15
PM-4265 Fix typos
himaniraghav3 Mar 15, 2026
a64c13b
Merge pull request #1537 from topcoder-platform/PM-4265
kkartunov Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ workflows:
- mm-final-2025-reveal
- engagements
- HOTFIX-PM-3269
- reports

- deployQa:
context: org-global
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/trivy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4

- name: Run Trivy scanner in repo mode
uses: aquasecurity/trivy-action@0.33.1
uses: aquasecurity/trivy-action@0.35.0
with:
scan-type: "fs"
ignore-unfixed: true
Expand Down
22 changes: 22 additions & 0 deletions src/apps/admin/src/AdminHomeRedirect.tsx
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()
Copy link
Copy Markdown

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 profile before accessing profile?.roles to prevent potential runtime errors if profile is null or undefined.

const defaultRoute: string = isAdministrator(profile?.roles)
? manageChallengeRouteId
: reportsRootRoute

return <Navigate replace to={defaultRoute} />
}

export default AdminHomeRedirect
26 changes: 11 additions & 15 deletions src/apps/admin/src/admin-app.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import {
lazyLoad,
LazyLoadedComponent,
PlatformRoute,
Rewrite,
UserRole,
} from '~/libs/core'

import {
Expand All @@ -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'))

Expand Down Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The ReportsPage component has been removed from the routes. Ensure that this is intentional and that there are no dependencies or features relying on this route.


Expand All @@ -186,7 +181,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [
authRequired: true,
children: [
{
element: <Rewrite to={manageChallengeRouteId} />,
element: <AdminHomeRedirect />,
route: '',
},
// Challenge Management Module
Expand Down Expand Up @@ -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
Expand All @@ -244,6 +241,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [
],
element: <ReviewManagement />,
id: manageReviewRouteId,
rolesRequired: administratorOnlyRoles,
route: manageReviewRouteId,
},
// Billing Account Module
Expand Down Expand Up @@ -297,6 +295,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [
],
element: <BillingAccount />,
id: billingAccountRouteId,
rolesRequired: administratorOnlyRoles,
route: billingAccountRouteId,
},
// Permission Management Module
Expand Down Expand Up @@ -335,6 +334,7 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [
],
element: <PermissionManagement />,
id: permissionManagementRouteId,
rolesRequired: administratorOnlyRoles,
route: permissionManagementRouteId,
},

Expand Down Expand Up @@ -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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
The rolesRequired for the root route has been changed from [UserRole.administrator] to adminReportsAccessRoles. Verify that adminReportsAccessRoles includes all necessary roles and that this change aligns with the intended access control policy.

route: rootRoute,
title: toolTitle,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@
gap: $sp-3;
}

.exportDescription {
margin: 0;
color: #555;
}

.exportActions {
display: flex;
flex-wrap: wrap;
gap: $sp-3;
}

.exportButton {
min-width: 220px;
}

.sectionTitle {
margin: 0;
font-size: 20px;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ChangeEvent,
FC,
MouseEvent,
useEffect,
useMemo,
useState,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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: '' },
Expand Down Expand Up @@ -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]),
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 design]
Disabling all export buttons when a report is downloading might not be necessary. Consider disabling only the button for the report currently being downloaded to improve user experience.

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}>
Expand Down
1 change: 0 additions & 1 deletion src/apps/admin/src/config/routes.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,3 @@ export const termsRouteId = 'terms'
export const defaultReviewersRouteId = 'default-reviewers'
export const platformRouteId = 'platform'
export const paymentsRouteId = 'payments'
export const reportsRouteId = 'reports'
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)

Expand All @@ -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])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
The dependency array for the useEffect hook now includes activeTab, pathname, and tabs. Ensure that this is intentional, as adding activeTab might cause unnecessary re-renders if activeTab changes due to the effect itself.


if (!tabs.length) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 design]
Returning an empty fragment when tabs is empty might be a missed opportunity to provide feedback to the user or handle this case more gracefully. Consider whether a loading state or an error message would be more appropriate.

return <></>
}

return (
<div className={styles.container}>
<TabsNavbar
defaultActive={activeTab}
tabs={SystemAdminTabsConfig}
tabs={tabs}
onChange={handleTabChange}
onChildChange={handleChildTabChange}
/>
Expand Down
Loading
Loading