mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
Suggest changes - initial frontend (#2213)
* feat: add initial controller * feat: add fe * feat: return status codes * remove backend experiment * refactor standalone route for project banner * update suggest changeset type * refactor changeset mock * suggest changes banner feature flag * fix: update routes snapshot Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
726674ea3e
commit
b8c3833ae4
@ -18,10 +18,12 @@ export const useStyles = makeStyles()(theme => ({
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: theme.palette.contentWrapper,
|
||||
position: 'relative',
|
||||
},
|
||||
content: {
|
||||
width: '1250px',
|
||||
margin: '16px auto',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
width: '1024px',
|
||||
},
|
||||
|
@ -40,36 +40,41 @@ export const App = () => {
|
||||
elseShow={
|
||||
<div className={styles.container}>
|
||||
<ToastRenderer />
|
||||
<LayoutPicker>
|
||||
<Routes>
|
||||
{availableRoutes.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<Routes>
|
||||
{availableRoutes.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<LayoutPicker
|
||||
isStandalone={
|
||||
route.isStandalone ===
|
||||
true
|
||||
}
|
||||
>
|
||||
<ProtectedRoute
|
||||
route={route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Navigate
|
||||
to="/features"
|
||||
replace
|
||||
/>
|
||||
</LayoutPicker>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
/>
|
||||
</Routes>
|
||||
<FeedbackNPS openUrl="http://feedback.unleash.run" />
|
||||
<SplashPageRedirect />
|
||||
</LayoutPicker>
|
||||
))}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Navigate
|
||||
to="/features"
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
/>
|
||||
</Routes>
|
||||
<FeedbackNPS openUrl="http://feedback.unleash.run" />
|
||||
<SplashPageRedirect />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
@ -2,7 +2,6 @@ import { Typography } from '@mui/material';
|
||||
import { IFeatureStrategyParameters } from 'interfaces/strategy';
|
||||
import RolloutSlider from '../RolloutSlider/RolloutSlider';
|
||||
import Select from 'component/common/select';
|
||||
import React from 'react';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import {
|
||||
FLEXIBLE_STRATEGY_GROUP_ID,
|
||||
|
@ -1,37 +1,19 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { matchPath } from 'react-router';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { MainLayout } from '../MainLayout/MainLayout';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ILayoutPickerProps {
|
||||
children: ReactNode;
|
||||
isStandalone?: boolean;
|
||||
}
|
||||
|
||||
export const LayoutPicker = ({ children }: ILayoutPickerProps) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={isStandalonePage(pathname)}
|
||||
show={children}
|
||||
elseShow={<MainLayout>{children}</MainLayout>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const isStandalonePage = (pathname: string): boolean => {
|
||||
return standalonePagePatterns.some(pattern => {
|
||||
return matchPath(pattern, pathname);
|
||||
});
|
||||
};
|
||||
|
||||
const standalonePagePatterns = [
|
||||
'/login',
|
||||
'/new-user',
|
||||
'/reset-password',
|
||||
'/reset-password-success',
|
||||
'/forgotten-password',
|
||||
'/splash/:splashId',
|
||||
'/404',
|
||||
];
|
||||
export const LayoutPicker: FC<ILayoutPickerProps> = ({
|
||||
isStandalone,
|
||||
children,
|
||||
}) => (
|
||||
<ConditionallyRender
|
||||
condition={isStandalone === true}
|
||||
show={children}
|
||||
elseShow={<MainLayout>{children}</MainLayout>}
|
||||
/>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { forwardRef, ReactNode } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
import { Grid } from '@mui/material';
|
||||
@ -30,47 +30,58 @@ const useStyles = makeStyles()(theme => ({
|
||||
|
||||
interface IMainLayoutProps {
|
||||
children: ReactNode;
|
||||
subheader?: ReactNode;
|
||||
}
|
||||
|
||||
export const MainLayout = ({ children }: IMainLayoutProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { classes: styles } = useAppStyles();
|
||||
const { uiConfig } = useUiConfig();
|
||||
export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
|
||||
({ children, subheader }, ref) => {
|
||||
const { classes } = useStyles();
|
||||
const { classes: styles } = useAppStyles();
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkipNavLink />
|
||||
<Header />
|
||||
<SkipNavTarget />
|
||||
<Grid container className={classes.container}>
|
||||
<main className={classnames(styles.contentWrapper)}>
|
||||
<Grid item className={styles.content} xs={12} sm={12}>
|
||||
<div
|
||||
className={classes.contentContainer}
|
||||
style={{ zIndex: 200 }}
|
||||
return (
|
||||
<>
|
||||
<SkipNavLink />
|
||||
<Header />
|
||||
<SkipNavTarget />
|
||||
<Grid container className={classes.container}>
|
||||
<main className={classnames(styles.contentWrapper)}>
|
||||
{subheader}
|
||||
<Grid
|
||||
item
|
||||
className={styles.content}
|
||||
xs={12}
|
||||
sm={12}
|
||||
my={2}
|
||||
>
|
||||
<BreadcrumbNav />
|
||||
<Proclamation toast={uiConfig.toast} />
|
||||
{children}
|
||||
</div>
|
||||
</Grid>
|
||||
<img
|
||||
src={formatAssetPath(textureImage)}
|
||||
alt=""
|
||||
style={{
|
||||
display: 'block',
|
||||
position: 'fixed',
|
||||
zIndex: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: 400,
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
<div
|
||||
className={classes.contentContainer}
|
||||
style={{ zIndex: 200 }}
|
||||
ref={ref}
|
||||
>
|
||||
<BreadcrumbNav />
|
||||
<Proclamation toast={uiConfig.toast} />
|
||||
{children}
|
||||
</div>
|
||||
</Grid>
|
||||
<img
|
||||
src={formatAssetPath(textureImage)}
|
||||
alt=""
|
||||
style={{
|
||||
display: 'block',
|
||||
position: 'fixed',
|
||||
zIndex: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: 400,
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ exports[`returns all baseRoutes 1`] = `
|
||||
[
|
||||
{
|
||||
"component": [Function],
|
||||
"isStandalone": true,
|
||||
"menu": {},
|
||||
"path": "/splash/:splashId",
|
||||
"title": "Unleash",
|
||||
@ -78,6 +79,7 @@ exports[`returns all baseRoutes 1`] = `
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "P",
|
||||
"isStandalone": true,
|
||||
"menu": {},
|
||||
"parent": "/projects",
|
||||
"path": "/projects/:projectId/*",
|
||||
|
@ -68,6 +68,7 @@ export const routes: IRoute[] = [
|
||||
component: SplashPage,
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
isStandalone: true,
|
||||
},
|
||||
|
||||
// Project
|
||||
@ -145,6 +146,7 @@ export const routes: IRoute[] = [
|
||||
flag: P,
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
isStandalone: true,
|
||||
},
|
||||
{
|
||||
path: '/projects',
|
||||
@ -546,6 +548,7 @@ export const routes: IRoute[] = [
|
||||
type: 'unprotected',
|
||||
hidden: true,
|
||||
menu: {},
|
||||
isStandalone: true,
|
||||
},
|
||||
/* If you update this route path, make sure you update the path in SWRProvider.tsx */
|
||||
{
|
||||
@ -555,6 +558,7 @@ export const routes: IRoute[] = [
|
||||
component: NewUser,
|
||||
type: 'unprotected',
|
||||
menu: {},
|
||||
isStandalone: true,
|
||||
},
|
||||
/* If you update this route path, make sure you update the path in SWRProvider.tsx */
|
||||
{
|
||||
@ -564,6 +568,7 @@ export const routes: IRoute[] = [
|
||||
component: ResetPassword,
|
||||
type: 'unprotected',
|
||||
menu: {},
|
||||
isStandalone: true,
|
||||
},
|
||||
/* If you update this route path, make sure you update the path in SWRProvider.tsx */
|
||||
{
|
||||
@ -573,6 +578,7 @@ export const routes: IRoute[] = [
|
||||
component: ForgottenPassword,
|
||||
type: 'unprotected',
|
||||
menu: {},
|
||||
isStandalone: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -24,6 +24,9 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
|
||||
import { ProjectLog } from './ProjectLog/ProjectLog';
|
||||
import { SuggestedChanges } from './SuggestedChanges/SuggestedChanges';
|
||||
import { DraftBanner } from './SuggestedChanges/DraftBanner/DraftBanner';
|
||||
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
|
||||
|
||||
const StyledDiv = styled('div')(() => ({
|
||||
display: 'flex',
|
||||
@ -53,7 +56,7 @@ const Project = () => {
|
||||
const { classes: styles } = useStyles();
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const { isOss } = useUiConfig();
|
||||
const { isOss, uiConfig } = useUiConfig();
|
||||
const basePath = `/projects/${projectId}`;
|
||||
const projectName = project?.name || projectId;
|
||||
|
||||
@ -65,6 +68,15 @@ const Project = () => {
|
||||
path: basePath,
|
||||
name: 'overview',
|
||||
},
|
||||
...(uiConfig?.flags?.suggestChanges
|
||||
? [
|
||||
{
|
||||
title: 'Suggested changes',
|
||||
path: `${basePath}/changes`,
|
||||
name: 'changes',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Health',
|
||||
path: `${basePath}/health`,
|
||||
@ -112,7 +124,10 @@ const Project = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<MainLayout
|
||||
ref={ref}
|
||||
subheader={uiConfig?.flags?.suggestChanges ? <DraftBanner /> : null}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.innerContainer}>
|
||||
<h2 className={styles.title}>
|
||||
@ -213,6 +228,7 @@ const Project = () => {
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="changes" element={<SuggestedChanges />} />
|
||||
<Route path="health" element={<ProjectHealth />} />
|
||||
<Route path="access/*" element={<ProjectAccess />} />
|
||||
<Route path="environments" element={<ProjectEnvironment />} />
|
||||
@ -220,7 +236,7 @@ const Project = () => {
|
||||
<Route path="logs" element={<ProjectLog />} />
|
||||
<Route path="*" element={<ProjectOverview />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</MainLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { VFC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||
|
||||
interface IChangesHeaderProps {
|
||||
author?: string;
|
||||
avatar?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export const ChangesHeader: VFC<IChangesHeaderProps> = ({
|
||||
author,
|
||||
avatar,
|
||||
createdAt,
|
||||
}) => {
|
||||
const { locationSettings } = useLocationSettings();
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
||||
data-loading
|
||||
>
|
||||
<div>Suggestion by </div>
|
||||
<div>
|
||||
<UserAvatar src={avatar} />
|
||||
</div>
|
||||
<div>{author}</div>
|
||||
<div>
|
||||
Submitted at:{' '}
|
||||
{formatDateYMDHMS(createdAt || 0, locationSettings.locale)}
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,77 @@
|
||||
import { VFC } from 'react';
|
||||
import { Box, Paper, Typography, Card } from '@mui/material';
|
||||
import { PlaygroundResultChip } from 'component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip'; // FIXME: refactor - extract to common
|
||||
import { ISuggestChange } from 'interfaces/suggestChangeset';
|
||||
|
||||
type ChangesetDiffProps = {
|
||||
changeset?: ISuggestChange[];
|
||||
};
|
||||
|
||||
export const ChangesetDiff: VFC<ChangesetDiffProps> = ({
|
||||
changeset: changeSet,
|
||||
}) => (
|
||||
<Paper
|
||||
elevation={4}
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
p: 2,
|
||||
borderColor: theme => theme.palette.dividerAlternative,
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
flexDirection: 'column',
|
||||
borderRadius: theme => `${theme.shape.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3">Changes</Typography>
|
||||
{/*// @ts-ignore FIXME: types */}
|
||||
{changeSet?.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 }}>
|
||||
{/*
|
||||
// @ts-ignore FIXME: types */}
|
||||
{item?.changes?.map(change => {
|
||||
if (change?.action === 'updateEnabled') {
|
||||
return (
|
||||
<Box key={change?.id}>
|
||||
New status:{' '}
|
||||
<PlaygroundResultChip
|
||||
showIcon={false}
|
||||
label={
|
||||
change?.payload
|
||||
? 'Enabled'
|
||||
: 'Disabled'
|
||||
}
|
||||
enabled={change?.payload}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box key={change.id}>
|
||||
Change with ID: {change.id}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Paper>
|
||||
);
|
@ -0,0 +1,63 @@
|
||||
import { VFC } from 'react';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
import { useStyles as useAppStyles } from 'component/App.styles';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface IDraftBannerProps {
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export const DraftBanner: VFC<IDraftBannerProps> = ({ environment }) => {
|
||||
const { classes } = useAppStyles();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: theme => theme.zIndex.appBar,
|
||||
borderTop: theme => `1px solid ${theme.palette.warning.border}`,
|
||||
borderBottom: theme =>
|
||||
`1px solid ${theme.palette.warning.border}`,
|
||||
backgroundColor: theme => theme.palette.warning.light,
|
||||
}}
|
||||
>
|
||||
<Box className={classes.content}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: 1,
|
||||
py: 1.5,
|
||||
color: theme => theme.palette.warning.main,
|
||||
}}
|
||||
>
|
||||
<WarningAmberIcon />
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
||||
<strong>Draft mode!</strong> – You have changes{' '}
|
||||
<ConditionallyRender
|
||||
condition={Boolean(environment)}
|
||||
show={
|
||||
<>
|
||||
in <strong>{environment} </strong>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
that need to be reviewed
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {}}
|
||||
sx={{ ml: 'auto' }}
|
||||
>
|
||||
Review changes
|
||||
</Button>
|
||||
<Button variant="text" onClick={() => {}} sx={{ ml: 1 }}>
|
||||
Discard all
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,149 @@
|
||||
import { useState, VFC } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Button,
|
||||
Typography,
|
||||
Popover,
|
||||
Radio,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
RadioGroup,
|
||||
} from '@mui/material';
|
||||
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ChangesetDiff } from './ChangesetDiff/ChangesetDiff';
|
||||
import { ChangesHeader } from './ChangesHeader/ChangesHeader';
|
||||
|
||||
export const SuggestedChanges: VFC = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const [selectedValue, setSelectedValue] = useState('');
|
||||
const { data: changeRequest } = useChangeRequest();
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const onClose = () => setAnchorEl(null);
|
||||
|
||||
const onRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedValue((event.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const onSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
if (selectedValue === 'approve') {
|
||||
console.log('approve');
|
||||
} else if (selectedValue === 'requestChanges') {
|
||||
console.log('requestChanges');
|
||||
}
|
||||
// show an error if no action was selected
|
||||
};
|
||||
|
||||
const onApply = async () => {
|
||||
try {
|
||||
console.log('apply');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 4,
|
||||
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`,
|
||||
}}
|
||||
>
|
||||
<Typography>{changeRequest?.state}</Typography>
|
||||
Environment: {changeRequest?.environment}
|
||||
<br />
|
||||
{/* <ChangesHeader
|
||||
author={changeRequest?.createdBy?.name}
|
||||
avatar={changeRequest?.createdBy?.imageUrl}
|
||||
createdAt={changeRequest?.createdAt}
|
||||
/> */}
|
||||
<br />
|
||||
<ChangesetDiff changeset={changeRequest?.changes} />
|
||||
<ConditionallyRender
|
||||
condition={changeRequest?.state === 'APPLIED'}
|
||||
show={<Typography>Applied</Typography>}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={changeRequest?.state === 'APPROVED'}
|
||||
show={
|
||||
<>
|
||||
<Button
|
||||
sx={{ mt: 2 }}
|
||||
variant="contained"
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply changes
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={changeRequest?.state === 'REVIEW'}
|
||||
show={
|
||||
<>
|
||||
<Button
|
||||
sx={{ mt: 2 }}
|
||||
variant="contained"
|
||||
onClick={onClick}
|
||||
>
|
||||
Review changes
|
||||
</Button>
|
||||
<Popover
|
||||
id={'review-popover'}
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
padding: '1rem 2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={selectedValue}
|
||||
onChange={onRadioChange}
|
||||
name="review-actions-radio"
|
||||
>
|
||||
<FormControlLabel
|
||||
value="approve"
|
||||
control={<Radio />}
|
||||
label="Approve"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="requestChanges"
|
||||
control={<Radio />}
|
||||
label="Request changes"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
@ -0,0 +1,50 @@
|
||||
// import useSWR from 'swr';
|
||||
// import { formatApiPath } from 'utils/formatPath';
|
||||
import { ISuggestChangeset } from 'interfaces/suggestChangeset';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
// FIXME: mock
|
||||
const data: ISuggestChangeset = {
|
||||
id: 123,
|
||||
environment: 'production',
|
||||
state: 'REVIEW',
|
||||
createdAt: new Date('2021-03-01T12:00:00.000Z'),
|
||||
project: 'default',
|
||||
createdBy: '123412341',
|
||||
changes: [
|
||||
{
|
||||
id: 1,
|
||||
feature: 'feature1',
|
||||
action: 'updateEnabled',
|
||||
payload: true,
|
||||
createdAt: new Date('2021-03-01T12:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
feature: 'feature2',
|
||||
action: 'updateEnabled',
|
||||
payload: false,
|
||||
createdAt: new Date('2022-09-30T16:34:00.000Z'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const useChangeRequest = () => {
|
||||
// const { data, error, mutate } = useSWR(
|
||||
// formatApiPath(`api/admin/suggest-changes/${id}`),
|
||||
// fetcher
|
||||
// );
|
||||
|
||||
return {
|
||||
data,
|
||||
// loading: !error && !data,
|
||||
// refetchChangeRequest: () => mutate(),
|
||||
// error,
|
||||
};
|
||||
};
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Request changes'))
|
||||
.then(res => res.json());
|
||||
};
|
@ -13,6 +13,7 @@ export interface IRoute {
|
||||
enterprise?: boolean;
|
||||
component: VoidFunctionComponent;
|
||||
menu: IRouteMenu;
|
||||
isStandalone?: boolean;
|
||||
}
|
||||
|
||||
interface IRouteMenu {
|
||||
|
52
frontend/src/interfaces/suggestChangeset.ts
Normal file
52
frontend/src/interfaces/suggestChangeset.ts
Normal file
@ -0,0 +1,52 @@
|
||||
export interface ISuggestChangeset {
|
||||
id: number;
|
||||
state: string;
|
||||
project: string;
|
||||
environment: string;
|
||||
createdBy?: string;
|
||||
createdAt?: Date;
|
||||
changes?: ISuggestChange[];
|
||||
events?: ISuggestChangeEvent[];
|
||||
}
|
||||
|
||||
export interface ISuggestChange {
|
||||
id: number;
|
||||
action:
|
||||
| 'updateEnabled'
|
||||
| 'strategyAdd'
|
||||
| 'strategyUpdate'
|
||||
| 'strategyDelete';
|
||||
feature: string;
|
||||
payload?: unknown;
|
||||
createdBy?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export enum SuggestChangesetEvent {
|
||||
CREATED = 'CREATED',
|
||||
UPDATED = 'UPDATED',
|
||||
SUBMITTED = 'SUBMITTED',
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
CLOSED = 'CLOSED',
|
||||
}
|
||||
|
||||
export enum SuggestChangeEvent {
|
||||
UPDATE_ENABLED = 'updateFeatureEnabledEvent',
|
||||
ADD_STRATEGY = 'addStrategyEvent',
|
||||
UPDATE_STRATEGY = 'updateStrategyEvent',
|
||||
DELETE_STRATEGY = 'deleteStrategyEvent',
|
||||
}
|
||||
|
||||
export interface ISuggestChangeEvent {
|
||||
id: number;
|
||||
event: SuggestChangesetEvent;
|
||||
data: ISuggestChangeEventData;
|
||||
createdBy?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export interface ISuggestChangeEventData {
|
||||
feature: string;
|
||||
data: unknown;
|
||||
}
|
@ -43,6 +43,7 @@ export interface IFlags {
|
||||
publicSignup?: boolean;
|
||||
personalAccessTokens?: boolean;
|
||||
syncSSOGroups?: boolean;
|
||||
suggestChanges?: boolean;
|
||||
}
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { formatRelative } from 'date-fns';
|
||||
|
||||
export const formatDateYMDHMS = (
|
||||
date: number | string | Date,
|
||||
locale: string
|
||||
|
@ -40,6 +40,7 @@ process.nextTick(async () => {
|
||||
responseTimeWithAppName: true,
|
||||
personalAccessTokens: true,
|
||||
syncSSOGroups: true,
|
||||
suggestChanges: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user