1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-12 01:17:04 +02:00

UI/bulk stale (#3320)

Mark and un-mark selected toggles as stale.
This commit is contained in:
Tymoteusz Czech 2023-03-16 14:54:38 +01:00 committed by GitHub
parent b4c4a23664
commit a983cf15b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 207 additions and 66 deletions

View File

@ -4,6 +4,7 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
interface IFeatureArchiveDialogProps {
isOpen: boolean;
@ -21,6 +22,7 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
featureIds,
}) => {
const { archiveFeatureToggle } = useFeatureApi();
const { archiveFeatures } = useProjectApi();
const { setToastData, setToastApiError } = useToast();
const isBulkArchive = featureIds?.length > 1;
@ -42,12 +44,7 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
const archiveToggles = async () => {
try {
// TODO: bulk archive
await Promise.allSettled(
featureIds.map(id => {
archiveFeatureToggle(projectId, id);
})
);
await archiveFeatures(projectId, featureIds);
setToastData({
text: 'Selected feature toggles have been archived',
type: 'success',

View File

@ -87,7 +87,7 @@ export const ActionsCell: VFC<IActionsCellProps> = ({
disableScrollLock={true}
PaperProps={{
sx: theme => ({
borderRadius: theme.shape.borderRadius,
borderRadius: `${theme.shape.borderRadius}px`,
padding: theme.spacing(1, 1.5),
}),
}}

View File

@ -21,6 +21,7 @@ export const ArchiveButton: VFC<IArchiveButtonProps> = ({
const onConfirm = async () => {
setIsDialogOpen(false);
await refetch();
// TODO: toast
};
return (

View File

@ -1,54 +0,0 @@
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>
);
};

View File

@ -0,0 +1,165 @@
import { useState, VFC } from 'react';
import {
IconButton,
ListItemIcon,
ListItemText,
MenuItem,
MenuList,
Popover,
Tooltip,
Typography,
} from '@mui/material';
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import { MoreVert, WatchLater } from '@mui/icons-material';
import type { FeatureSchema } from 'openapi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useProject from 'hooks/api/getters/useProject/useProject';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
interface IMoreActionsProps {
projectId: string;
data: FeatureSchema[];
}
const menuId = 'selection-actions-menu';
export const MoreActions: VFC<IMoreActionsProps> = ({ projectId, data }) => {
const { refetch } = useProject(projectId);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const { staleFeatures } = useProjectApi();
const { setToastData, setToastApiError } = useToast();
const open = Boolean(anchorEl);
const selectedIds = data.map(({ name }) => name);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const hasStale = data.some(({ stale }) => stale === true);
const hasUnstale = data.some(({ stale }) => stale === false);
const onMarkAsStale = async () => {
try {
handleClose();
await staleFeatures(projectId, selectedIds);
await refetch();
setToastData({
title: 'State updated',
text: 'Feature toggles marked as stale',
type: 'success',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const onUnmarkAsStale = async () => {
try {
handleClose();
await staleFeatures(projectId, selectedIds, false);
await refetch();
setToastData({
title: 'State updated',
text: 'Feature toggles unmarked as stale',
type: 'success',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
return (
<>
<Tooltip title="Feature toggle actions" arrow describeChild>
<IconButton
id={menuId}
aria-controls={open ? menuId : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
type="button"
>
<MoreVert />
</IconButton>
</Tooltip>
<Popover
id={`${menuId}-menu`}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
disableScrollLock={true}
PaperProps={{
sx: theme => ({
borderRadius: `${theme.shape.borderRadius}px`,
padding: theme.spacing(1, 1.5),
}),
}}
>
<MenuList aria-labelledby={`${menuId}-menu`}>
<PermissionHOC
projectId={projectId}
permission={UPDATE_FEATURE}
>
{({ hasAccess }) => (
<>
<ConditionallyRender
condition={hasUnstale}
show={() => (
<MenuItem
onClick={onMarkAsStale}
disabled={!hasAccess}
sx={{
borderRadius: theme =>
`${theme.shape.borderRadius}px`,
}}
>
<ListItemIcon>
<WatchLater />
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
Mark as stale
</Typography>
</ListItemText>
</MenuItem>
)}
/>
<ConditionallyRender
condition={hasStale}
show={() => (
<MenuItem
onClick={onUnmarkAsStale}
disabled={!hasAccess}
sx={{
borderRadius: theme =>
`${theme.shape.borderRadius}px`,
}}
>
<ListItemIcon>
<WatchLater />
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
Un-mark as stale
</Typography>
</ListItemText>
</MenuItem>
)}
/>
</>
)}
</PermissionHOC>
</MenuList>
</Popover>
</>
);
};

View File

@ -1,12 +1,12 @@
import { useMemo, useState, VFC } from 'react';
import { Box, Button, Paper, styled, Typography } from '@mui/material';
import { FileDownload, Label } from '@mui/icons-material';
import { FileDownload, Label, WatchLater } from '@mui/icons-material';
import type { FeatureSchema } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ArchiveButton } from './ArchiveButton/ArchiveButton';
import { MarkAsStaleButtons } from './MarkAsStaleButtons/MarkAsStaleButtons';
import { MoreActions } from './MoreActions/MoreActions';
interface ISelectionActionsBarProps {
selectedIds: string[];
@ -79,7 +79,6 @@ export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
&ensp;selected
</StyledText>
<ArchiveButton projectId={projectId} features={selectedIds} />
<MarkAsStaleButtons projectId={projectId} data={selectedData} />
<Button
startIcon={<FileDownload />}
variant="outlined"
@ -96,6 +95,7 @@ export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
>
Tags
</Button>
<MoreActions projectId={projectId} data={selectedData} />
</StyledBar>
<ConditionallyRender
condition={Boolean(uiConfig?.flags?.featuresExportImport)}

View File

@ -1,3 +1,4 @@
import type { BatchStaleSchema } from 'openapi';
import useAPI from '../useApi/useApi';
interface ICreatePayload {
@ -215,6 +216,35 @@ const useProjectApi = () => {
return makeRequest(req.caller, req.id);
};
const archiveFeatures = async (projectId: string, featureIds: string[]) => {
const path = `api/admin/projects/${projectId}/archive`;
const req = createRequest(path, {
method: 'POST',
body: JSON.stringify({ features: featureIds }),
});
return makeRequest(req.caller, req.id);
};
const staleFeatures = async (
projectId: string,
featureIds: string[],
stale = true
) => {
const payload: BatchStaleSchema = {
features: featureIds,
stale,
};
const path = `api/admin/projects/${projectId}/stale`;
const req = createRequest(path, {
method: 'POST',
body: JSON.stringify(payload),
});
return makeRequest(req.caller, req.id);
};
return {
createProject,
validateId,
@ -227,10 +257,12 @@ const useProjectApi = () => {
removeGroupFromRole,
changeUserRole,
changeGroupRole,
errors,
loading,
archiveFeatures,
staleFeatures,
searchProjectUser,
setDefaultProjectStickiness,
errors,
loading,
};
};