diff --git a/frontend/src/component/common/PageContent/PageContent.styles.ts b/frontend/src/component/common/PageContent/PageContent.styles.ts index d9ead5943b..ff8a025c55 100644 --- a/frontend/src/component/common/PageContent/PageContent.styles.ts +++ b/frontend/src/component/common/PageContent/PageContent.styles.ts @@ -6,7 +6,6 @@ export const useStyles = makeStyles()(theme => ({ boxShadow: 'none', }, headerContainer: { - padding: theme.spacing(2, 4), borderBottomStyle: 'solid', borderBottomWidth: 1, borderBottomColor: theme.palette.divider, @@ -14,6 +13,9 @@ export const useStyles = makeStyles()(theme => ({ padding: '1.5rem 1rem', }, }, + headerPadding: { + padding: theme.spacing(2, 4), + }, bodyContainer: { padding: theme.spacing(4), [theme.breakpoints.down('md')]: { diff --git a/frontend/src/component/common/PageContent/PageContent.tsx b/frontend/src/component/common/PageContent/PageContent.tsx index e96973eea2..33a6da6ab6 100644 --- a/frontend/src/component/common/PageContent/PageContent.tsx +++ b/frontend/src/component/common/PageContent/PageContent.tsx @@ -19,6 +19,7 @@ interface IPageContentProps extends PaperProps { disableBorder?: boolean; disableLoading?: boolean; bodyClass?: string; + headerClass?: string; } const PageContentLoading: FC<{ isLoading: boolean }> = ({ @@ -40,6 +41,7 @@ export const PageContent: FC = ({ disablePadding = false, disableBorder = false, bodyClass = '', + headerClass = '', isLoading = false, disableLoading = false, className, @@ -47,10 +49,15 @@ export const PageContent: FC = ({ }) => { const { classes: styles } = useStyles(); - const headerClasses = classnames('header', styles.headerContainer, { - [styles.paddingDisabled]: disablePadding, - [styles.borderDisabled]: disableBorder, - }); + const headerClasses = classnames( + 'header', + styles.headerContainer, + headerClass || styles.headerPadding, + { + [styles.paddingDisabled]: disablePadding, + [styles.borderDisabled]: disableBorder, + } + ); const bodyClasses = classnames( 'body', diff --git a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetStatusCell/ChangesetStatusCell.tsx b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetStatusCell/ChangesetStatusCell.tsx index 6b9f3c9994..697c65bb2d 100644 --- a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetStatusCell/ChangesetStatusCell.tsx +++ b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetStatusCell/ChangesetStatusCell.tsx @@ -1,6 +1,7 @@ +import { VFC } from 'react'; import { Chip, styled } from '@mui/material'; -import { colors } from '../../../../../themes/colors'; -import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell'; +import { colors } from 'themes/colors'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { Check, CircleOutlined, Close } from '@mui/icons-material'; interface IChangesetStatusCellProps { @@ -60,7 +61,10 @@ export const StyledReviewChip = styled(StyledChip)(({ theme }) => ({ color: theme.palette.primary.main, }, })); -export const ChangesetStatusCell = ({ value }: IChangesetStatusCellProps) => { + +export const ChangesetStatusCell: VFC = ({ + value, +}) => { const renderState = (state: string) => { switch (state) { case SuggestChangesetState.IN_REVIEW: diff --git a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetTitleCell/ChangesetTitleCell.tsx b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetTitleCell/ChangesetTitleCell.tsx index df4dd0e128..b19ede44e6 100644 --- a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetTitleCell/ChangesetTitleCell.tsx +++ b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/ChangesetTitleCell/ChangesetTitleCell.tsx @@ -2,6 +2,7 @@ import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell'; import { Link, styled, Typography } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; import { useTheme } from '@mui/system'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; interface IChangesetTitleCellProps { value?: any; @@ -18,9 +19,10 @@ export const ChangesetTitleCell = ({ value, row: { original }, }: IChangesetTitleCellProps) => { - const { id, features: changes, project } = original; + const projectId = useRequiredPathParam('projectId'); + const { id, features: changes } = original; const theme = useTheme(); - const path = `projects/${project}/suggest-changes/${id}`; + const path = `/projects/${projectId}/suggest-changes/${id}`; if (!value) { return ; diff --git a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.styles.ts b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.styles.ts index c6f0c8fc97..63916f448b 100644 --- a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.styles.ts +++ b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.styles.ts @@ -1,8 +1,12 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ + header: { + padding: theme.spacing(0, 4), + }, tabContainer: { paddingLeft: 0, + paddingBottom: 0, }, tabButton: { textTransform: 'none', diff --git a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.tsx b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.tsx index a142a301e1..b646fd1e97 100644 --- a/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.tsx +++ b/frontend/src/component/suggest-changes/ProjectSuggestions/SuggestionsTabs/SuggestionsTabs.tsx @@ -77,7 +77,7 @@ export const SuggestionsTabs = ({ }, ]; - const [activeTab, setActiveTab] = useState(tabs[0]); + const [activeTab, setActiveTab] = useState(0); const columns = useMemo( () => [ @@ -137,7 +137,7 @@ export const SuggestionsTabs = ({ data: searchedData, getSearchText, getSearchContext, - } = useSearch(columns, searchValue, activeTab.data); + } = useSearch(columns, searchValue, tabs[activeTab]?.data); const data = useMemo( () => (loading ? featuresPlaceholder : searchedData), @@ -206,34 +206,31 @@ export const SuggestionsTabs = ({ setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false }); }, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps - const renderTabs = () => { - return ( -
- - {tabs.map(tab => ( - setActiveTab(tab)} - className={classes.tabButton} - /> - ))} - -
- ); - }; - return ( + + {tabs.map((tab, index) => ( + setActiveTab(index)} + className={classes.tabButton} + /> + ))} + + + } actions={ = ({ project }) => { const { draft, loading } = useSuggestedChangesDraft(project); const environment = ''; - if (!loading && !draft) { + if (!loading && draft?.length === 0) { return null; } diff --git a/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeHeader/SuggestedChangeHeader.tsx b/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeHeader/SuggestedChangeHeader.tsx index cd21b9466f..cdb30d01f0 100644 --- a/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeHeader/SuggestedChangeHeader.tsx +++ b/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeHeader/SuggestedChangeHeader.tsx @@ -37,8 +37,8 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({ } - label="Changes applied" - enabled="unknown" + label="Changes approved" + enabled /> @@ -60,7 +60,7 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({ {' '} | Updates:{' '} - {suggestedChange?.changes.length} feature toggles + {suggestedChange?.features.length} feature toggles diff --git a/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview.tsx b/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview.tsx index 99b7dad527..9d96eea822 100644 --- a/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview.tsx +++ b/frontend/src/component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview.tsx @@ -1,13 +1,38 @@ 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 { SuggestedChangeHeader } from './SuggestedChangeHeader/SuggestedChangeHeader'; import { SuggestedChangeTimeline } from './SuggestedChangeTimeline/SuggestedChangeTimeline'; import { SuggestedChangeReviewers } from './SuggestedChangeReviewers/SuggestedChangeReviewers'; 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 = () => { - 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 ( <> @@ -40,6 +65,13 @@ export const SuggestedChangeOverview: FC = () => { })} > + diff --git a/frontend/src/component/suggestChanges/SuggestedChangesSidebar/SuggestedChangesSidebar.tsx b/frontend/src/component/suggestChanges/SuggestedChangesSidebar/SuggestedChangesSidebar.tsx index 406118fe9c..84f10fa535 100644 --- a/frontend/src/component/suggestChanges/SuggestedChangesSidebar/SuggestedChangesSidebar.tsx +++ b/frontend/src/component/suggestChanges/SuggestedChangesSidebar/SuggestedChangesSidebar.tsx @@ -7,6 +7,7 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { HelpOutline } from '@mui/icons-material'; import { SuggestedChangeset } from '../SuggestedChangeset/SuggestedChangeset'; import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft'; +import { useSuggestChangeApi } from 'hooks/api/actions/useSuggestChangeApi/useSuggestChangeApi'; interface ISuggestedChangesSidebarProps { open: boolean; @@ -45,10 +46,20 @@ export const SuggestedChangesSidebar: VFC = ({ project, onClose, }) => { - const { draft, loading } = useSuggestedChangesDraft(project); + const { + draft, + loading, + refetch: refetchSuggestedChanges, + } = useSuggestedChangesDraft(project); + const { changeState } = useSuggestChangeApi(); - const onReview = async () => { - alert('approve'); + const onReview = async (draftId: number) => { + try { + await changeState(project, draftId, { state: 'In review' }); + refetchSuggestedChanges(); + } catch (e) { + console.log('something went wrong'); + } }; const onDiscard = async () => { alert('discard'); @@ -163,7 +174,11 @@ export const SuggestedChangesSidebar: VFC = ({ diff --git a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap index a445801c93..321be66814 100644 --- a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap +++ b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap @@ -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" >
{ +export const useSuggestChangeApi = () => { const { makeRequest, createRequest, errors, loading } = useAPI({ propagateErrors: true, }); const addSuggestion = async ( + project: string, environment: string, payload: ISuggestChangeSchema ) => { @@ -26,7 +27,38 @@ export const useSuggestChangeApi = (project: string) => { }); try { 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) { throw e; } @@ -34,6 +66,8 @@ export const useSuggestChangeApi = (project: string) => { return { addSuggestion, + applyChanges, + changeState, errors, loading, }; diff --git a/frontend/src/hooks/api/getters/useSuggestChange/useSuggestedChange.ts b/frontend/src/hooks/api/getters/useSuggestChange/useSuggestedChange.ts index 265e7afee0..94983bbb08 100644 --- a/frontend/src/hooks/api/getters/useSuggestChange/useSuggestedChange.ts +++ b/frontend/src/hooks/api/getters/useSuggestChange/useSuggestedChange.ts @@ -1,99 +1,18 @@ -// import useSWR from 'swr'; -// import { formatApiPath } from 'utils/formatPath'; -import { ISuggestChangeset } from 'interfaces/suggestChangeset'; +import useSWR from 'swr'; +import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; -// FIXME: mock -const data: any = { - id: '12', - environment: 'production', - 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 - // ); +export const useSuggestedChange = (projectId: string, id: string) => { + const { data, error, mutate } = useSWR( + formatApiPath(`api/admin/projects/${projectId}/suggest-changes/${id}`), + fetcher + ); return { data, - // loading: !error && !data, - // refetchChangeRequest: () => mutate(), - // error, + loading: !error && !data, + refetchSuggestedChange: () => mutate(), + error, }; }; diff --git a/frontend/src/hooks/useSuggestToggle.ts b/frontend/src/hooks/useSuggestToggle.ts index 5048164bb4..e6e776899e 100644 --- a/frontend/src/hooks/useSuggestToggle.ts +++ b/frontend/src/hooks/useSuggestToggle.ts @@ -2,10 +2,14 @@ import { useCallback, useState } from 'react'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { useSuggestChangeApi } from './api/actions/useSuggestChangeApi/useSuggestChangeApi'; +import { useSuggestedChangesDraft } from './api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft'; export const useSuggestToggle = (project: string) => { const { setToastData, setToastApiError } = useToast(); - const { addSuggestion } = useSuggestChangeApi(project); + const { addSuggestion } = useSuggestChangeApi(); + const { refetch: refetchSuggestedChange } = + useSuggestedChangesDraft(project); + const [suggestChangesDialogDetails, setSuggestChangesDialogDetails] = useState<{ enabled?: boolean; @@ -32,13 +36,18 @@ export const useSuggestToggle = (project: string) => { const onSuggestToggleConfirm = useCallback(async () => { try { - await addSuggestion(suggestChangesDialogDetails.environment!, { - feature: suggestChangesDialogDetails.featureName!, - action: 'updateEnabled', - payload: { - enabled: Boolean(suggestChangesDialogDetails.enabled), - }, - }); + await addSuggestion( + project, + suggestChangesDialogDetails.environment!, + { + feature: suggestChangesDialogDetails.featureName!, + action: 'updateEnabled', + payload: { + enabled: Boolean(suggestChangesDialogDetails.enabled), + }, + } + ); + refetchSuggestedChange(); setSuggestChangesDialogDetails({ isOpen: false }); setToastData({ type: 'success',