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 { EditReleasePlanTemplate } from 'component/releases/ReleasePlanTemplate/EditReleasePlanTemplate';
|
||||||
import { ExploreCounters } from 'component/counters/ExploreCounters/ExploreCounters.js';
|
import { ExploreCounters } from 'component/counters/ExploreCounters/ExploreCounters.js';
|
||||||
import { UnknownFlagsTable } from 'component/unknownFlags/UnknownFlagsTable';
|
import { UnknownFlagsTable } from 'component/unknownFlags/UnknownFlagsTable';
|
||||||
|
import { ChangeRequests } from 'component/changeRequest/ChangeRequests/ChangeRequests';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -478,6 +479,18 @@ export const routes: IRoute[] = [
|
|||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// My change requests
|
||||||
|
{
|
||||||
|
path: '/change-requests',
|
||||||
|
title: 'Change Requests',
|
||||||
|
component: ChangeRequests,
|
||||||
|
type: 'protected',
|
||||||
|
menu: {},
|
||||||
|
flag: 'globalChangeRequestList',
|
||||||
|
hidden: true,
|
||||||
|
enterprise: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
{
|
{
|
||||||
path: '/admin/*',
|
path: '/admin/*',
|
||||||
|
Loading…
Reference in New Issue
Block a user