mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-21 13:47:39 +02:00
parent
b4c4a23664
commit
a983cf15b9
@ -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',
|
||||||
|
@ -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),
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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> = ({
|
|||||||
 selected
|
 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)}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user