diff --git a/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx b/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx index 7cf3d63e42..bd8afb879d 100644 --- a/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx @@ -1,5 +1,4 @@ -import type { VFC } from 'react'; -import type { ChangeRequestType } from '../changeRequest.types'; +import type { FC } from 'react'; import { Badge } from 'component/common/Badge/Badge'; import AccessTime from '@mui/icons-material/AccessTime'; import Check from '@mui/icons-material/Check'; @@ -9,20 +8,27 @@ import ErrorIcon from '@mui/icons-material/Error'; import PauseCircle from '@mui/icons-material/PauseCircle'; import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; import { useLocationSettings } from 'hooks/useLocationSettings'; +import type { + ScheduledChangeRequest, + UnscheduledChangeRequest, +} from '../changeRequest.types'; -interface IChangeRequestStatusBadgeProps { - changeRequest: ChangeRequestType | undefined; +export interface IChangeRequestStatusBadgeProps { + changeRequest: + | Pick + | Pick + | undefined; } -const ReviewRequiredBadge: VFC = () => ( +const ReviewRequiredBadge: FC = () => ( }> Review required ); -const DraftBadge: VFC = () => Draft; +const DraftBadge: FC = () => Draft; -export const ChangeRequestStatusBadge: VFC = ({ +export const ChangeRequestStatusBadge: FC = ({ changeRequest, }) => { const { locationSettings } = useLocationSettings(); diff --git a/frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx b/frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx index b8953e025b..f08a3ba2c0 100644 --- a/frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx +++ b/frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx @@ -1,285 +1,174 @@ import { useMemo } from 'react'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { - SortableTableHeader, - Table, - TableBody, - TableCell, - TableRow, -} from 'component/common/Table'; -import { useSortBy, useTable } from 'react-table'; -import { sortTypes } from 'utils/sortTypes'; +import { PaginatedTable } from 'component/common/Table'; +import { createColumnHelper, useReactTable } from '@tanstack/react-table'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; -import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { ChangeRequestStatusCell } from 'component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestStatusCell'; import { AvatarCell } from 'component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/AvatarCell'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import { GlobalChangeRequestTitleCell } from './GlobalChangeRequestTitleCell.js'; import { FeaturesCell } from '../ProjectChangeRequests/ChangeRequestsTabs/FeaturesCell.js'; import { useUiFlag } from 'hooks/useUiFlag.js'; +import { withTableState } from 'utils/withTableState'; +import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; +import { + useChangeRequestSearch, + DEFAULT_PAGE_LIMIT, + type SearchChangeRequestsInput, +} from 'hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch'; +import type { ChangeRequestSearchItemSchema } from 'openapi'; +import { + NumberParam, + StringParam, + withDefault, + useQueryParams, + encodeQueryParams, +} from 'use-query-params'; +import useLoading from 'hooks/useLoading'; +import { styles as themeStyles } from 'component/common'; +import { FilterItemParam } from 'utils/serializeQueryParams'; -// Mock data with varied projects and change requests -const mockChangeRequests = [ - { - id: 101, - title: 'Activate harpoons', - project: 'payment-service', - projectName: 'Payment Service', - features: [{ name: 'securePaymentFlow' }], - segments: [], - createdBy: { username: 'alice', name: 'Alice Johnson', imageUrl: null }, - createdAt: '2024-01-10T10:22:00Z', - environment: 'Production', - state: 'Review required', - }, - { - id: 102, - title: 'change request #102', - project: 'user-management', - projectName: 'User Management', - features: [{ name: 'enhancedValidation' }], - segments: [], - createdBy: { username: 'bob', name: 'Bob Smith', imageUrl: null }, - createdAt: '2024-01-10T08:15:00Z', - environment: 'Production', - state: 'Approved', - }, - { - id: 103, - title: 'Enable new checkout flow', - project: 'e-commerce-platform', - projectName: 'E-commerce Platform', - features: [{ name: 'newCheckoutUX' }, { name: 'paymentOptionsV2' }], - segments: [], - createdBy: { username: 'carol', name: 'Carol Davis', imageUrl: null }, - createdAt: '2024-01-10T12:30:00Z', - environment: 'Testing', - state: 'Review required', - }, - { - id: 104, - title: 'Update user permissions', - project: 'user-management', - projectName: 'User Management', - features: [ - { name: 'roleBasedAccess' }, - { name: 'permissionMatrix' }, - { name: 'adminDashboard' }, - ], - segments: [], - createdBy: { username: 'david', name: 'David Wilson', imageUrl: null }, - createdAt: '2024-01-09T16:45:00Z', - environment: 'Sandbox', - state: 'Review required', - }, - { - id: 105, - title: 'Deploy feature rollback', - project: 'analytics-platform', - projectName: 'Analytics Platform', - features: [ - { name: 'performanceTracking' }, - { name: 'realTimeAnalytics' }, - { name: 'customDashboards' }, - { name: 'dataExport' }, - ], - segments: [], - createdBy: { username: 'eve', name: 'Eve Brown', imageUrl: null }, - createdAt: '2024-01-09T14:20:00Z', - environment: 'Sandbox', - state: 'Scheduled', - schedule: { - scheduledAt: '2024-01-12T09:46:51+05:30', - status: 'pending', - }, - }, - { - id: 106, - title: 'change request #106', - project: 'notification-service', - projectName: 'Notification Service', - features: [{ name: 'emailTemplates' }], - segments: [], - createdBy: { username: 'frank', name: 'Frank Miller', imageUrl: null }, - createdAt: '2024-01-08T11:00:00Z', - environment: 'Testing', - state: 'Approved', - }, - { - id: 107, - title: 'Optimize database queries', - project: 'data-warehouse', - projectName: 'Data Warehouse', - features: [{ name: 'queryOptimization' }], - segments: [], - createdBy: { username: 'grace', name: 'Grace Lee', imageUrl: null }, - createdAt: '2024-01-08T09:30:00Z', - environment: 'Testing', - state: 'Approved', - }, - { - id: 108, - title: 'change request #108', - project: 'mobile-app', - projectName: 'Mobile App', - features: [{ name: 'pushNotifications' }], - segments: [], - createdBy: { username: 'henry', name: 'Henry Chen', imageUrl: null }, - createdAt: '2024-01-07T15:20:00Z', - environment: 'Production', - state: 'Approved', - }, - { - id: 109, - title: 'Archive legacy features', - project: 'payment-service', - projectName: 'Payment Service', - features: [{ name: 'legacyPaymentGateway' }], - segments: [], - createdBy: { username: 'alice', name: 'Alice Johnson', imageUrl: null }, - createdAt: '2024-01-07T13:10:00Z', - environment: 'Production', - state: 'Scheduled', - schedule: { - scheduledAt: '2024-01-12T09:46:51+05:30', - status: 'failed', - reason: 'Mr Freeze', - }, - }, -]; +const columnHelper = createColumnHelper(); const ChangeRequestsInner = () => { - const loading = false; + const { user } = useAuthUser(); + + const shouldApplyDefaults = useMemo(() => { + const urlParams = new URLSearchParams(window.location.search); + return ( + !urlParams.has('createdBy') && + !urlParams.has('requestedApproverId') && + user + ); + }, [user]); + + const stateConfig = { + offset: withDefault(NumberParam, 0), + limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), + sortBy: withDefault(StringParam, 'createdAt'), + sortOrder: withDefault(StringParam, 'desc'), + createdBy: FilterItemParam, + requestedApproverId: FilterItemParam, + }; + + const initialState = shouldApplyDefaults + ? { + createdBy: { + operator: 'IS' as const, + values: user ? [user.id.toString()] : [], + }, + } + : {}; + + const [tableState, setTableState] = useQueryParams(stateConfig, { + updateType: 'replaceIn', + }); + + const effectiveTableState = useMemo( + () => ({ + ...initialState, + ...tableState, + }), + [initialState, tableState], + ); + + const { + changeRequests: data, + total, + loading, + } = useChangeRequestSearch( + encodeQueryParams( + stateConfig, + effectiveTableState, + ) as SearchChangeRequestsInput, + ); + const columns = useMemo( () => [ - { + columnHelper.accessor('title', { id: 'Title', - Header: 'Title', - // todo (globalChangeRequestList): sort out width calculation. It's configured both here with a min width down in the inner cell? - width: 300, - canSort: true, - accessor: 'title', - Cell: GlobalChangeRequestTitleCell, - }, - { + header: 'Title', + meta: { width: '300px' }, + cell: ({ getValue, row }) => ( + + ), + }), + columnHelper.accessor('features', { id: 'Updated feature flags', - Header: 'Updated feature flags', - canSort: false, - accessor: 'features', - searchable: true, - filterName: 'feature', - filterParsing: (values: Array<{ name: string }>) => { - return values?.map(({ name }) => name).join('\n') || ''; - }, - filterBy: ( - row: { features: Array<{ name: string }> }, - values: Array, - ) => { - return row.features.find((feature) => - values - .map((value) => value.toLowerCase()) - .includes(feature.name.toLowerCase()), - ); - }, - Cell: ({ - value, + header: 'Updated feature flags', + enableSorting: false, + cell: ({ + getValue, row: { original: { title, project }, }, - }: any) => ( - + }) => { + const features = getValue(); + const featureObjects = features.map((name: string) => ({ + name, + })); + return ( + + ); + }, + }), + columnHelper.accessor('createdBy', { + id: 'By', + header: 'By', + meta: { width: '180px', align: 'left' }, + enableSorting: false, + cell: ({ getValue }) => , + }), + columnHelper.accessor('createdAt', { + id: 'Submitted', + header: 'Submitted', + meta: { width: '100px' }, + cell: ({ getValue }) => , + }), + columnHelper.accessor('environment', { + id: 'Environment', + header: 'Environment', + meta: { width: '100px' }, + cell: ({ getValue }) => , + }), + columnHelper.accessor('state', { + id: 'Status', + header: 'Status', + meta: { width: '170px' }, + cell: ({ getValue, row }) => ( + // @ts-expect-error (`globalChangeRequestList`) The schema (and query) needs to be updated + ), - }, - { - Header: 'By', - accessor: 'createdBy', - maxWidth: 180, - canSort: false, - Cell: AvatarCell, - align: 'left', - searchable: true, - filterName: 'by', - filterParsing: (value: { username?: string }) => - value?.username || '', - }, - { - Header: 'Submitted', - accessor: 'createdAt', - maxWidth: 100, - Cell: TimeAgoCell, - }, - { - Header: 'Environment', - accessor: 'environment', - searchable: true, - maxWidth: 100, - Cell: HighlightCell, - filterName: 'environment', - }, - { - Header: 'Status', - accessor: 'state', - searchable: true, - maxWidth: '170px', - Cell: ChangeRequestStatusCell, - filterName: 'status', - }, + }), ], [], ); - const { headerGroups, rows, prepareRow, getTableProps, getTableBodyProps } = - useTable( - { - columns: columns as any[], - data: mockChangeRequests, - initialState: { - sortBy: [ - { - id: 'createdAt', - desc: true, - }, - ], - }, - sortTypes, - autoResetHiddenColumns: false, - disableSortRemove: true, - autoResetSortBy: false, - defaultColumn: { - Cell: TextCell, - }, - }, - useSortBy, - ); + const table = useReactTable( + withTableState(effectiveTableState, setTableState, { + columns, + data, + }), + ); + + const bodyLoadingRef = useLoading(loading); return ( } > - - - - {rows.map((row) => { - prepareRow(row); - const { key, ...rowProps } = row.getRowProps(); - return ( - - {row.cells.map((cell) => { - const { key, ...cellProps } = - cell.getCellProps(); - return ( - - {cell.render('Cell')} - - ); - })} - - ); - })} - -
+
+ +
); }; diff --git a/frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx b/frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx index 678c2a0918..17f0b1275a 100644 --- a/frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx +++ b/frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx @@ -1,6 +1,7 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { Link, styled, Typography } from '@mui/material'; import { Link as RouterLink, type LinkProps } from 'react-router-dom'; +import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview'; type IGlobalChangeRequestTitleCellProps = { value?: any; @@ -41,12 +42,12 @@ export const GlobalChangeRequestTitleCell = ({ id, title, project, - projectName, features: featureChanges, segments: segmentChanges, } = original; + const projectName = useProjectOverviewNameOrId(project); const totalChanges = - (featureChanges || []).length + (segmentChanges || []).length; + featureChanges?.length ?? 0 + segmentChanges?.length ?? 0; const projectPath = `/projects/${project}`; const crPath = `${projectPath}/change-requests/${id}`; diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestStatusCell.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestStatusCell.tsx index 734a3f1fcc..84f7bc2618 100644 --- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestStatusCell.tsx +++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestStatusCell.tsx @@ -1,14 +1,18 @@ -import type { VFC } from 'react'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; -import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types'; -import { ChangeRequestStatusBadge } from 'component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge'; +import { + ChangeRequestStatusBadge, + type IChangeRequestStatusBadgeProps, +} from 'component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge'; +import type { FC } from 'react'; interface IChangeRequestStatusCellProps { value?: string | null; // FIXME: proper type - row: { original: ChangeRequestType }; + row: { + original: IChangeRequestStatusBadgeProps['changeRequest']; + }; } -export const ChangeRequestStatusCell: VFC = ({ +export const ChangeRequestStatusCell: FC = ({ value, row: { original }, }) => { diff --git a/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts index fca192d487..3ac7d4966c 100644 --- a/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts +++ b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts @@ -31,6 +31,12 @@ const fallbackData: ChangeRequestSearchResponseSchema = { const SWR_CACHE_SIZE = 10; const PATH = 'api/admin/search/change-requests?'; +export type SearchChangeRequestsInput = { + [K in keyof SearchChangeRequestsParams]?: + | SearchChangeRequestsParams[K] + | null; +}; + const createChangeRequestSearch = () => { const internalCache: InternalCache = {}; @@ -56,7 +62,7 @@ const createChangeRequestSearch = () => { }; return ( - params: SearchChangeRequestsParams, + params: SearchChangeRequestsInput, options: SWRConfiguration = {}, cachePrefix: string = '', ): UseChangeRequestSearchOutput => { @@ -100,11 +106,13 @@ const createChangeRequestSearch = () => { export const DEFAULT_PAGE_LIMIT = 25; -const getChangeRequestSearchFetcher = (params: SearchChangeRequestsParams) => { +const getChangeRequestSearchFetcher = (params: SearchChangeRequestsInput) => { const urlSearchParams = new URLSearchParams( Array.from( Object.entries(params) - .filter(([_, value]) => !!value) + .filter( + (param): param is [string, string | number] => !!param[1], + ) .map(([key, value]) => [key, value.toString()]), ), ).toString();