mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
Suggest changes dialog (#2247)
* refactor: suggested changes folder structure * feat: add dialogue confirmation
This commit is contained in:
parent
2304ea1d1e
commit
ea2cf144f9
@ -10,6 +10,9 @@ import React from 'react';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useStyles } from './FeatureOverviewEnvSwitch.styles';
|
import { useStyles } from './FeatureOverviewEnvSwitch.styles';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useSuggestToggle } from 'hooks/useSuggestToggle';
|
||||||
|
import { SuggestChangesDialogue } from 'component/suggestChanges/SuggestChangeConfirmDialog/SuggestChangeConfirmDialog';
|
||||||
|
|
||||||
interface IFeatureOverviewEnvSwitchProps {
|
interface IFeatureOverviewEnvSwitchProps {
|
||||||
env: IFeatureEnvironment;
|
env: IFeatureEnvironment;
|
||||||
@ -31,6 +34,12 @@ const FeatureOverviewEnvSwitch = ({
|
|||||||
const { refetchFeature } = useFeature(projectId, featureId);
|
const { refetchFeature } = useFeature(projectId, featureId);
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const {
|
||||||
|
onSuggestToggle,
|
||||||
|
onSuggestToggleClose,
|
||||||
|
suggestChangesDialogDetails,
|
||||||
|
} = useSuggestToggle();
|
||||||
|
|
||||||
const handleToggleEnvironmentOn = async () => {
|
const handleToggleEnvironmentOn = async () => {
|
||||||
try {
|
try {
|
||||||
@ -74,6 +83,11 @@ const FeatureOverviewEnvSwitch = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleEnvironment = async (e: React.ChangeEvent) => {
|
const toggleEnvironment = async (e: React.ChangeEvent) => {
|
||||||
|
if (uiConfig?.flags?.suggestChanges && env.name === 'production') {
|
||||||
|
e.preventDefault();
|
||||||
|
onSuggestToggle(featureId, env.name, env.enabled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (env.enabled) {
|
if (env.enabled) {
|
||||||
await handleToggleEnvironmentOff();
|
await handleToggleEnvironmentOff();
|
||||||
return;
|
return;
|
||||||
@ -104,6 +118,13 @@ const FeatureOverviewEnvSwitch = ({
|
|||||||
/>
|
/>
|
||||||
{content}
|
{content}
|
||||||
</label>
|
</label>
|
||||||
|
<SuggestChangesDialogue
|
||||||
|
isOpen={suggestChangesDialogDetails.isOpen}
|
||||||
|
onClose={onSuggestToggleClose}
|
||||||
|
featureName={featureId}
|
||||||
|
environment={suggestChangesDialogDetails?.environment}
|
||||||
|
onConfirm={() => {}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -24,8 +24,8 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|||||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
|
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
|
||||||
import { ProjectLog } from './ProjectLog/ProjectLog';
|
import { ProjectLog } from './ProjectLog/ProjectLog';
|
||||||
import { SuggestedChangeOverview } from './SuggestedChanges/SuggestedChangeOverview/SuggestedChangeOverview';
|
import { SuggestedChangeOverview } from 'component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview';
|
||||||
import { DraftBanner } from './SuggestedChanges/DraftBanner/DraftBanner';
|
import { DraftBanner } from 'component/suggestChanges/DraftBanner/DraftBanner';
|
||||||
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
|
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
|
||||||
|
|
||||||
const StyledDiv = styled('div')(() => ({
|
const StyledDiv = styled('div')(() => ({
|
||||||
|
@ -36,6 +36,8 @@ import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/Feat
|
|||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import { useMediaQuery } from '@mui/material';
|
import { useMediaQuery } from '@mui/material';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { useSuggestToggle } from 'hooks/useSuggestToggle';
|
||||||
|
import { SuggestChangesDialogue } from 'component/suggestChanges/SuggestChangeConfirmDialog/SuggestChangeConfirmDialog';
|
||||||
|
|
||||||
interface IProjectFeatureTogglesProps {
|
interface IProjectFeatureTogglesProps {
|
||||||
features: IProject['features'];
|
features: IProject['features'];
|
||||||
@ -99,6 +101,11 @@ export const ProjectFeatureToggles = ({
|
|||||||
|
|
||||||
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||||
useFeatureApi();
|
useFeatureApi();
|
||||||
|
const {
|
||||||
|
onSuggestToggle,
|
||||||
|
onSuggestToggleClose,
|
||||||
|
suggestChangesDialogDetails,
|
||||||
|
} = useSuggestToggle();
|
||||||
|
|
||||||
const onToggle = useCallback(
|
const onToggle = useCallback(
|
||||||
async (
|
async (
|
||||||
@ -107,6 +114,13 @@ export const ProjectFeatureToggles = ({
|
|||||||
environment: string,
|
environment: string,
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
) => {
|
) => {
|
||||||
|
if (
|
||||||
|
uiConfig?.flags?.suggestChanges &&
|
||||||
|
environment === 'production'
|
||||||
|
) {
|
||||||
|
onSuggestToggle(featureName, environment, enabled);
|
||||||
|
throw new Error('Additional approval required');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
await toggleFeatureEnvironmentOn(
|
await toggleFeatureEnvironmentOn(
|
||||||
@ -501,6 +515,13 @@ export const ProjectFeatureToggles = ({
|
|||||||
}}
|
}}
|
||||||
featureId={featureArchiveState || ''}
|
featureId={featureArchiveState || ''}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
/>{' '}
|
||||||
|
<SuggestChangesDialogue
|
||||||
|
isOpen={suggestChangesDialogDetails.isOpen}
|
||||||
|
onClose={onSuggestToggleClose}
|
||||||
|
featureName={suggestChangesDialogDetails?.featureName}
|
||||||
|
environment={suggestChangesDialogDetails?.environment}
|
||||||
|
onConfirm={() => {}}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { VFC } from 'react';
|
|
||||||
import { ISuggestChange } from 'interfaces/suggestChangeset';
|
|
||||||
import { Box } from '@mui/system';
|
|
||||||
import { PlaygroundResultChip } from 'component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip'; // FIXME: refactor - extract to common
|
|
||||||
|
|
||||||
export const ChangeItem: VFC<ISuggestChange> = ({ action, id, payload }) => {
|
|
||||||
if (action === 'updateEnabled') {
|
|
||||||
return (
|
|
||||||
<Box key={id}>
|
|
||||||
New status:{' '}
|
|
||||||
<PlaygroundResultChip
|
|
||||||
showIcon={false}
|
|
||||||
label={payload ? 'Enabled' : 'Disabled'}
|
|
||||||
enabled={Boolean(payload)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Box key={id}>Change with ID: {id}</Box>;
|
|
||||||
};
|
|
@ -1,83 +0,0 @@
|
|||||||
import { VFC } from 'react';
|
|
||||||
import { Box, Typography, Card, styled } from '@mui/material';
|
|
||||||
import { ISuggestChange } from 'interfaces/suggestChangeset';
|
|
||||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
|
||||||
import { PlaygroundResultChip } from 'component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip'; // FIXME: refactor - extract to common
|
|
||||||
import { ChangeItem } from './ChangeItem/ChangeItem';
|
|
||||||
|
|
||||||
type ChangesetDiffProps = {
|
|
||||||
changes?: ISuggestChange[];
|
|
||||||
state: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledHeader = styled('div')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
[theme.breakpoints.down(560)]: {
|
|
||||||
flexDirection: 'column',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
paddingBottom: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ChangesetDiff: VFC<ChangesetDiffProps> = ({ changes, state }) => (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 3,
|
|
||||||
border: '2px solid',
|
|
||||||
borderColor: theme => theme.palette.playgroundBackground,
|
|
||||||
display: 'flex',
|
|
||||||
gap: 2,
|
|
||||||
flexDirection: 'column',
|
|
||||||
borderRadius: theme => `${theme.shape.borderRadiusExtraLarge}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledHeader>
|
|
||||||
<EnvironmentIcon enabled={true} />
|
|
||||||
<Box>
|
|
||||||
<StringTruncator
|
|
||||||
text={`production`}
|
|
||||||
maxWidth="100"
|
|
||||||
maxLength={15}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ ml: 'auto' }}>
|
|
||||||
<PlaygroundResultChip
|
|
||||||
showIcon={false}
|
|
||||||
label={state === 'CREATED' ? 'Draft mode' : '???'}
|
|
||||||
enabled="unknown"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</StyledHeader>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
|
||||||
You request changes for these feature toggles:
|
|
||||||
</Typography>
|
|
||||||
{/* TODO: group by feature name */}
|
|
||||||
{changes?.map(item => (
|
|
||||||
<Card
|
|
||||||
key={item.feature}
|
|
||||||
elevation={0}
|
|
||||||
sx={{
|
|
||||||
borderRadius: theme => `${theme.shape.borderRadius}px`,
|
|
||||||
overflow: 'hidden',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme => theme.palette.dividerAlternative,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme =>
|
|
||||||
theme.palette.tableHeaderBackground,
|
|
||||||
p: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography>{item.feature}</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ p: 2 }}>
|
|
||||||
<ChangeItem {...item} />
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
@ -1,90 +0,0 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
import { Box, Paper } from '@mui/material';
|
|
||||||
import { SuggestedFeatureToggleChange } from '../SuggestedFeatureToggleChange/SuggestedFeatureToggleChange';
|
|
||||||
import { objectId } from '../../../../../../utils/objectId';
|
|
||||||
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { ToggleStatusChange } from '../SuggestedFeatureToggleChange/ToggleStatusChange';
|
|
||||||
import {
|
|
||||||
StrategyAddedChange,
|
|
||||||
StrategyDeletedChange,
|
|
||||||
StrategyEditedChange,
|
|
||||||
} from '../SuggestedFeatureToggleChange/StrategyChange';
|
|
||||||
import {
|
|
||||||
formatStrategyName,
|
|
||||||
GetFeatureStrategyIcon,
|
|
||||||
} from '../../../../../../utils/strategyNames';
|
|
||||||
|
|
||||||
export const SuggestedChangeSet: FC<{ suggestedChange: any }> = ({
|
|
||||||
suggestedChange,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
elevation={0}
|
|
||||||
sx={theme => ({
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
marginLeft: theme.spacing(2),
|
|
||||||
width: '70%',
|
|
||||||
padding: 2,
|
|
||||||
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={theme => ({
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Changes
|
|
||||||
{suggestedChange.changes?.map((featureToggleChange: any) => (
|
|
||||||
<SuggestedFeatureToggleChange
|
|
||||||
key={featureToggleChange.feature}
|
|
||||||
featureToggleName={featureToggleChange.feature}
|
|
||||||
>
|
|
||||||
{featureToggleChange.changeSet.map((change: any) => (
|
|
||||||
<Box key={objectId(change)}>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
change.action === 'updateEnabled'
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<ToggleStatusChange
|
|
||||||
enabled={
|
|
||||||
change?.payload?.data?.data
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={change.action === 'addStrategy'}
|
|
||||||
show={
|
|
||||||
<StrategyAddedChange>
|
|
||||||
<GetFeatureStrategyIcon
|
|
||||||
strategyName={
|
|
||||||
change.payload.name
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{formatStrategyName(
|
|
||||||
change.payload.name
|
|
||||||
)}
|
|
||||||
</StrategyAddedChange>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
change.action === 'deleteStrategy'
|
|
||||||
}
|
|
||||||
show={<StrategyDeletedChange />}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
change.action === 'updateStrategy'
|
|
||||||
}
|
|
||||||
show={<StrategyEditedChange />}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</SuggestedFeatureToggleChange>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -3,6 +3,7 @@ import { Box, Button, Typography } from '@mui/material';
|
|||||||
import { useStyles as useAppStyles } from 'component/App.styles';
|
import { useStyles as useAppStyles } from 'component/App.styles';
|
||||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { SuggestedChangesSidebar } from '../SuggestedChangesSidebar/SuggestedChangesSidebar';
|
||||||
|
|
||||||
interface IDraftBannerProps {
|
interface IDraftBannerProps {
|
||||||
environment?: string;
|
environment?: string;
|
||||||
@ -10,7 +11,7 @@ interface IDraftBannerProps {
|
|||||||
|
|
||||||
export const DraftBanner: VFC<IDraftBannerProps> = ({ environment }) => {
|
export const DraftBanner: VFC<IDraftBannerProps> = ({ environment }) => {
|
||||||
const { classes } = useAppStyles();
|
const { classes } = useAppStyles();
|
||||||
const [reviewChangesOpen, setReviewChangesOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -49,7 +50,9 @@ export const DraftBanner: VFC<IDraftBannerProps> = ({ environment }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => setReviewChangesOpen(true)}
|
onClick={() => {
|
||||||
|
setIsSidebarOpen(true);
|
||||||
|
}}
|
||||||
sx={{ ml: 'auto' }}
|
sx={{ ml: 'auto' }}
|
||||||
>
|
>
|
||||||
Review changes
|
Review changes
|
||||||
@ -59,6 +62,12 @@ export const DraftBanner: VFC<IDraftBannerProps> = ({ environment }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<SuggestedChangesSidebar
|
||||||
|
open={isSidebarOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -0,0 +1,58 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Alert, Box, Typography } from '@mui/material';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
|
interface ISuggestChangesDialogueProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
featureName?: string;
|
||||||
|
environment?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SuggestChangesDialogue: FC<ISuggestChangesDialogueProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onConfirm,
|
||||||
|
onClose,
|
||||||
|
enabled,
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
}) => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
|
const onSuggestClick = async () => {
|
||||||
|
try {
|
||||||
|
alert('Suggesting changes');
|
||||||
|
onConfirm();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
open={isOpen}
|
||||||
|
primaryButtonText="Add to draft"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClick={onSuggestClick}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Suggest changes"
|
||||||
|
>
|
||||||
|
<Alert severity="info" sx={{ mb: 2 }}>
|
||||||
|
Suggest changes is enabled for {environment}. Your changes needs
|
||||||
|
to be approved before they will be live. All the changes you do
|
||||||
|
now will be added into a draft that you can submit for review.
|
||||||
|
</Alert>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Suggested changes:
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
<strong>{enabled ? 'Disable' : 'Enable'}</strong> feature toggle{' '}
|
||||||
|
<strong>{featureName}</strong> in <strong>{environment}</strong>
|
||||||
|
</Typography>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Avatar, Box, Card, Paper, Typography } from '@mui/material';
|
import { Avatar, Box, Card, Paper, Typography } from '@mui/material';
|
||||||
import { StyledTrueChip } from '../../../../../playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip';
|
import { PlaygroundResultChip } from 'component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip';
|
||||||
import { ReactComponent as ChangesAppliedIcon } from '../../../../../../assets/icons/merge.svg';
|
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
||||||
import TimeAgo from 'react-timeago';
|
import TimeAgo from 'react-timeago';
|
||||||
|
|
||||||
export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
||||||
@ -35,9 +35,10 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
|||||||
#{suggestedChange.id}
|
#{suggestedChange.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
<StyledTrueChip
|
<PlaygroundResultChip
|
||||||
icon={<ChangesAppliedIcon strokeWidth="0.25" />}
|
// icon={<ChangesAppliedIcon strokeWidth="0.25" />}
|
||||||
label="Changes applied"
|
label="Changes applied"
|
||||||
|
enabled="unknown"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}>
|
@ -1,10 +1,10 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Box } from '@mui/material';
|
import { Box, Paper } from '@mui/material';
|
||||||
import { useSuggestedChange } from 'hooks/api/getters/useSuggestChange/useSuggestedChange';
|
import { useSuggestedChange } from 'hooks/api/getters/useSuggestChange/useSuggestedChange';
|
||||||
import { SuggestedChangeHeader } from './SuggestedChangeHeader/SuggestedChangeHeader';
|
import { SuggestedChangeHeader } from './SuggestedChangeHeader/SuggestedChangeHeader';
|
||||||
import { SuggestedChangeTimeline } from './SuggestedChangeTimeline/SuggestedChangeTimeline';
|
import { SuggestedChangeTimeline } from './SuggestedChangeTimeline/SuggestedChangeTimeline';
|
||||||
import { SuggestedChangeReviewers } from './SuggestedChangeReviewers/SuggestedChangeReviewers';
|
import { SuggestedChangeReviewers } from './SuggestedChangeReviewers/SuggestedChangeReviewers';
|
||||||
import { SuggestedChangeSet } from './SuggestedChangeSet/SuggestedChangeSet';
|
import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset';
|
||||||
|
|
||||||
export const SuggestedChangeOverview: FC = () => {
|
export const SuggestedChangeOverview: FC = () => {
|
||||||
const { data: suggestedChange } = useSuggestedChange();
|
const { data: suggestedChange } = useSuggestedChange();
|
||||||
@ -23,8 +23,25 @@ export const SuggestedChangeOverview: FC = () => {
|
|||||||
<SuggestedChangeTimeline />
|
<SuggestedChangeTimeline />
|
||||||
<SuggestedChangeReviewers />
|
<SuggestedChangeReviewers />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Paper
|
||||||
<SuggestedChangeSet suggestedChange={suggestedChange} />
|
elevation={0}
|
||||||
|
sx={theme => ({
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginLeft: theme.spacing(2),
|
||||||
|
width: '70%',
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: theme =>
|
||||||
|
`${theme.shape.borderRadiusLarge}px`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={theme => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SuggestedChangeset suggestedChange={suggestedChange} />
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
@ -1,6 +1,6 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { PlaygroundResultChip } from '../../../../../playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip';
|
import { PlaygroundResultChip } from 'component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip';
|
||||||
|
|
||||||
export const ToggleStatusChange: FC<{ enabled: boolean }> = ({ enabled }) => {
|
export const ToggleStatusChange: FC<{ enabled: boolean }> = ({ enabled }) => {
|
||||||
return (
|
return (
|
@ -0,0 +1,135 @@
|
|||||||
|
import React, { useState, VFC } from 'react';
|
||||||
|
import { Box, Button, Typography, styled, Tooltip } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { HelpOutline } from '@mui/icons-material';
|
||||||
|
import { useSuggestedChange } from 'hooks/api/getters/useSuggestChange/useSuggestedChange';
|
||||||
|
import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset';
|
||||||
|
interface ISuggestedChangesSidebarProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
const StyledPageContent = styled(PageContent)(({ theme }) => ({
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: theme.spacing(7.5, 6),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
padding: theme.spacing(4, 2),
|
||||||
|
},
|
||||||
|
'& .header': {
|
||||||
|
padding: theme.spacing(0, 0, 2, 0),
|
||||||
|
},
|
||||||
|
'& .body': {
|
||||||
|
padding: theme.spacing(3, 0, 0, 0),
|
||||||
|
},
|
||||||
|
borderRadius: `${theme.spacing(1.5, 0, 0, 1.5)} !important`,
|
||||||
|
}));
|
||||||
|
const StyledHelpOutline = styled(HelpOutline)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
marginLeft: '0.3rem',
|
||||||
|
color: theme.palette.grey[700],
|
||||||
|
}));
|
||||||
|
const StyledHeaderHint = styled('div')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { data: suggestedChange } = useSuggestedChange();
|
||||||
|
|
||||||
|
const onReview = async () => {
|
||||||
|
console.log('approve');
|
||||||
|
};
|
||||||
|
const onDiscard = async () => {
|
||||||
|
console.log('discard');
|
||||||
|
};
|
||||||
|
const onApply = async () => {
|
||||||
|
try {
|
||||||
|
console.log('apply');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<SidebarModal open={open} onClose={onClose} label="Review changes">
|
||||||
|
<StyledPageContent
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
secondary
|
||||||
|
titleElement={
|
||||||
|
<>
|
||||||
|
Review your changes
|
||||||
|
<Tooltip
|
||||||
|
title="You can review your changes from this page.
|
||||||
|
Needs a text to explain the process."
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<StyledHelpOutline />
|
||||||
|
</Tooltip>
|
||||||
|
<StyledHeaderHint>
|
||||||
|
Make sure you are sending the right changes
|
||||||
|
suggestions to be reviewed
|
||||||
|
</StyledHeaderHint>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
></PageHeader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* TODO: multiple environments (changesets) */}
|
||||||
|
<Typography>{suggestedChange?.state}</Typography>
|
||||||
|
<br />
|
||||||
|
<SuggestedChangeset suggestedChange={suggestedChange} />
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={suggestedChange?.state === 'APPROVED'}
|
||||||
|
show={<Typography>Applied</Typography>}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={suggestedChange?.state === 'CLOSED'}
|
||||||
|
show={<Typography>Applied</Typography>}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={suggestedChange?.state === 'APPROVED'}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
variant="contained"
|
||||||
|
onClick={onApply}
|
||||||
|
>
|
||||||
|
Apply changes
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={suggestedChange?.state === 'CREATED'}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
sx={{ mt: 2, ml: 'auto' }}
|
||||||
|
variant="contained"
|
||||||
|
onClick={onReview}
|
||||||
|
>
|
||||||
|
Request changes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
sx={{ mt: 2, ml: 2 }}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDiscard}
|
||||||
|
>
|
||||||
|
Discard changes
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</StyledPageContent>
|
||||||
|
</SidebarModal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { SuggestedFeatureToggleChange } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/SuggestedFeatureToggleChange';
|
||||||
|
import { objectId } from 'utils/objectId';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ToggleStatusChange } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/ToggleStatusChange';
|
||||||
|
import {
|
||||||
|
StrategyAddedChange,
|
||||||
|
StrategyDeletedChange,
|
||||||
|
StrategyEditedChange,
|
||||||
|
} from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/StrategyChange';
|
||||||
|
import {
|
||||||
|
formatStrategyName,
|
||||||
|
GetFeatureStrategyIcon,
|
||||||
|
} from 'utils/strategyNames';
|
||||||
|
|
||||||
|
export const SuggestedChangeset: FC<{ suggestedChange: any }> = ({
|
||||||
|
suggestedChange,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
Changes
|
||||||
|
{suggestedChange.changes?.map((featureToggleChange: any) => (
|
||||||
|
<SuggestedFeatureToggleChange
|
||||||
|
key={featureToggleChange.feature}
|
||||||
|
featureToggleName={featureToggleChange.feature}
|
||||||
|
>
|
||||||
|
{featureToggleChange.changeSet.map((change: any) => (
|
||||||
|
<Box key={objectId(change)}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={change.action === 'updateEnabled'}
|
||||||
|
show={
|
||||||
|
<ToggleStatusChange
|
||||||
|
enabled={change?.payload?.data?.data}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={change.action === 'addStrategy'}
|
||||||
|
show={
|
||||||
|
<StrategyAddedChange>
|
||||||
|
<GetFeatureStrategyIcon
|
||||||
|
strategyName={change.payload.name}
|
||||||
|
/>
|
||||||
|
{formatStrategyName(
|
||||||
|
change.payload.name
|
||||||
|
)}
|
||||||
|
</StrategyAddedChange>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={change.action === 'deleteStrategy'}
|
||||||
|
show={<StrategyDeletedChange />}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={change.action === 'updateStrategy'}
|
||||||
|
show={<StrategyEditedChange />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SuggestedFeatureToggleChange>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
33
frontend/src/hooks/useSuggestToggle.ts
Normal file
33
frontend/src/hooks/useSuggestToggle.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
export const useSuggestToggle = () => {
|
||||||
|
const [suggestChangesDialogDetails, setSuggestChangesDialogDetails] =
|
||||||
|
useState<{
|
||||||
|
enabled?: boolean;
|
||||||
|
featureName?: string;
|
||||||
|
environment?: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
}>({ isOpen: false });
|
||||||
|
|
||||||
|
const onSuggestToggle = useCallback(
|
||||||
|
(featureName: string, environment: string, enabled: boolean) => {
|
||||||
|
setSuggestChangesDialogDetails({
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
enabled,
|
||||||
|
isOpen: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSuggestToggleClose = useCallback(() => {
|
||||||
|
setSuggestChangesDialogDetails({ isOpen: false });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onSuggestToggle,
|
||||||
|
onSuggestToggleClose,
|
||||||
|
suggestChangesDialogDetails,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user