1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-28 17:55:15 +02:00
unleash.unleash/frontend/src/component/project/Project/ProjectSettings/ChangeRequestConfiguration/ChangeRequestTable.tsx
2022-12-08 10:59:37 +01:00

314 lines
12 KiB
TypeScript

import React, { useContext, useMemo, useState, VFC } from 'react';
import { HeaderGroup, useGlobalFilter, useTable } from 'react-table';
import { Alert, Box, styled, Typography } from '@mui/material';
import {
SortableTableHeader,
Table,
TableBody,
TableCell,
TableRow,
} from 'component/common/Table';
import { sortTypes } from 'utils/sortTypes';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useChangeRequestConfig } from 'hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig';
import {
IChangeRequestConfig,
useChangeRequestApi,
} from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { UPDATE_PROJECT } from '@server/types/permissions';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ChangeRequestProcessHelp } from './ChangeRequestProcessHelp/ChangeRequestProcessHelp';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import AccessContext from 'contexts/AccessContext';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(1),
display: 'flex',
justifyContent: 'center',
'& .MuiInputBase-input': {
fontSize: theme.fontSizes.smallBody,
},
}));
export const ChangeRequestTable: VFC = () => {
const { trackEvent } = usePlausibleTracker();
const [dialogState, setDialogState] = useState<{
isOpen: boolean;
enableEnvironment: string;
isEnabled: boolean;
requiredApprovals: number;
}>({
isOpen: false,
enableEnvironment: '',
isEnabled: false,
requiredApprovals: 1,
});
const theme = useTheme();
const projectId = useRequiredPathParam('projectId');
const { data, loading, refetchChangeRequestConfig } =
useChangeRequestConfig(projectId);
const { updateChangeRequestEnvironmentConfig } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast();
const onRowChange =
(
enableEnvironment: string,
isEnabled: boolean,
requiredApprovals: number
) =>
() => {
setDialogState({
isOpen: true,
enableEnvironment,
isEnabled,
requiredApprovals,
});
};
const onConfirm = async () => {
if (dialogState.enableEnvironment) {
await updateConfiguration();
}
setDialogState(state => ({ ...state, isOpen: false }));
};
async function updateConfiguration(config?: IChangeRequestConfig) {
try {
await updateChangeRequestEnvironmentConfig(
config || {
project: projectId,
environment: dialogState.enableEnvironment,
enabled: !dialogState.isEnabled,
requiredApprovals: dialogState.requiredApprovals,
}
);
setToastData({
type: 'success',
title: 'Updated change request status',
text: 'Successfully updated change request status.',
});
await refetchChangeRequestConfig();
} catch (error) {
setToastApiError(formatUnknownError(error));
}
}
const approvalOptions = Array.from(Array(10).keys())
.map(key => String(key + 1))
.map(key => {
const labelText = key === '1' ? 'approval' : 'approvals';
return {
key,
label: `${key} ${labelText}`,
sx: { 'font-size': theme.fontSizes.smallBody },
};
});
function onRequiredApprovalsChange(original: any, approvals: string) {
updateConfiguration({
project: projectId,
environment: original.environment,
enabled: original.changeRequestEnabled,
requiredApprovals: Number(approvals),
});
}
const columns = useMemo(
() => [
{
Header: 'Environment',
accessor: 'environment',
disableSortBy: true,
},
{
Header: 'Type',
accessor: 'type',
disableGlobalFilter: true,
disableSortBy: true,
},
{
Header: 'Required approvals',
Cell: ({ row: { original } }: any) => {
const { hasAccess } = useContext(AccessContext);
return (
<ConditionallyRender
condition={original.changeRequestEnabled}
show={
<StyledBox data-loading>
<GeneralSelect
sx={{ width: '140px', marginLeft: 1 }}
options={approvalOptions}
value={original.requiredApprovals || 1}
onChange={approvals => {
onRequiredApprovalsChange(
original,
approvals
);
}}
disabled={
!hasAccess(
UPDATE_PROJECT,
projectId
)
}
IconComponent={
KeyboardArrowDownOutlined
}
fullWidth
/>
</StyledBox>
}
/>
);
},
width: 100,
disableGlobalFilter: true,
disableSortBy: true,
},
{
Header: 'Status',
accessor: 'changeRequestEnabled',
id: 'changeRequestEnabled',
align: 'center',
Cell: ({ value, row: { original } }: any) => (
<StyledBox data-loading>
<PermissionSwitch
checked={value}
projectId={projectId}
permission={UPDATE_PROJECT}
inputProps={{ 'aria-label': original.environment }}
onClick={onRowChange(
original.environment,
original.changeRequestEnabled,
original.requiredApprovals
)}
/>
</StyledBox>
),
width: 100,
disableGlobalFilter: true,
disableSortBy: true,
},
],
[]
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable(
{
// @ts-ignore
columns,
data,
sortTypes,
autoResetGlobalFilter: false,
disableSortRemove: true,
defaultColumn: {
Cell: TextCell,
},
},
useGlobalFilter
);
return (
<PageContent
header={
<PageHeader
titleElement="Change request configuration"
actions={<ChangeRequestProcessHelp />}
/>
}
isLoading={loading}
>
<Alert severity="info" sx={{ mb: 3 }}>
If change request is enabled for an environment, then any change
in that environment needs to be approved before it will be
applied
</Alert>
<Table {...getTableProps()}>
<SortableTableHeader
headerGroups={headerGroups as HeaderGroup<object>[]}
/>
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
<Dialogue
onClick={() => {
trackEvent('change_request', {
props: {
eventType: `change request ${
!dialogState.isEnabled ? 'enabled' : 'disabled'
}`,
},
});
onConfirm();
}}
open={dialogState.isOpen}
onClose={() =>
setDialogState(state => ({ ...state, isOpen: false }))
}
primaryButtonText={dialogState.isEnabled ? 'Disable' : 'Enable'}
secondaryButtonText="Cancel"
title={`${
dialogState.isEnabled ? 'Disable' : 'Enable'
} change requests`}
>
<Typography sx={{ mb: 1 }}>
You are about to{' '}
{dialogState.isEnabled ? 'disable' : 'enable'} Change
request
<ConditionallyRender
condition={Boolean(dialogState.enableEnvironment)}
show={
<>
{' '}
for{' '}
<strong>{dialogState.enableEnvironment}</strong>
</>
}
/>
.
</Typography>
<ConditionallyRender
condition={!dialogState.isEnabled}
show={
<Typography variant="body2" color="text.secondary">
When enabling change request for an environment, you
need to be sure that your Unleash Admin already have
created the custom project roles in your Unleash
instance so you can assign your project members from
the project access page.
</Typography>
}
/>
</Dialogue>
</PageContent>
);
};