1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +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 useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
interface IFeatureArchiveDialogProps { interface IFeatureArchiveDialogProps {
isOpen: boolean; isOpen: boolean;
@ -21,6 +22,7 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
featureIds, featureIds,
}) => { }) => {
const { archiveFeatureToggle } = useFeatureApi(); const { archiveFeatureToggle } = useFeatureApi();
const { archiveFeatures } = useProjectApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const isBulkArchive = featureIds?.length > 1; const isBulkArchive = featureIds?.length > 1;
@ -42,12 +44,7 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
const archiveToggles = async () => { const archiveToggles = async () => {
try { try {
// TODO: bulk archive await archiveFeatures(projectId, featureIds);
await Promise.allSettled(
featureIds.map(id => {
archiveFeatureToggle(projectId, id);
})
);
setToastData({ setToastData({
text: 'Selected feature toggles have been archived', text: 'Selected feature toggles have been archived',
type: 'success', type: 'success',

View File

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

View File

@ -21,6 +21,7 @@ export const ArchiveButton: VFC<IArchiveButtonProps> = ({
const onConfirm = async () => { const onConfirm = async () => {
setIsDialogOpen(false); setIsDialogOpen(false);
await refetch(); await refetch();
// TODO: toast
}; };
return ( 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 { useMemo, useState, VFC } from 'react';
import { Box, Button, Paper, styled, Typography } from '@mui/material'; 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 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 { ArchiveButton } from './ArchiveButton/ArchiveButton';
import { MarkAsStaleButtons } from './MarkAsStaleButtons/MarkAsStaleButtons'; import { MoreActions } from './MoreActions/MoreActions';
interface ISelectionActionsBarProps { interface ISelectionActionsBarProps {
selectedIds: string[]; selectedIds: string[];
@ -79,7 +79,6 @@ export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
&ensp;selected &ensp;selected
</StyledText> </StyledText>
<ArchiveButton projectId={projectId} features={selectedIds} /> <ArchiveButton projectId={projectId} features={selectedIds} />
<MarkAsStaleButtons projectId={projectId} data={selectedData} />
<Button <Button
startIcon={<FileDownload />} startIcon={<FileDownload />}
variant="outlined" variant="outlined"
@ -96,6 +95,7 @@ export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
> >
Tags Tags
</Button> </Button>
<MoreActions projectId={projectId} data={selectedData} />
</StyledBar> </StyledBar>
<ConditionallyRender <ConditionallyRender
condition={Boolean(uiConfig?.flags?.featuresExportImport)} condition={Boolean(uiConfig?.flags?.featuresExportImport)}

View File

@ -1,3 +1,4 @@
import type { BatchStaleSchema } from 'openapi';
import useAPI from '../useApi/useApi'; import useAPI from '../useApi/useApi';
interface ICreatePayload { interface ICreatePayload {
@ -215,6 +216,35 @@ const useProjectApi = () => {
return makeRequest(req.caller, req.id); 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 { return {
createProject, createProject,
validateId, validateId,
@ -227,10 +257,12 @@ const useProjectApi = () => {
removeGroupFromRole, removeGroupFromRole,
changeUserRole, changeUserRole,
changeGroupRole, changeGroupRole,
errors, archiveFeatures,
loading, staleFeatures,
searchProjectUser, searchProjectUser,
setDefaultProjectStickiness, setDefaultProjectStickiness,
errors,
loading,
}; };
}; };