mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
UI/bulk archive (#3319)
Ability to archive multiple feature toggles from project overview
This commit is contained in:
parent
a5f1b89b4a
commit
0784afd255
@ -3,13 +3,14 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
|||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
interface IFeatureArchiveDialogProps {
|
interface IFeatureArchiveDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
featureId: string;
|
featureIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
||||||
@ -17,14 +18,15 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
projectId,
|
projectId,
|
||||||
featureId,
|
featureIds,
|
||||||
}) => {
|
}) => {
|
||||||
const { archiveFeatureToggle } = useFeatureApi();
|
const { archiveFeatureToggle } = useFeatureApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const isBulkArchive = featureIds?.length > 1;
|
||||||
|
|
||||||
const archiveToggle = async () => {
|
const archiveToggle = async () => {
|
||||||
try {
|
try {
|
||||||
await archiveFeatureToggle(projectId, featureId);
|
await archiveFeatureToggle(projectId, featureIds[0]);
|
||||||
setToastData({
|
setToastData({
|
||||||
text: 'Your feature toggle has been archived',
|
text: 'Your feature toggle has been archived',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -38,16 +40,69 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const archiveToggles = async () => {
|
||||||
|
try {
|
||||||
|
// TODO: bulk archive
|
||||||
|
await Promise.allSettled(
|
||||||
|
featureIds.map(id => {
|
||||||
|
archiveFeatureToggle(projectId, id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setToastData({
|
||||||
|
text: 'Selected feature toggles have been archived',
|
||||||
|
type: 'success',
|
||||||
|
title: 'Feature toggles archived',
|
||||||
|
});
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
onClick={() => archiveToggle()}
|
onClick={isBulkArchive ? archiveToggles : archiveToggle}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
primaryButtonText="Archive toggle"
|
primaryButtonText={
|
||||||
|
isBulkArchive ? 'Archive toggles' : 'Archive toggle'
|
||||||
|
}
|
||||||
secondaryButtonText="Cancel"
|
secondaryButtonText="Cancel"
|
||||||
title="Archive feature toggle"
|
title={
|
||||||
|
isBulkArchive
|
||||||
|
? 'Archive feature toggles'
|
||||||
|
: 'Archive feature toggle'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Are you sure you want to archive this feature toggle?
|
<ConditionallyRender
|
||||||
|
condition={isBulkArchive}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to archive{' '}
|
||||||
|
<strong>{featureIds?.length}</strong> feature
|
||||||
|
toggles?
|
||||||
|
</p>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={featureIds?.length <= 5}
|
||||||
|
show={
|
||||||
|
<ul>
|
||||||
|
{featureIds?.map(id => (
|
||||||
|
<li key={id}>{id}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<p>
|
||||||
|
Are you sure you want to archive these feature toggles?
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Dialogue>
|
</Dialogue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -248,7 +248,7 @@ export const FeatureView = () => {
|
|||||||
}}
|
}}
|
||||||
onClose={() => setShowDelDialog(false)}
|
onClose={() => setShowDelDialog(false)}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
featureId={featureId}
|
featureIds={[featureId]}
|
||||||
/>
|
/>
|
||||||
<FeatureStaleDialog
|
<FeatureStaleDialog
|
||||||
isStale={feature.stale}
|
isStale={feature.stale}
|
||||||
|
@ -685,7 +685,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setFeatureArchiveState(undefined);
|
setFeatureArchiveState(undefined);
|
||||||
}}
|
}}
|
||||||
featureId={featureArchiveState || ''}
|
featureIds={[featureArchiveState || '']}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<ChangeRequestDialogue
|
<ChangeRequestDialogue
|
||||||
@ -717,6 +717,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
<SelectionActionsBar
|
<SelectionActionsBar
|
||||||
selectedIds={Object.keys(selectedRowIds)}
|
selectedIds={Object.keys(selectedRowIds)}
|
||||||
data={features}
|
data={features}
|
||||||
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import { useState, VFC } from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { Archive } from '@mui/icons-material';
|
||||||
|
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||||
|
import { DELETE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||||
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
|
|
||||||
|
interface IArchiveButtonProps {
|
||||||
|
projectId: string;
|
||||||
|
features: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArchiveButton: VFC<IArchiveButtonProps> = ({
|
||||||
|
projectId,
|
||||||
|
features,
|
||||||
|
}) => {
|
||||||
|
const { refetch } = useProject(projectId);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const onConfirm = async () => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
await refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PermissionHOC projectId={projectId} permission={DELETE_FEATURE}>
|
||||||
|
{({ hasAccess }) => (
|
||||||
|
<Button
|
||||||
|
disabled={!hasAccess || isDialogOpen}
|
||||||
|
startIcon={<Archive />}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setIsDialogOpen(true)}
|
||||||
|
>
|
||||||
|
Archive
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</PermissionHOC>
|
||||||
|
<FeatureArchiveDialog
|
||||||
|
projectId={projectId}
|
||||||
|
featureIds={features}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
isOpen={isDialogOpen}
|
||||||
|
onClose={() => setIsDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,54 @@
|
|||||||
|
import { VFC } from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { WatchLater } from '@mui/icons-material';
|
||||||
|
import type { FeatureSchema } from 'openapi';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||||
|
|
||||||
|
interface IMarkAsStaleButtonsProps {
|
||||||
|
projectId: string;
|
||||||
|
data: FeatureSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarkAsStaleButtons: VFC<IMarkAsStaleButtonsProps> = ({
|
||||||
|
projectId,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const hasStale = data.some(d => d.stale);
|
||||||
|
const hasUnstale = data.some(d => !d.stale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PermissionHOC projectId={projectId} permission={UPDATE_FEATURE}>
|
||||||
|
{({ hasAccess }) => (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasUnstale || !hasAccess}
|
||||||
|
show={
|
||||||
|
<Button
|
||||||
|
startIcon={<WatchLater />}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
disabled={!hasAccess}
|
||||||
|
>
|
||||||
|
Mark as stale
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(hasAccess && hasStale)}
|
||||||
|
show={
|
||||||
|
<Button
|
||||||
|
startIcon={<WatchLater />}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Un-mark as stale
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PermissionHOC>
|
||||||
|
);
|
||||||
|
};
|
@ -1,26 +1,30 @@
|
|||||||
import { useMemo, useState, VFC } from 'react';
|
import { useMemo, useState, VFC } from 'react';
|
||||||
import { Box, Button, Paper, styled, Typography } from '@mui/material';
|
import { Box, Button, Paper, styled, Typography } from '@mui/material';
|
||||||
import { Archive, FileDownload, Label, WatchLater } from '@mui/icons-material';
|
import { FileDownload, Label } from '@mui/icons-material';
|
||||||
import type { FeatureSchema } from 'openapi';
|
import type { FeatureSchema } from 'openapi';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ArchiveButton } from './ArchiveButton/ArchiveButton';
|
||||||
|
import { MarkAsStaleButtons } from './MarkAsStaleButtons/MarkAsStaleButtons';
|
||||||
|
|
||||||
interface ISelectionActionsBarProps {
|
interface ISelectionActionsBarProps {
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
data: FeatureSchema[];
|
data: FeatureSchema[];
|
||||||
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledContainer = styled(Box)(() => ({
|
const StyledContainer = styled(Box)(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
flexWrap: 'wrap',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledBar = styled(Paper)(({ theme }) => ({
|
const StyledBar = styled(Paper)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-end',
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
@ -28,7 +32,8 @@ const StyledBar = styled(Paper)(({ theme }) => ({
|
|||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
border: `1px solid ${theme.palette.secondary.main}`,
|
border: `1px solid ${theme.palette.secondary.main}`,
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
columnGap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
|
flexWrap: 'wrap',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledCount = styled('span')(({ theme }) => ({
|
const StyledCount = styled('span')(({ theme }) => ({
|
||||||
@ -39,12 +44,14 @@ const StyledCount = styled('span')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledText = styled(Typography)(({ theme }) => ({
|
const StyledText = styled(Typography)(({ theme }) => ({
|
||||||
marginRight: theme.spacing(2),
|
paddingRight: theme.spacing(2),
|
||||||
|
marginRight: 'auto',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
|
export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
|
||||||
selectedIds,
|
selectedIds,
|
||||||
data,
|
data,
|
||||||
|
projectId,
|
||||||
}) => {
|
}) => {
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
@ -71,22 +78,8 @@ export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
|
|||||||
<StyledCount>{selectedIds.length}</StyledCount>
|
<StyledCount>{selectedIds.length}</StyledCount>
|
||||||
 selected
|
 selected
|
||||||
</StyledText>
|
</StyledText>
|
||||||
<Button
|
<ArchiveButton projectId={projectId} features={selectedIds} />
|
||||||
disabled
|
<MarkAsStaleButtons projectId={projectId} data={selectedData} />
|
||||||
startIcon={<Archive />}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Archive
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled
|
|
||||||
startIcon={<WatchLater />}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Mark as stale
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
startIcon={<FileDownload />}
|
startIcon={<FileDownload />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
Loading…
Reference in New Issue
Block a user