1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02:00

feat: integrate with API for suggest changes (#2286)

* feat: integrate with API for suggest changes

* fix: suggestions table tabs state (#2287)

* fix: suggestions table tabs state

* fix suggestion header padding

* fix: update snapshots

* fix: pr comments

* fix: revert store change

* fix: revert store fix

Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
This commit is contained in:
Fredrik Strand Oseberg 2022-10-31 12:46:31 +01:00 committed by GitHub
parent 95779754fb
commit 15c22d7630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 172 additions and 147 deletions

View File

@ -6,7 +6,6 @@ export const useStyles = makeStyles()(theme => ({
boxShadow: 'none', boxShadow: 'none',
}, },
headerContainer: { headerContainer: {
padding: theme.spacing(2, 4),
borderBottomStyle: 'solid', borderBottomStyle: 'solid',
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: theme.palette.divider, borderBottomColor: theme.palette.divider,
@ -14,6 +13,9 @@ export const useStyles = makeStyles()(theme => ({
padding: '1.5rem 1rem', padding: '1.5rem 1rem',
}, },
}, },
headerPadding: {
padding: theme.spacing(2, 4),
},
bodyContainer: { bodyContainer: {
padding: theme.spacing(4), padding: theme.spacing(4),
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {

View File

@ -19,6 +19,7 @@ interface IPageContentProps extends PaperProps {
disableBorder?: boolean; disableBorder?: boolean;
disableLoading?: boolean; disableLoading?: boolean;
bodyClass?: string; bodyClass?: string;
headerClass?: string;
} }
const PageContentLoading: FC<{ isLoading: boolean }> = ({ const PageContentLoading: FC<{ isLoading: boolean }> = ({
@ -40,6 +41,7 @@ export const PageContent: FC<IPageContentProps> = ({
disablePadding = false, disablePadding = false,
disableBorder = false, disableBorder = false,
bodyClass = '', bodyClass = '',
headerClass = '',
isLoading = false, isLoading = false,
disableLoading = false, disableLoading = false,
className, className,
@ -47,10 +49,15 @@ export const PageContent: FC<IPageContentProps> = ({
}) => { }) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const headerClasses = classnames('header', styles.headerContainer, { const headerClasses = classnames(
'header',
styles.headerContainer,
headerClass || styles.headerPadding,
{
[styles.paddingDisabled]: disablePadding, [styles.paddingDisabled]: disablePadding,
[styles.borderDisabled]: disableBorder, [styles.borderDisabled]: disableBorder,
}); }
);
const bodyClasses = classnames( const bodyClasses = classnames(
'body', 'body',

View File

@ -1,6 +1,7 @@
import { VFC } from 'react';
import { Chip, styled } from '@mui/material'; import { Chip, styled } from '@mui/material';
import { colors } from '../../../../../themes/colors'; import { colors } from 'themes/colors';
import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Check, CircleOutlined, Close } from '@mui/icons-material'; import { Check, CircleOutlined, Close } from '@mui/icons-material';
interface IChangesetStatusCellProps { interface IChangesetStatusCellProps {
@ -60,7 +61,10 @@ export const StyledReviewChip = styled(StyledChip)(({ theme }) => ({
color: theme.palette.primary.main, color: theme.palette.primary.main,
}, },
})); }));
export const ChangesetStatusCell = ({ value }: IChangesetStatusCellProps) => {
export const ChangesetStatusCell: VFC<IChangesetStatusCellProps> = ({
value,
}) => {
const renderState = (state: string) => { const renderState = (state: string) => {
switch (state) { switch (state) {
case SuggestChangesetState.IN_REVIEW: case SuggestChangesetState.IN_REVIEW:

View File

@ -2,6 +2,7 @@ import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell';
import { Link, styled, Typography } from '@mui/material'; import { Link, styled, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { useTheme } from '@mui/system'; import { useTheme } from '@mui/system';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
interface IChangesetTitleCellProps { interface IChangesetTitleCellProps {
value?: any; value?: any;
@ -18,9 +19,10 @@ export const ChangesetTitleCell = ({
value, value,
row: { original }, row: { original },
}: IChangesetTitleCellProps) => { }: IChangesetTitleCellProps) => {
const { id, features: changes, project } = original; const projectId = useRequiredPathParam('projectId');
const { id, features: changes } = original;
const theme = useTheme(); const theme = useTheme();
const path = `projects/${project}/suggest-changes/${id}`; const path = `/projects/${projectId}/suggest-changes/${id}`;
if (!value) { if (!value) {
return <TextCell />; return <TextCell />;

View File

@ -1,8 +1,12 @@
import { makeStyles } from 'tss-react/mui'; import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({ export const useStyles = makeStyles()(theme => ({
header: {
padding: theme.spacing(0, 4),
},
tabContainer: { tabContainer: {
paddingLeft: 0, paddingLeft: 0,
paddingBottom: 0,
}, },
tabButton: { tabButton: {
textTransform: 'none', textTransform: 'none',

View File

@ -77,7 +77,7 @@ export const SuggestionsTabs = ({
}, },
]; ];
const [activeTab, setActiveTab] = useState(tabs[0]); const [activeTab, setActiveTab] = useState(0);
const columns = useMemo( const columns = useMemo(
() => [ () => [
@ -137,7 +137,7 @@ export const SuggestionsTabs = ({
data: searchedData, data: searchedData,
getSearchText, getSearchText,
getSearchContext, getSearchContext,
} = useSearch(columns, searchValue, activeTab.data); } = useSearch(columns, searchValue, tabs[activeTab]?.data);
const data = useMemo( const data = useMemo(
() => (loading ? featuresPlaceholder : searchedData), () => (loading ? featuresPlaceholder : searchedData),
@ -206,34 +206,31 @@ export const SuggestionsTabs = ({
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false }); setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
}, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps }, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
const renderTabs = () => {
return ( return (
<PageContent
isLoading={loading}
headerClass={classes.header}
header={
<PageHeader
titleElement={
<div className={classes.tabContainer}> <div className={classes.tabContainer}>
<Tabs <Tabs
value={activeTab?.title} value={tabs[activeTab]?.title}
indicatorColor="primary" indicatorColor="primary"
textColor="primary" textColor="primary"
> >
{tabs.map(tab => ( {tabs.map((tab, index) => (
<Tab <Tab
key={tab.title} key={tab.title}
label={`${tab.title} (${tab.data.length})`} label={`${tab.title} (${tab.data.length})`}
value={tab.title} value={tab.title}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(index)}
className={classes.tabButton} className={classes.tabButton}
/> />
))} ))}
</Tabs> </Tabs>
</div> </div>
); }
};
return (
<PageContent
isLoading={loading}
header={
<PageHeader
titleElement={renderTabs()}
actions={ actions={
<Search <Search
initialValue={searchValue} initialValue={searchValue}

View File

@ -16,7 +16,7 @@ export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => {
const { draft, loading } = useSuggestedChangesDraft(project); const { draft, loading } = useSuggestedChangesDraft(project);
const environment = ''; const environment = '';
if (!loading && !draft) { if (!loading && draft?.length === 0) {
return null; return null;
} }

View File

@ -37,8 +37,8 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
</Typography> </Typography>
<PlaygroundResultChip <PlaygroundResultChip
// icon={<ChangesAppliedIcon strokeWidth="0.25" />} // icon={<ChangesAppliedIcon strokeWidth="0.25" />}
label="Changes applied" label="Changes approved"
enabled="unknown" enabled
/> />
</Box> </Box>
<Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}> <Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}>
@ -60,7 +60,7 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
</Typography>{' '} </Typography>{' '}
| Updates:{' '} | Updates:{' '}
<Typography display="inline" fontWeight="bold"> <Typography display="inline" fontWeight="bold">
{suggestedChange?.changes.length} feature toggles {suggestedChange?.features.length} feature toggles
</Typography> </Typography>
</Card> </Card>
</Box> </Box>

View File

@ -1,13 +1,38 @@
import { FC } from 'react'; import { FC } from 'react';
import { Box, Paper } from '@mui/material'; import { Box, Button, 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';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useSuggestChangeApi } from 'hooks/api/actions/useSuggestChangeApi/useSuggestChangeApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
export const SuggestedChangeOverview: FC = () => { export const SuggestedChangeOverview: FC = () => {
const { data: suggestedChange } = useSuggestedChange(); const projectId = useRequiredPathParam('projectId');
const id = useRequiredPathParam('id');
const { data: suggestedChange } = useSuggestedChange(projectId, id);
const { applyChanges } = useSuggestChangeApi();
const { setToastData, setToastApiError } = useToast();
if (!suggestedChange) {
return null;
}
const onApplyChanges = async () => {
try {
await applyChanges(projectId, id);
setToastData({
type: 'success',
title: 'Success',
text: 'Changes appplied',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
return ( return (
<> <>
@ -40,6 +65,13 @@ export const SuggestedChangeOverview: FC = () => {
})} })}
> >
<SuggestedChangeset suggestedChange={suggestedChange} /> <SuggestedChangeset suggestedChange={suggestedChange} />
<Button
variant="contained"
sx={{ marginTop: 2 }}
onClick={onApplyChanges}
>
Apply changes
</Button>
</Box> </Box>
</Paper> </Paper>
</Box> </Box>

View File

@ -7,6 +7,7 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { HelpOutline } from '@mui/icons-material'; import { HelpOutline } from '@mui/icons-material';
import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset'; import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset';
import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft'; import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
import { useSuggestChangeApi } from 'hooks/api/actions/useSuggestChangeApi/useSuggestChangeApi';
interface ISuggestedChangesSidebarProps { interface ISuggestedChangesSidebarProps {
open: boolean; open: boolean;
@ -45,10 +46,20 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
project, project,
onClose, onClose,
}) => { }) => {
const { draft, loading } = useSuggestedChangesDraft(project); const {
draft,
loading,
refetch: refetchSuggestedChanges,
} = useSuggestedChangesDraft(project);
const { changeState } = useSuggestChangeApi();
const onReview = async () => { const onReview = async (draftId: number) => {
alert('approve'); try {
await changeState(project, draftId, { state: 'In review' });
refetchSuggestedChanges();
} catch (e) {
console.log('something went wrong');
}
}; };
const onDiscard = async () => { const onDiscard = async () => {
alert('discard'); alert('discard');
@ -163,7 +174,11 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
<Button <Button
sx={{ mt: 2, ml: 'auto' }} sx={{ mt: 2, ml: 'auto' }}
variant="contained" variant="contained"
onClick={onReview} onClick={() =>
onReview(
environmentChangeset.id
)
}
> >
Request changes Request changes
</Button> </Button>

View File

@ -10,7 +10,7 @@ exports[`renders an empty list correctly 1`] = `
className="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 css-p9j8ie-MuiPaper-root-container" className="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 css-p9j8ie-MuiPaper-root-container"
> >
<div <div
className="header css-1ywhhai-headerContainer" className="header css-zq4ve2-headerContainer css-70tvrt-headerPadding"
> >
<div <div
className="css-1ylehva-headerContainer" className="css-1ylehva-headerContainer"

View File

@ -10,12 +10,13 @@ interface ISuggestChangeSchema {
payload: string | boolean | object | number; payload: string | boolean | object | number;
} }
export const useSuggestChangeApi = (project: string) => { export const useSuggestChangeApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true, propagateErrors: true,
}); });
const addSuggestion = async ( const addSuggestion = async (
project: string,
environment: string, environment: string,
payload: ISuggestChangeSchema payload: ISuggestChangeSchema
) => { ) => {
@ -26,7 +27,38 @@ export const useSuggestChangeApi = (project: string) => {
}); });
try { try {
const response = await makeRequest(req.caller, req.id); const response = await makeRequest(req.caller, req.id);
return await response.json(); return response.json();
} catch (e) {
throw e;
}
};
const changeState = async (
project: string,
suggestChangeId: number,
payload: any
) => {
const path = `api/admin/projects/${project}/suggest-changes/${suggestChangeId}/state`;
const req = createRequest(path, {
method: 'PUT',
body: JSON.stringify(payload),
});
try {
const response = await makeRequest(req.caller, req.id);
return response.json();
} catch (e) {
throw e;
}
};
const applyChanges = async (project: string, suggestChangeId: string) => {
const path = `api/admin/projects/${project}/suggest-changes/${suggestChangeId}/apply`;
const req = createRequest(path, {
method: 'PUT',
});
try {
const response = await makeRequest(req.caller, req.id);
return response;
} catch (e) { } catch (e) {
throw e; throw e;
} }
@ -34,6 +66,8 @@ export const useSuggestChangeApi = (project: string) => {
return { return {
addSuggestion, addSuggestion,
applyChanges,
changeState,
errors, errors,
loading, loading,
}; };

View File

@ -1,99 +1,18 @@
// import useSWR from 'swr'; import useSWR from 'swr';
// import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import { ISuggestChangeset } from 'interfaces/suggestChangeset';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
// FIXME: mock export const useSuggestedChange = (projectId: string, id: string) => {
const data: any = { const { data, error, mutate } = useSWR(
id: '12', formatApiPath(`api/admin/projects/${projectId}/suggest-changes/${id}`),
environment: 'production', fetcher
state: 'DRAFT', );
project: 'default',
createdBy: {
email: 'mateusz@getunleash.ai',
avatar: 'https://gravatar-uri.com/1321',
},
createdAt: '2020-10-20T12:00:00.000Z',
changes: [
{
feature: 'my-feature-toggle',
changeSet: [
{
id: 'f79d399f-cb38-4982-b9b6-4141sdsdaad',
action: 'updateEnabled',
payload: { data: { data: true } },
},
{
id: 'f79d399f-cb38-4982-b9b6-4141sdsdaad',
action: 'addStrategy',
payload: {
name: 'flexibleRollout',
constraints: [],
parameters: {
rollout: '50',
stickiness: 'default',
groupId: 'suggest-changes',
},
},
},
{
id: 'f79d399f-cb38-4982-b9b6-4141sdsdaad',
action: 'updateStrategy',
payload: {
data: {},
},
},
{
id: 'f79d399f-cb38-4982-b9b6-4141sdsdaad',
action: 'deleteStrategy',
payload: {
data: {},
},
},
],
},
{
feature: 'new-feature-toggle',
changeSet: [
{
id: 'f79d399f-cb38-4982-b9b6-4141sdsdaad',
action: 'updateEnabled',
payload: {
data: { data: false },
strategyId: '123-14',
},
},
],
},
{
feature: 'add-strategy-feature-toggle',
changeSet: [
{
id: 'f79d399f-cb38-4982-b9b6-4141sdsdaad',
action: 'addStrategy',
payload: {
data: {},
},
},
],
},
],
};
/**
* @deprecated for draft: useSuggestedChangesDraft
*/
export const useSuggestedChange = () => {
// const { data, error, mutate } = useSWR(
// formatApiPath(`api/admin/suggest-changes/${id}`),
// fetcher
// );
return { return {
data, data,
// loading: !error && !data, loading: !error && !data,
// refetchChangeRequest: () => mutate(), refetchSuggestedChange: () => mutate(),
// error, error,
}; };
}; };

View File

@ -2,10 +2,14 @@ import { useCallback, useState } from 'react';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useSuggestChangeApi } from './api/actions/useSuggestChangeApi/useSuggestChangeApi'; import { useSuggestChangeApi } from './api/actions/useSuggestChangeApi/useSuggestChangeApi';
import { useSuggestedChangesDraft } from './api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
export const useSuggestToggle = (project: string) => { export const useSuggestToggle = (project: string) => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { addSuggestion } = useSuggestChangeApi(project); const { addSuggestion } = useSuggestChangeApi();
const { refetch: refetchSuggestedChange } =
useSuggestedChangesDraft(project);
const [suggestChangesDialogDetails, setSuggestChangesDialogDetails] = const [suggestChangesDialogDetails, setSuggestChangesDialogDetails] =
useState<{ useState<{
enabled?: boolean; enabled?: boolean;
@ -32,13 +36,18 @@ export const useSuggestToggle = (project: string) => {
const onSuggestToggleConfirm = useCallback(async () => { const onSuggestToggleConfirm = useCallback(async () => {
try { try {
await addSuggestion(suggestChangesDialogDetails.environment!, { await addSuggestion(
project,
suggestChangesDialogDetails.environment!,
{
feature: suggestChangesDialogDetails.featureName!, feature: suggestChangesDialogDetails.featureName!,
action: 'updateEnabled', action: 'updateEnabled',
payload: { payload: {
enabled: Boolean(suggestChangesDialogDetails.enabled), enabled: Boolean(suggestChangesDialogDetails.enabled),
}, },
}); }
);
refetchSuggestedChange();
setSuggestChangesDialogDetails({ isOpen: false }); setSuggestChangesDialogDetails({ isOpen: false });
setToastData({ setToastData({
type: 'success', type: 'success',