Skip to content

Feat/system admin #1108

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

Open
wants to merge 26 commits into
base: dev
Choose a base branch
from
Open

Feat/system admin #1108

wants to merge 26 commits into from

Conversation

jmgasper
Copy link
Collaborator

Related JIRA Ticket:

https://topcoder.atlassian.net/browse/PM-440
https://topcoder.atlassian.net/issues/PM-1076

What's in this PR?

Initial system admin app implementation

jmgasper and others added 26 commits April 15, 2025 14:42
Permission, Billing account, Review update
…6878

Topcoder Admin App - Marathon Match Functionality
Admin billing accounts: always set payment_terms_id = 1 when send update
@@ -136,14 +132,9 @@ export const adminRoutes: ReadonlyArray<PlatformRoute> = [
route: ':challengeId/manage-user',
},
{
element: <ManageResourcePage />,
element: <ManageSubmissionPage />,

Choose a reason for hiding this comment

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

The id for this route is still 'manage-resource', but the element has been changed to ManageSubmissionPage. Consider updating the id to reflect the new functionality, such as 'manage-submission'.

element: <AddResourcePage />,
id: 'add-resource',
route: ':challengeId/manage-resource/add',
route: ':challengeId/manage-submission',

Choose a reason for hiding this comment

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

The route for this entry has been updated to ':challengeId/manage-submission', but the id is still 'manage-resource'. Ensure that the id accurately describes the route's purpose, possibly changing it to 'manage-submission'.

@@ -14,6 +14,7 @@ import _ from 'lodash'
import classNames from 'classnames'
import moment from 'moment'

import { EnvironmentConfig } from '~/config'

Choose a reason for hiding this comment

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

The import statement for EnvironmentConfig is added, but it is not used in the current code. Consider removing it if it's not needed to avoid unnecessary imports.

@@ -55,8 +57,8 @@ export const BillingAccountNewPage: FC<Props> = (props: Props) => {
billingAccount,
}: useManageAddBillingAccountProps = useManageAddBillingAccount(accountId)
const pageTitle = useMemo(
() => (accountId ? 'Edit Billing Account' : 'New Billing Account'),
[accountId],
() => (isEdit ? 'Edit Billing Account' : 'New Billing Account'),

Choose a reason for hiding this comment

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

The variable isEdit is used here, but it is not clear from the diff if it is defined or how it is being set. Ensure that isEdit is properly defined and initialized in the component to avoid potential runtime errors.

@@ -81,7 +83,9 @@ export const BillingAccountNewPage: FC<Props> = (props: Props) => {
subscriptionNumber: '' as any,
},
mode: 'all',
resolver: yupResolver(formEditBillingAccountSchema),
resolver: yupResolver(
isEdit ? formEditBillingAccountSchema : formAddBillingAccountSchema,

Choose a reason for hiding this comment

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

The use of isEdit to determine the schema could be clarified by ensuring isEdit is well-defined and initialized before this point in the code. Verify that isEdit is correctly set based on the intended logic to avoid potential runtime errors.

@@ -80,7 +81,7 @@ export const fetchGroups = async (params: {
const result = await xhrGetAsync<UserGroup[]>(
`${EnvironmentConfig.API.V5}/groups?${qs.stringify(params)}`,
)
return result.map(adjustUserGroupResponse)
return _.orderBy(result.map(adjustUserGroupResponse), ['createdAt'], ['desc'])

Choose a reason for hiding this comment

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

Consider checking if result is not null or undefined before calling map on it to avoid potential runtime errors.

@@ -94,6 +95,16 @@ export const createGroup = async (data: FormAddGroup): Promise<UserGroup> => {
`${EnvironmentConfig.API.V5}/groups`,
data,
)

if (!result.updatedAt) {

Choose a reason for hiding this comment

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

Consider checking if result.createdAt is defined before assigning it to result.updatedAt to avoid potential errors if createdAt is undefined.

result.updatedAt = result.createdAt
}

if (!result.updatedBy) {

Choose a reason for hiding this comment

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

The hardcoded value '00000000' for updatedBy might not be appropriate for all cases. Consider using a more descriptive default value or handling this scenario differently.


if (!result.updatedBy) {
result.updatedBy = '00000000'
result.updatedByHandle = '00000000 (not found)'

Choose a reason for hiding this comment

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

The hardcoded value '00000000 (not found)' for updatedByHandle might not be appropriate for all cases. Consider using a more descriptive default value or handling this scenario differently.

): Promise<ApiV5ResponseSuccess> => {
for (const reviewSummationId of reviewSummationIds) {
// eslint-disable-next-line no-await-in-loop
await removeReviewSummation(reviewSummationId)

Choose a reason for hiding this comment

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

Using await inside a loop can lead to performance issues because it waits for each promise to resolve before proceeding to the next iteration. Consider using Promise.all to handle multiple asynchronous operations concurrently.

export const fetchSubmissionsOfChallenge = async (
challengeId: string,
): Promise<MemberSubmission[]> => {
if (!challengeId) {

Choose a reason for hiding this comment

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

Consider adding a type guard or validation for challengeId to ensure it is a valid string before proceeding with the API call.

*/
export const removeSubmission = async (
submissionId: string,
): Promise<ApiV5ResponseSuccess> => {

Choose a reason for hiding this comment

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

Consider adding a check to ensure submissionId is valid before making the API call to prevent potential errors.

submissions: MemberSubmission[]
} {
let maxFinalScore
= _.get(submissions[0], 'submissions[0]', {

Choose a reason for hiding this comment

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

The use of _.get(submissions[0], 'submissions[0]', { finalScore: 0 }).finalScore ?? 0 is a bit confusing. It seems like you are trying to get the finalScore of the first submission, but the use of submissions[0] twice is unclear. Consider simplifying or clarifying this logic.

if (pA === pB) {
const timeA = _.get(a, 'submissions[0].submittedDate')
const timeB = _.get(b, 'submissions[0].submittedDate')
return timeA.getTime() - timeB.getTime()

Choose a reason for hiding this comment

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

Ensure that timeA and timeB are valid Date objects before calling getTime() on them to avoid potential runtime errors.

if (pA === pB) {
const timeA = _.get(a, 'submissions[0].submittedDate')
const timeB = _.get(b, 'submissions[0].submittedDate')
return timeA.getTime() - timeB.getTime()

Choose a reason for hiding this comment

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

Ensure that timeA and timeB are valid Date objects before calling getTime() on them to avoid potential runtime errors.

let pB = _.get(b, 'submissions[0]', { finalScore: 0 }).finalScore
if (pA === undefined) pA = 0
if (pB === undefined) pB = 0
if (pA > 0) maxFinalScore = pA

Choose a reason for hiding this comment

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

The logic for updating maxFinalScore seems incorrect. It should only update maxFinalScore if the current score is greater than the existing maxFinalScore.

* @param decimal number of unit to fix
* @returns number
*/
function toAcurateFixed(num: number, decimal: number): string | undefined {

Choose a reason for hiding this comment

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

The function name toAcurateFixed seems to have a typo. Consider renaming it to toAccurateFixed for clarity.

return num
}

if (Number.isNaN(Number(num))) return num as number

Choose a reason for hiding this comment

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

The return statement return num as number might not be appropriate if num is a string that cannot be converted to a number. Consider returning undefined or handling this case differently to avoid unexpected behavior.

* @returns true if object is number
*/
export function checkIsNumberObject(numberObject: any): boolean {
return typeof numberObject === 'number'

Choose a reason for hiding this comment

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

The function checkIsNumberObject checks if the input is of type number, but the name suggests it checks for a number object. Consider renaming the function to checkIsNumber for clarity.

export function checkIsStringNumeric(str: string): boolean {
if (typeof str !== 'string') return false // we only process strings!
return (
!Number.isNaN(str as any)

Choose a reason for hiding this comment

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

The use of str as any is unnecessary and can be avoided. Consider using Number.isNaN(Number(str)) instead for clarity.

if (typeof str !== 'string') return false // we only process strings!
return (
!Number.isNaN(str as any)
&& !Number.isNaN(parseFloat(str)) // use type coercion to parse the

Choose a reason for hiding this comment

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

The comment about parseFloat should be moved to a more appropriate location or rephrased to clarify its relevance to the code. It currently interrupts the logic flow.

@@ -134,7 +106,7 @@ export function isValidNumber(value: number | undefined): boolean {
/**
* validation schema for form new billing account
*/
export const formEditBillingAccountSchema: Yup.ObjectSchema<FormEditBillingAccount>
export const formAddBillingAccountSchema: Yup.ObjectSchema<FormEditBillingAccount>

Choose a reason for hiding this comment

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

The schema name formAddBillingAccountSchema suggests it is for adding a billing account, but it is typed as FormEditBillingAccount. Ensure that the schema name and type are consistent with their intended use.

/**
* validation schema for form new billing account
*/
export const formEditBillingAccountSchema: Yup.ObjectSchema<FormEditBillingAccount>

Choose a reason for hiding this comment

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

The formEditBillingAccountSchema is defined twice in the code. Consider renaming the second instance to avoid confusion and potential errors.

@@ -152,7 +152,7 @@ export const PermissionGroupMembersPage: FC<Props> = (props: Props) => {
{(groupMembers[memberType] || [])
.length === 0 ? (
<p className={styles.noRecordFound}>
No members
No groups

Choose a reason for hiding this comment

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

The message 'No groups' may not accurately reflect the context if this section is intended to display members. Consider revising the message to better match the context of what is being displayed.

@@ -32,6 +32,7 @@ import {
} from '../../lib/models'
import {
approveApplication,
getChallengeByLegacyId,

Choose a reason for hiding this comment

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

The function getChallengeByLegacyId is imported but not used in this file. Consider removing it if it's not needed to avoid unnecessary imports.

@@ -139,6 +141,15 @@ export const ManageReviewerPage: FC = () => {
search()
}, [challengeId]) // eslint-disable-line react-hooks/exhaustive-deps -- missing dependency: search

// Gets the challenge details by legacyId
useEffect(() => {
getChallengeByLegacyId(+challengeId)

Choose a reason for hiding this comment

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

Consider adding error handling for the promise returned by getChallengeByLegacyId. This will help manage any potential errors that might occur during the API call.

useEffect(() => {
getChallengeByLegacyId(+challengeId)
.then(challenge => {
setChallengeUuid(challenge.id)

Choose a reason for hiding this comment

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

Ensure that challenge.id is always defined before calling setChallengeUuid. You might want to add a check to handle cases where challenge might be undefined or challenge.id is not present.

primary
onClick={handleRejectPendingConfirmDialog}
size='lg'
to={`${rootRoute}/challenge-management/${challengeUuid}/manage-user`}

Choose a reason for hiding this comment

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

The to prop in the LinkButton component should be checked to ensure that the URL is correctly constructed and does not lead to any broken links. Consider validating the challengeUuid variable to ensure it is defined and correctly formatted.

CONNECT_URL: 'https://connect.topcoder-dev.com',
DEFAULT_PAYMENT_TERMS: 1,

Choose a reason for hiding this comment

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

Consider clarifying the purpose of DEFAULT_PAYMENT_TERMS. If it represents a duration, specifying the unit (e.g., days, weeks) could improve readability and prevent potential misinterpretation.

@@ -45,8 +45,11 @@ export interface GlobalConfig {
},
ADMIN: {
CONNECT_URL: string
DEFAULT_PAYMENT_TERMS: number

Choose a reason for hiding this comment

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

Consider providing a more descriptive name for DEFAULT_PAYMENT_TERMS to clarify what the number represents, such as DEFAULT_PAYMENT_TERMS_DAYS if it refers to days.

DIRECT_URL: string
WORK_MANAGER_URL: string
ONLINE_REVIEW_URL: string
CHALLENGE_URL: string
AV_SCAN_SCORER_REVIEW_TYPE_ID: string

Choose a reason for hiding this comment

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

Ensure that CHALLENGE_URL is consistent with other URL naming conventions in the codebase, such as using CHALLENGE_MANAGER_URL if it aligns better with existing patterns.

DIRECT_URL: string
WORK_MANAGER_URL: string
ONLINE_REVIEW_URL: string
CHALLENGE_URL: string
AV_SCAN_SCORER_REVIEW_TYPE_ID: string
}

Choose a reason for hiding this comment

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

The AV_SCAN_SCORER_REVIEW_TYPE_ID could benefit from a more descriptive name to indicate what the ID represents or is used for, especially if it is a specific type of review ID.

CONNECT_URL: 'https://connect.topcoder.com',
DEFAULT_PAYMENT_TERMS: 1,

Choose a reason for hiding this comment

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

The DEFAULT_PAYMENT_TERMS value is set to 1, which might not be self-explanatory. Ensure that this value is correct and consider if it should be configurable or documented elsewhere.

@@ -30,6 +34,9 @@ interface TableProps<T> {
readonly onRowClick?: (data: T) => void
readonly onToggleSort?: (sort: Sort) => void
readonly removeDefaultSort?: boolean
readonly colWidth?: colWidthType | undefined,

Choose a reason for hiding this comment

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

Consider renaming colWidth to columnWidth for clarity and consistency with naming conventions.

@@ -30,6 +34,9 @@ interface TableProps<T> {
readonly onRowClick?: (data: T) => void
readonly onToggleSort?: (sort: Sort) => void
readonly removeDefaultSort?: boolean
readonly colWidth?: colWidthType | undefined,
readonly setColWidth?: Dispatch<SetStateAction<colWidthType>> | undefined

Choose a reason for hiding this comment

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

The setColWidth prop could be renamed to setColumnWidth to match the suggested change for colWidth and improve readability.

@@ -39,6 +46,11 @@ interface DefaultSortDirectionMap {

const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) => JSX.Element
= <T extends { [propertyName: string]: any }>(props: TableProps<T>) => {
const { width: screenWidth }: WindowSize = useWindowSize()

Choose a reason for hiding this comment

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

Consider destructuring useWindowSize() directly in the function parameters if screenWidth is the only property used, to improve readability and reduce unnecessary variable declarations.

const { width: screenWidth }: WindowSize = useWindowSize()
const tableRef = useRef<HTMLTableElement>(null)
const screenWidthBkRef = useRef<number>(0)
const colWidthInput = props.colWidth

Choose a reason for hiding this comment

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

Ensure colWidthInput is used appropriately in the component. If it's not used, consider removing it to avoid unused variable declarations.

const tableRef = useRef<HTMLTableElement>(null)
const screenWidthBkRef = useRef<number>(0)
const colWidthInput = props.colWidth
const setColWidth = props.setColWidth

Choose a reason for hiding this comment

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

Ensure setColWidth is used appropriately in the component. If it's not used, consider removing it to avoid unused variable declarations.

@@ -62,6 +74,52 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
const [sortedData, setSortedData]: [ReadonlyArray<T>, Dispatch<SetStateAction<ReadonlyArray<T>>>]
= useState<ReadonlyArray<T>>(props.data)

useEffect(() => {
if (_.isEmpty(colWidthInput) && colWidthInput) {

Choose a reason for hiding this comment

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

The condition _.isEmpty(colWidthInput) && colWidthInput seems contradictory. If colWidthInput is empty, the second part of the condition will always be false. Consider revising the logic to ensure it behaves as intended.

@@ -62,6 +74,52 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
const [sortedData, setSortedData]: [ReadonlyArray<T>, Dispatch<SetStateAction<ReadonlyArray<T>>>]
= useState<ReadonlyArray<T>>(props.data)

useEffect(() => {
if (_.isEmpty(colWidthInput) && colWidthInput) {
setTimeout(() => {

Choose a reason for hiding this comment

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

Using setTimeout with a fixed delay of 10 milliseconds may lead to unpredictable behavior depending on the execution environment. Consider using requestAnimationFrame or another method to ensure the timing is appropriate.

}
}, 10)
}
// eslint-disable-next-line react-hooks/exhaustive-deps

Choose a reason for hiding this comment

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

The use of // eslint-disable-next-line react-hooks/exhaustive-deps indicates that the dependencies of the useEffect hook might be incomplete. Consider reviewing the dependencies to ensure all necessary variables are included.

return // do not reset col width on init
}

if (!_.isEmpty(colWidthInput) && screenWidthBkRef.current !== screenWidth) {

Choose a reason for hiding this comment

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

The condition !_.isEmpty(colWidthInput) is used here, but it seems inconsistent with the previous useEffect where the opposite condition is checked. Ensure the logic aligns with the intended behavior.

}

screenWidthBkRef.current = screenWidth
// eslint-disable-next-line react-hooks/exhaustive-deps

Choose a reason for hiding this comment

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

The use of // eslint-disable-next-line react-hooks/exhaustive-deps suggests potential issues with the dependencies of the useEffect hook. Review the dependencies to ensure all necessary variables are accounted for.

return (
<th
className={styles.th}
className={classNames(styles.th, columnId)}

Choose a reason for hiding this comment

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

The columnId variable is being used directly as a class name. Ensure that columnId does not contain any special characters that could interfere with CSS class naming conventions.

key={getKey(index)}
>
<div
className={
classNames(styles['header-container'], styles[col.type], colorClass, sortableClass)
}
style={colWidth ? { width: `${colWidth}px` } : {}}

Choose a reason for hiding this comment

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

The inline style for width is being applied conditionally. Consider verifying that colWidth is a valid number before using it to set the width, to prevent potential rendering issues.

@@ -182,14 +243,15 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
columns={props.columns}
index={index}
showExpand={props.showExpand}
colWidth={colWidthInput}

Choose a reason for hiding this comment

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

Consider verifying if colWidthInput is correctly defined and passed to the Row component, as it seems to be a new addition. Ensure it aligns with the expected data type and usage within the component.

preventDefault={props.preventDefault}
/>
))

return (
/* TODO: sticky header */
<div className={styles['table-wrap']}>
<table className={styles.table}>
<div className={classNames(styles['table-wrap'], props.className)}>

Choose a reason for hiding this comment

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

The addition of props.className using classNames is a good approach for dynamic styling. However, ensure that props.className is validated and does not conflict with existing styles in styles['table-wrap'].

<div className={styles['table-wrap']}>
<table className={styles.table}>
<div className={classNames(styles['table-wrap'], props.className)}>
<table ref={tableRef} className={styles.table}>

Choose a reason for hiding this comment

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

The tableRef is introduced here. Ensure that it is properly initialized and used within the component logic, especially if it is intended for DOM manipulation or other operations.

@@ -93,6 +94,7 @@ const TableCell: <T extends { [propertyName: string]: any }>(
'TableCell_blockCell',
)}
onClick={onClick}
style={props.style}

Choose a reason for hiding this comment

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

Consider verifying if props.style is always defined or if there should be a default value to prevent potential rendering issues when props.style is undefined.

/>
))
const cells: Array<JSX.Element> = displayColumns.map((col, colIndex) => {
const columnId = `column-id-${col.columnId}-`

Choose a reason for hiding this comment

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

Consider checking if col.columnId is defined before using it to construct columnId. This will prevent potential errors if col.columnId is undefined or null.

))
const cells: Array<JSX.Element> = displayColumns.map((col, colIndex) => {
const columnId = `column-id-${col.columnId}-`
const colWidth = props.colWidth?.[columnId]

Choose a reason for hiding this comment

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

Ensure that props.colWidth is properly defined and contains valid values for columnId. If props.colWidth is undefined or does not contain the expected keys, this could lead to unexpected behavior.

col.className,
'TableCell_blockExpandValue',
)}
style={colWidth ? { width: `${colWidth}px` } : {}}

Choose a reason for hiding this comment

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

Consider verifying if colWidth is a valid number before using it in the style attribute to prevent potential rendering issues. You might want to ensure that colWidth is not NaN or negative.

@@ -34,7 +36,7 @@ const TableSort: FC<TableSortProps> = (props: TableSortProps) => {

return (
<Button
className={classNames(props.iconClass, 'TableSort')}
className={classNames(props.iconClass, 'TableSort', styles.btnSort)}

Choose a reason for hiding this comment

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

Ensure that styles.btnSort is defined and imported correctly. If it's not defined, it could lead to runtime errors.

}

export function useWindowSize(): WindowSize {
const [windowSize, setWindowSize]: [WindowSize, Dispatch<SetStateAction<WindowSize>>] = useState({

Choose a reason for hiding this comment

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

Consider initializing windowSize with the actual current window dimensions instead of zeros to avoid an unnecessary initial resize event.


// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize)
}, [handleResize]) // Empty array ensures that effect is only run on mount

Choose a reason for hiding this comment

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

The comment 'Empty array ensures that effect is only run on mount' is misleading because the dependency array is not empty; it contains handleResize. Consider rephrasing or removing the comment.

@@ -17439,7 +17439,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:

Choose a reason for hiding this comment

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

The addition of multiple aliases for strip-ansi seems unnecessary unless there is a specific reason for supporting multiple versions. Consider consolidating to a single version if possible to avoid potential conflicts or confusion.

@kkartunov kkartunov self-requested a review June 17, 2025 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants