From fabf76e12caf08659f32b922c8c83b63b96c6ff9 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 11 Sep 2025 09:15:57 +0200 Subject: [PATCH] feat: global change requests table (#10650) Adds basic table layout for the global change requests page and makes the page accessible at `/change-requests`. The table is based on the project-based change request table, but with a slightly different set of columns. Uses mock data for now. There's still some styling to be done for the column widths and handling narrower screens. image If the flag is disabled, we render nothing useful. image closes 1-4076 --- .../ChangeRequests/ChangeRequests.tsx | 297 ++++++++++++++++++ .../GlobalChangeRequestTitleCell.tsx | 69 ++++ frontend/src/component/menu/routes.ts | 13 + 3 files changed, 379 insertions(+) create mode 100644 frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx create mode 100644 frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx diff --git a/frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx b/frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx new file mode 100644 index 0000000000..b8953e025b --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequests/ChangeRequests.tsx @@ -0,0 +1,297 @@ +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 { 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'; + +// 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 ChangeRequestsInner = () => { + const loading = false; + const columns = useMemo( + () => [ + { + 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, + }, + { + 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, + row: { + original: { title, project }, + }, + }: any) => ( + + ), + }, + { + 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, + ); + + 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')} + + ); + })} + + ); + })} + +
+
+ ); +}; + +export const ChangeRequests = () => { + if (!useUiFlag('globalChangeRequestList')) { + return ( + }> +

Nothing to see here. Move along.

+
+ ); + } + + return ; +}; diff --git a/frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx b/frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx new file mode 100644 index 0000000000..678c2a0918 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequests/GlobalChangeRequestTitleCell.tsx @@ -0,0 +1,69 @@ +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'; + +type IGlobalChangeRequestTitleCellProps = { + value?: any; + row: { original: any }; +}; + +const LinkContainer = styled('div')(({ theme }) => ({ + color: theme.palette.text.secondary, +})); + +const BaseLink = styled(({ children, ...props }: LinkProps) => ( + + {children} + +))(({ theme }) => ({ + textDecoration: 'none', + color: 'inherit', + ':hover': { + textDecoration: 'underline', + }, +})); + +const ChangeRequestLink = styled(BaseLink)(({ theme }) => ({ + color: theme.palette.primary.main, + fontWeight: 'bold', +})); + +const UpdateText = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.secondary, + fontSize: theme.typography.body2.fontSize, +})); + +export const GlobalChangeRequestTitleCell = ({ + value, + row: { original }, +}: IGlobalChangeRequestTitleCellProps) => { + const { + id, + title, + project, + projectName, + features: featureChanges, + segments: segmentChanges, + } = original; + const totalChanges = + (featureChanges || []).length + (segmentChanges || []).length; + const projectPath = `/projects/${project}`; + const crPath = `${projectPath}/change-requests/${id}`; + + if (!value) { + return ; + } + + return ( + + + {projectName} + + {title} + + + {`${totalChanges}`} {totalChanges === 1 ? `update` : 'updates'} + + + ); +}; diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index c7450a6d35..aa859e02b0 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -53,6 +53,7 @@ import { CreateReleasePlanTemplate } from 'component/releases/ReleasePlanTemplat import { EditReleasePlanTemplate } from 'component/releases/ReleasePlanTemplate/EditReleasePlanTemplate'; import { ExploreCounters } from 'component/counters/ExploreCounters/ExploreCounters.js'; import { UnknownFlagsTable } from 'component/unknownFlags/UnknownFlagsTable'; +import { ChangeRequests } from 'component/changeRequest/ChangeRequests/ChangeRequests'; export const routes: IRoute[] = [ // Splash @@ -478,6 +479,18 @@ export const routes: IRoute[] = [ menu: {}, }, + // My change requests + { + path: '/change-requests', + title: 'Change Requests', + component: ChangeRequests, + type: 'protected', + menu: {}, + flag: 'globalChangeRequestList', + hidden: true, + enterprise: true, + }, + // Admin { path: '/admin/*',