1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: get suggested changeset draft (#2274)

* feat: get suggested changeset draft

* fix: update routes snapshot
This commit is contained in:
Tymoteusz Czech 2022-10-28 09:43:49 +02:00 committed by GitHub
parent c6c873d67d
commit b7183fdf98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 347 additions and 194 deletions

View File

@ -30,12 +30,16 @@ import StatusChip from 'component/common/StatusChip/StatusChip';
import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureArchiveDialog } from '../../common/FeatureArchiveDialog/FeatureArchiveDialog';
import { DraftBanner } from 'component/suggestChanges/DraftBanner/DraftBanner';
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
export const FeatureView = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { refetch: projectRefetch } = useProject(projectId);
const { refetchFeature } = useFeature(projectId, featureId);
const { uiConfig } = useUiConfig();
const [openTagDialog, setOpenTagDialog] = useState(false);
const [showDelDialog, setShowDelDialog] = useState(false);
@ -81,123 +85,145 @@ export const FeatureView = () => {
}
return (
<ConditionallyRender
condition={error === undefined}
show={
<div ref={ref}>
<div className={styles.header}>
<div className={styles.innerContainer}>
<div className={styles.toggleInfoContainer}>
<h1
className={styles.featureViewHeader}
data-loading
>
{feature.name}{' '}
</h1>
<ConditionallyRender
condition={!smallScreen}
show={<StatusChip stale={feature?.stale} />}
/>
</div>
<div className={styles.toolbarContainer}>
<PermissionIconButton
permission={CREATE_FEATURE}
projectId={projectId}
data-loading
component={Link}
to={`/projects/${projectId}/features/${featureId}/strategies/copy`}
tooltipProps={{
title: 'Copy feature toggle',
}}
>
<FileCopy />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Archive feature toggle',
}}
data-loading
onClick={() => setShowDelDialog(true)}
>
<Archive />
</PermissionIconButton>
<PermissionIconButton
onClick={() => setOpenStaleDialog(true)}
permission={UPDATE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Toggle stale state',
}}
data-loading
>
<WatchLater />
</PermissionIconButton>
<PermissionIconButton
onClick={() => setOpenTagDialog(true)}
permission={UPDATE_FEATURE}
projectId={projectId}
tooltipProps={{ title: 'Add tag' }}
data-loading
>
<Label />
</PermissionIconButton>
</div>
</div>
<div className={styles.separator} />
<div className={styles.tabContainer}>
<Tabs
value={activeTab.path}
indicatorColor="primary"
textColor="primary"
>
{tabData.map(tab => (
<Tab
key={tab.title}
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
className={styles.tabButton}
/>
))}
</Tabs>
</div>
</div>
<Routes>
<Route path="metrics" element={<FeatureMetrics />} />
<Route path="logs" element={<FeatureLog />} />
<Route path="variants" element={<FeatureVariants />} />
<Route path="settings" element={<FeatureSettings />} />
<Route path="*" element={<FeatureOverview />} />
</Routes>
<FeatureArchiveDialog
isOpen={showDelDialog}
onConfirm={() => {
projectRefetch();
navigate(`/projects/${projectId}`);
}}
onClose={() => setShowDelDialog(false)}
projectId={projectId}
featureId={featureId}
/>
<FeatureStaleDialog
isStale={feature.stale}
isOpen={openStaleDialog}
onClose={() => {
setOpenStaleDialog(false);
refetchFeature();
}}
featureId={featureId}
projectId={projectId}
/>
<AddTagDialog
open={openTagDialog}
setOpen={setOpenTagDialog}
/>
</div>
<MainLayout
ref={ref}
subheader={
uiConfig?.flags?.suggestChanges ? (
<DraftBanner project={projectId} />
) : null
}
/>
>
<ConditionallyRender
condition={error === undefined}
show={
<div ref={ref}>
<div className={styles.header}>
<div className={styles.innerContainer}>
<div className={styles.toggleInfoContainer}>
<h1
className={styles.featureViewHeader}
data-loading
>
{feature.name}{' '}
</h1>
<ConditionallyRender
condition={!smallScreen}
show={
<StatusChip
stale={feature?.stale}
/>
}
/>
</div>
<div className={styles.toolbarContainer}>
<PermissionIconButton
permission={CREATE_FEATURE}
projectId={projectId}
data-loading
component={Link}
to={`/projects/${projectId}/features/${featureId}/strategies/copy`}
tooltipProps={{
title: 'Copy feature toggle',
}}
>
<FileCopy />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Archive feature toggle',
}}
data-loading
onClick={() => setShowDelDialog(true)}
>
<Archive />
</PermissionIconButton>
<PermissionIconButton
onClick={() => setOpenStaleDialog(true)}
permission={UPDATE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Toggle stale state',
}}
data-loading
>
<WatchLater />
</PermissionIconButton>
<PermissionIconButton
onClick={() => setOpenTagDialog(true)}
permission={UPDATE_FEATURE}
projectId={projectId}
tooltipProps={{ title: 'Add tag' }}
data-loading
>
<Label />
</PermissionIconButton>
</div>
</div>
<div className={styles.separator} />
<div className={styles.tabContainer}>
<Tabs
value={activeTab.path}
indicatorColor="primary"
textColor="primary"
>
{tabData.map(tab => (
<Tab
key={tab.title}
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
className={styles.tabButton}
/>
))}
</Tabs>
</div>
</div>
<Routes>
<Route
path="metrics"
element={<FeatureMetrics />}
/>
<Route path="logs" element={<FeatureLog />} />
<Route
path="variants"
element={<FeatureVariants />}
/>
<Route
path="settings"
element={<FeatureSettings />}
/>
<Route path="*" element={<FeatureOverview />} />
</Routes>
<FeatureArchiveDialog
isOpen={showDelDialog}
onConfirm={() => {
projectRefetch();
navigate(`/projects/${projectId}`);
}}
onClose={() => setShowDelDialog(false)}
projectId={projectId}
featureId={featureId}
/>
<FeatureStaleDialog
isStale={feature.stale}
isOpen={openStaleDialog}
onClose={() => {
setOpenStaleDialog(false);
refetchFeature();
}}
featureId={featureId}
projectId={projectId}
/>
<AddTagDialog
open={openTagDialog}
setOpen={setOpenTagDialog}
/>
</div>
}
/>
</MainLayout>
);
};

View File

@ -54,6 +54,7 @@ exports[`returns all baseRoutes 1`] = `
},
{
"component": [Function],
"isStandalone": true,
"menu": {},
"parent": "/projects",
"path": "/projects/:projectId/features/:featureId/*",

View File

@ -121,6 +121,7 @@ export const routes: IRoute[] = [
title: 'FeatureView',
component: FeatureView,
type: 'protected',
isStandalone: true,
menu: {},
},
{

View File

@ -117,7 +117,11 @@ const Project = () => {
return (
<MainLayout
ref={ref}
subheader={uiConfig?.flags?.suggestChanges ? <DraftBanner /> : null}
subheader={
uiConfig?.flags?.suggestChanges ? (
<DraftBanner project={projectId} />
) : null
}
>
<div className={styles.header}>
<div className={styles.innerContainer}>

View File

@ -4,14 +4,21 @@ import { useStyles as useAppStyles } from 'component/App.styles';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SuggestedChangesSidebar } from '../SuggestedChangesSidebar/SuggestedChangesSidebar';
import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
interface IDraftBannerProps {
environment?: string;
project: string;
}
export const DraftBanner: VFC<IDraftBannerProps> = ({ environment }) => {
export const DraftBanner: VFC<IDraftBannerProps> = ({ project }) => {
const { classes } = useAppStyles();
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const { draft, loading } = useSuggestedChangesDraft(project);
const environment = '';
if (!loading && !draft) {
return null;
}
return (
<Box
@ -63,6 +70,7 @@ export const DraftBanner: VFC<IDraftBannerProps> = ({ environment }) => {
</Box>
</Box>
<SuggestedChangesSidebar
project={project}
open={isSidebarOpen}
onClose={() => {
setIsSidebarOpen(false);

View File

@ -1,14 +1,16 @@
import React, { useState, VFC } from 'react';
import { 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';
import { useSuggestedChangesDraft } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
interface ISuggestedChangesSidebarProps {
open: boolean;
project: string;
onClose: () => void;
}
const StyledPageContent = styled(PageContent)(({ theme }) => ({
@ -26,11 +28,13 @@ const StyledPageContent = styled(PageContent)(({ theme }) => ({
},
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,
@ -38,23 +42,43 @@ const StyledHeaderHint = styled('div')(({ theme }) => ({
export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
open,
project,
onClose,
}) => {
const { data: suggestedChange } = useSuggestedChange();
const { draft, loading } = useSuggestedChangesDraft(project);
const onReview = async () => {
console.log('approve');
alert('approve');
};
const onDiscard = async () => {
console.log('discard');
alert('discard');
};
const onApply = async () => {
try {
console.log('apply');
alert('apply');
} catch (e) {
console.log(e);
}
};
if (!loading && !draft) {
return (
<SidebarModal open={open} onClose={onClose} label="Review changes">
<StyledPageContent
header={
<PageHeader
secondary
titleElement="Review your changes"
></PageHeader>
}
>
There are no changes to review.
{/* FIXME: empty state */}
</StyledPageContent>
</SidebarModal>
);
}
return (
<SidebarModal open={open} onClose={onClose} label="Review changes">
<StyledPageContent
@ -80,55 +104,82 @@ export const SuggestedChangesSidebar: VFC<ISuggestedChangesSidebarProps> = ({
></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>
{draft?.map(environmentChangeset => (
<Box
key={environmentChangeset.id}
sx={{
padding: 2,
border: '2px solid',
borderColor: theme => theme.palette.neutral.light,
borderRadius: theme =>
`${theme.shape.borderRadiusLarge}px`,
}}
>
<Typography>
env: {environmentChangeset?.environment}
</Typography>
<Typography>
state: {environmentChangeset?.state}
</Typography>
<hr />
<SuggestedChangeset
suggestedChange={environmentChangeset}
/>
<Box sx={{ display: 'flex' }}>
<ConditionallyRender
condition={
environmentChangeset?.state === 'APPROVED'
}
show={<Typography>Applied</Typography>}
/>
<ConditionallyRender
condition={
environmentChangeset?.state === 'CLOSED'
}
show={<Typography>Applied</Typography>}
/>
<ConditionallyRender
condition={
environmentChangeset?.state === 'APPROVED'
}
show={
<>
<Button
sx={{ mt: 2 }}
variant="contained"
onClick={onApply}
>
Apply changes
</Button>
</>
}
/>
<ConditionallyRender
condition={
environmentChangeset?.state === 'Draft'
}
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>
</Box>
))}
</StyledPageContent>
</SidebarModal>
);

View File

@ -4,38 +4,39 @@ import { SuggestedFeatureToggleChange } from '../SuggestedChangeOverview/Suggest
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';
// import {
// StrategyAddedChange,
// StrategyDeletedChange,
// StrategyEditedChange,
// } from '../SuggestedChangeOverview/SuggestedFeatureToggleChange/StrategyChange';
// import {
// formatStrategyName,
// GetFeatureStrategyIcon,
// } from 'utils/strategyNames';
import type { ISuggestChangesResponse } from 'hooks/api/getters/useSuggestedChangesDraft/useSuggestedChangesDraft';
export const SuggestedChangeset: FC<{ suggestedChange: any }> = ({
suggestedChange,
}) => {
export const SuggestedChangeset: FC<{
suggestedChange: ISuggestChangesResponse;
}> = ({ suggestedChange }) => {
return (
<Box>
Changes
{suggestedChange.changes?.map((featureToggleChange: any) => (
{suggestedChange.features?.map(featureToggleChange => (
<SuggestedFeatureToggleChange
key={featureToggleChange.feature}
featureToggleName={featureToggleChange.feature}
key={featureToggleChange.name}
featureToggleName={featureToggleChange.name}
>
{featureToggleChange.changeSet.map((change: any) => (
{featureToggleChange.changes.map(change => (
<Box key={objectId(change)}>
<ConditionallyRender
condition={change.action === 'updateEnabled'}
show={
<ToggleStatusChange
enabled={change?.payload?.data?.data}
enabled={change?.payload?.enabled}
/>
}
/>
<ConditionallyRender
{/* <ConditionallyRender
condition={change.action === 'addStrategy'}
show={
<StrategyAddedChange>
@ -55,7 +56,7 @@ export const SuggestedChangeset: FC<{ suggestedChange: any }> = ({
<ConditionallyRender
condition={change.action === 'updateStrategy'}
show={<StrategyEditedChange />}
/>
/> */}
</Box>
))}
</SuggestedFeatureToggleChange>

View File

@ -80,6 +80,9 @@ const data: any = {
],
};
/**
* @deprecated for draft: useSuggestedChangesDraft
*/
export const useSuggestedChange = () => {
// const { data, error, mutate } = useSWR(
// formatApiPath(`api/admin/suggest-changes/${id}`),

View File

@ -0,0 +1,58 @@
import useSWR from 'swr';
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
interface IChange {
id: number;
action: string;
payload: {
enabled: boolean; // FIXME: add other action types
};
createdAt: Date;
createdBy: {
id: number;
username?: any;
imageUrl?: any;
};
}
export interface ISuggestChangesResponse {
id: number;
environment: string;
state: string;
project: string;
createdBy: {
id: number;
username?: any;
imageUrl?: any;
};
createdAt: Date;
features: Array<{
name: string;
changes: IChange[];
}>;
}
const fetcher = (path: string) => {
return fetch(path)
.then(handleErrorResponses('SuggestedChanges'))
.then(res => res.json());
};
export const useSuggestedChangesDraft = (project: string) => {
const { data, error, mutate } = useSWR<ISuggestChangesResponse[]>(
formatApiPath(`api/admin/projects/${project}/suggest-changes/draft`),
fetcher
);
return useMemo(
() => ({
draft: data,
loading: !error && !data,
refetch: () => mutate(),
error,
}),
[data, error, mutate]
);
};