mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +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:
parent
95779754fb
commit
15c22d7630
@ -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')]: {
|
||||
|
@ -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<IPageContentProps> = ({
|
||||
disablePadding = false,
|
||||
disableBorder = false,
|
||||
bodyClass = '',
|
||||
headerClass = '',
|
||||
isLoading = false,
|
||||
disableLoading = false,
|
||||
className,
|
||||
@ -47,10 +49,15 @@ export const PageContent: FC<IPageContentProps> = ({
|
||||
}) => {
|
||||
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',
|
||||
|
@ -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<IChangesetStatusCellProps> = ({
|
||||
value,
|
||||
}) => {
|
||||
const renderState = (state: string) => {
|
||||
switch (state) {
|
||||
case SuggestChangesetState.IN_REVIEW:
|
||||
|
@ -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 <TextCell />;
|
||||
|
@ -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',
|
||||
|
@ -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 (
|
||||
<div className={classes.tabContainer}>
|
||||
<Tabs
|
||||
value={activeTab?.title}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
>
|
||||
{tabs.map(tab => (
|
||||
<Tab
|
||||
key={tab.title}
|
||||
label={`${tab.title} (${tab.data.length})`}
|
||||
value={tab.title}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={classes.tabButton}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
headerClass={classes.header}
|
||||
header={
|
||||
<PageHeader
|
||||
titleElement={renderTabs()}
|
||||
titleElement={
|
||||
<div className={classes.tabContainer}>
|
||||
<Tabs
|
||||
value={tabs[activeTab]?.title}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
key={tab.title}
|
||||
label={`${tab.title} (${tab.data.length})`}
|
||||
value={tab.title}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className={classes.tabButton}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
|
@ -16,7 +16,7 @@ export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => {
|
||||
const { draft, loading } = useSuggestedChangesDraft(project);
|
||||
const environment = '';
|
||||
|
||||
if (!loading && !draft) {
|
||||
if (!loading && draft?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,8 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
||||
</Typography>
|
||||
<PlaygroundResultChip
|
||||
// icon={<ChangesAppliedIcon strokeWidth="0.25" />}
|
||||
label="Changes applied"
|
||||
enabled="unknown"
|
||||
label="Changes approved"
|
||||
enabled
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', verticalAlign: 'center', gap: 2 }}>
|
||||
@ -60,7 +60,7 @@ export const SuggestedChangeHeader: FC<{ suggestedChange: any }> = ({
|
||||
</Typography>{' '}
|
||||
| Updates:{' '}
|
||||
<Typography display="inline" fontWeight="bold">
|
||||
{suggestedChange?.changes.length} feature toggles
|
||||
{suggestedChange?.features.length} feature toggles
|
||||
</Typography>
|
||||
</Card>
|
||||
</Box>
|
||||
|
@ -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 = () => {
|
||||
})}
|
||||
>
|
||||
<SuggestedChangeset suggestedChange={suggestedChange} />
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ marginTop: 2 }}
|
||||
onClick={onApplyChanges}
|
||||
>
|
||||
Apply changes
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
@ -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<ISuggestedChangesSidebarProps> = ({
|
||||
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<ISuggestedChangesSidebarProps> = ({
|
||||
<Button
|
||||
sx={{ mt: 2, ml: 'auto' }}
|
||||
variant="contained"
|
||||
onClick={onReview}
|
||||
onClick={() =>
|
||||
onReview(
|
||||
environmentChangeset.id
|
||||
)
|
||||
}
|
||||
>
|
||||
Request changes
|
||||
</Button>
|
||||
|
@ -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"
|
||||
>
|
||||
<div
|
||||
className="header css-1ywhhai-headerContainer"
|
||||
className="header css-zq4ve2-headerContainer css-70tvrt-headerPadding"
|
||||
>
|
||||
<div
|
||||
className="css-1ylehva-headerContainer"
|
||||
|
@ -10,12 +10,13 @@ interface ISuggestChangeSchema {
|
||||
payload: string | boolean | object | number;
|
||||
}
|
||||
|
||||
export const useSuggestChangeApi = (project: string) => {
|
||||
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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user