mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-19 17:52:45 +02:00
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. <img width="1386" height="671" alt="image" src="https://github.com/user-attachments/assets/b24ed625-d3f6-4281-ba44-30744d5063f3" /> If the flag is disabled, we render nothing useful. <img width="1429" height="287" alt="image" src="https://github.com/user-attachments/assets/289b5707-4389-4c08-bf68-55d63e186ba5" /> closes 1-4076
This commit is contained in:
parent
d3e7e67b91
commit
fabf76e12c
@ -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<string>,
|
||||
) => {
|
||||
return row.features.find((feature) =>
|
||||
values
|
||||
.map((value) => value.toLowerCase())
|
||||
.includes(feature.name.toLowerCase()),
|
||||
);
|
||||
},
|
||||
Cell: ({
|
||||
value,
|
||||
row: {
|
||||
original: { title, project },
|
||||
},
|
||||
}: any) => (
|
||||
<FeaturesCell project={project} value={value} key={title} />
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={<PageHeader title='Change requests' />}
|
||||
>
|
||||
<Table {...getTableProps()}>
|
||||
<SortableTableHeader headerGroups={headerGroups} />
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, ...rowProps } = row.getRowProps();
|
||||
return (
|
||||
<TableRow hover key={key} {...rowProps}>
|
||||
{row.cells.map((cell) => {
|
||||
const { key, ...cellProps } =
|
||||
cell.getCellProps();
|
||||
return (
|
||||
<TableCell key={key} {...cellProps}>
|
||||
{cell.render('Cell')}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChangeRequests = () => {
|
||||
if (!useUiFlag('globalChangeRequestList')) {
|
||||
return (
|
||||
<PageContent header={<PageHeader title='Change requests' />}>
|
||||
<p>Nothing to see here. Move along.</p>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
return <ChangeRequestsInner />;
|
||||
};
|
@ -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) => (
|
||||
<Link component={RouterLink} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
))(({ 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 <TextCell />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TextCell sx={{ minWidth: '300px' }}>
|
||||
<LinkContainer>
|
||||
<BaseLink to={projectPath}>{projectName}</BaseLink>
|
||||
<span aria-hidden='true'> / </span>
|
||||
<ChangeRequestLink to={crPath}>{title}</ChangeRequestLink>
|
||||
</LinkContainer>
|
||||
<UpdateText>
|
||||
{`${totalChanges}`} {totalChanges === 1 ? `update` : 'updates'}
|
||||
</UpdateText>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
@ -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/*',
|
||||
|
Loading…
Reference in New Issue
Block a user