mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-18 13:48:58 +02:00
change feature toggle switch rendering
This commit is contained in:
parent
0fe28cb67b
commit
3ade62fa90
@ -3,7 +3,7 @@ import { Alert } from '@mui/material';
|
|||||||
import { Checkbox, FormControlLabel } from '@mui/material';
|
import { Checkbox, FormControlLabel } from '@mui/material';
|
||||||
import { PRODUCTION } from 'constants/environmentTypes';
|
import { PRODUCTION } from 'constants/environmentTypes';
|
||||||
import { IFeatureToggle } from 'interfaces/featureToggle';
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
import { createPersistentGlobalStateHook } from 'hooks/usePersistentGlobalState';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
|
||||||
interface IFeatureStrategyProdGuardProps {
|
interface IFeatureStrategyProdGuardProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -24,7 +24,8 @@ export const FeatureStrategyProdGuard = ({
|
|||||||
label,
|
label,
|
||||||
loading,
|
loading,
|
||||||
}: IFeatureStrategyProdGuardProps) => {
|
}: IFeatureStrategyProdGuardProps) => {
|
||||||
const [settings, setSettings] = useFeatureStrategyProdGuardSettings();
|
const { value: settings, setValue: setSettings } =
|
||||||
|
getFeatureStrategyProdGuardSettings();
|
||||||
|
|
||||||
const toggleHideSetting = () => {
|
const toggleHideSetting = () => {
|
||||||
setSettings((prev) => ({ hide: !prev.hide }));
|
setSettings((prev) => ({ hide: !prev.hide }));
|
||||||
@ -65,7 +66,7 @@ export const useFeatureStrategyProdGuard = (
|
|||||||
featureOrType: string | IFeatureToggle,
|
featureOrType: string | IFeatureToggle,
|
||||||
environmentId?: string,
|
environmentId?: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const [settings] = useFeatureStrategyProdGuardSettings();
|
const { value: settings } = getFeatureStrategyProdGuardSettings();
|
||||||
|
|
||||||
if (settings.hide) {
|
if (settings.hide) {
|
||||||
return false;
|
return false;
|
||||||
@ -85,8 +86,12 @@ export const useFeatureStrategyProdGuard = (
|
|||||||
// Store the "always hide" prod guard dialog setting in localStorage.
|
// Store the "always hide" prod guard dialog setting in localStorage.
|
||||||
const localStorageKey = 'useFeatureStrategyProdGuardSettings:v2';
|
const localStorageKey = 'useFeatureStrategyProdGuardSettings:v2';
|
||||||
|
|
||||||
const useFeatureStrategyProdGuardSettings =
|
const getFeatureStrategyProdGuardSettings = () =>
|
||||||
createPersistentGlobalStateHook<IFeatureStrategyProdGuardSettings>(
|
createLocalStorage<IFeatureStrategyProdGuardSettings>(localStorageKey, {
|
||||||
localStorageKey,
|
hide: false,
|
||||||
{ hide: false },
|
});
|
||||||
);
|
|
||||||
|
export const isProdGuardEnabled = (type: string) => {
|
||||||
|
const { value: settings } = getFeatureStrategyProdGuardSettings();
|
||||||
|
return type === PRODUCTION && !settings.hide;
|
||||||
|
};
|
||||||
|
@ -80,7 +80,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
|
|||||||
featureId={feature.name}
|
featureId={feature.name}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
environmentName={environment.name}
|
environmentName={environment.name}
|
||||||
type={featureEnvironment?.type}
|
type={featureEnvironment?.type || ''}
|
||||||
onToggle={handleToggle}
|
onToggle={handleToggle}
|
||||||
onError={showInfoBox}
|
onError={showInfoBox}
|
||||||
value={enabled}
|
value={enabled}
|
||||||
|
@ -0,0 +1,244 @@
|
|||||||
|
import { ReactNode, useCallback, useMemo, useState, type VFC } from 'react';
|
||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
|
||||||
|
import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { useOptimisticUpdate } from './hooks/useOptimisticUpdate';
|
||||||
|
import { flexRow } from 'themes/themeStyles';
|
||||||
|
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||||
|
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||||
|
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||||
|
import {
|
||||||
|
FeatureStrategyProdGuard,
|
||||||
|
isProdGuardEnabled,
|
||||||
|
useFeatureStrategyProdGuard,
|
||||||
|
} from 'component/feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard';
|
||||||
|
import { EnableEnvironmentDialog } from './EnableEnvironmentDialog/EnableEnvironmentDialog';
|
||||||
|
import { ListItemType } from '../ProjectFeatureToggles.types';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
|
||||||
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||||
|
|
||||||
|
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
|
||||||
|
mx: 'auto',
|
||||||
|
...flexRow,
|
||||||
|
}));
|
||||||
|
|
||||||
|
type OnFeatureToggleSwitchArgs = {
|
||||||
|
featureId: string;
|
||||||
|
projectId: string;
|
||||||
|
environmentName: string;
|
||||||
|
environmentType?: string;
|
||||||
|
hasStrategies?: boolean;
|
||||||
|
hasEnabledStrategies?: boolean;
|
||||||
|
isChangeRequestEnabled?: boolean;
|
||||||
|
onError?: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FeatureToggleSwitchProps = {
|
||||||
|
featureId: string;
|
||||||
|
projectId: string;
|
||||||
|
environmentName: string;
|
||||||
|
value: boolean;
|
||||||
|
onToggle: (newState: boolean, onRollback: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Middleware = (next: () => void) => void;
|
||||||
|
|
||||||
|
const composeAndRunMiddlewares = (middlewares: Middleware[]) => {
|
||||||
|
const runMiddleware = (currentIndex: number) => {
|
||||||
|
if (currentIndex < middlewares.length) {
|
||||||
|
middlewares[currentIndex](() => runMiddleware(currentIndex + 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
runMiddleware(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFeatureToggleSwitch = () => {
|
||||||
|
const { loading, toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||||
|
useFeatureApi();
|
||||||
|
// FIXME: change modals approach
|
||||||
|
const [modals, setModals] = useState<ReactNode>(null);
|
||||||
|
|
||||||
|
const onToggle = useCallback(
|
||||||
|
async (newState: boolean, config: OnFeatureToggleSwitchArgs) => {
|
||||||
|
const confirmProductionChanges: Middleware = (next) => {
|
||||||
|
if (config.isChangeRequestEnabled) {
|
||||||
|
// skip if change requests are enabled
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProdGuardEnabled(config.environmentType || '')) {
|
||||||
|
return setModals(
|
||||||
|
<FeatureStrategyProdGuard
|
||||||
|
open
|
||||||
|
onClose={() => {
|
||||||
|
setModals(null);
|
||||||
|
config.onError?.();
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setModals(null);
|
||||||
|
next();
|
||||||
|
}}
|
||||||
|
// FIXME: internalize loading
|
||||||
|
loading={loading}
|
||||||
|
label={`${
|
||||||
|
!newState ? 'Disable' : 'Enable'
|
||||||
|
} Environment`}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToChangeRequest: Middleware = (next) => {
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureActiveStrategies: Middleware = (next) => {
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
return composeAndRunMiddlewares([
|
||||||
|
confirmProductionChanges,
|
||||||
|
addToChangeRequest,
|
||||||
|
ensureActiveStrategies,
|
||||||
|
() => {
|
||||||
|
// FIXME: remove
|
||||||
|
console.log('done', { newState, config });
|
||||||
|
config.onSuccess?.();
|
||||||
|
},
|
||||||
|
// TODO: make actual changes
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { onToggle, modals };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeatureToggleSwitch: VFC<FeatureToggleSwitchProps> = ({
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentName,
|
||||||
|
value,
|
||||||
|
onToggle,
|
||||||
|
}) => {
|
||||||
|
const [isChecked, setIsChecked, rollbackIsChecked] =
|
||||||
|
useOptimisticUpdate<boolean>(value);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
setIsChecked(!isChecked);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
onToggle(!isChecked, rollbackIsChecked);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = `${featureId}-${environmentName}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledBoxContainer
|
||||||
|
key={key} // Prevent animation when archiving rows
|
||||||
|
data-testid={`TOGGLE-${key}`}
|
||||||
|
>
|
||||||
|
<PermissionSwitch
|
||||||
|
tooltip={
|
||||||
|
isChecked
|
||||||
|
? `Disable feature in ${environmentName}`
|
||||||
|
: `Enable feature in ${environmentName}`
|
||||||
|
}
|
||||||
|
checked={isChecked}
|
||||||
|
environmentId={environmentName}
|
||||||
|
projectId={projectId}
|
||||||
|
permission={UPDATE_FEATURE_ENVIRONMENT}
|
||||||
|
inputProps={{ 'aria-label': environmentName }}
|
||||||
|
onClick={onClick}
|
||||||
|
data-testid={'permission-switch'}
|
||||||
|
disableRipple
|
||||||
|
disabled={value !== isChecked}
|
||||||
|
/>
|
||||||
|
</StyledBoxContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledSwitchContainer = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'hasWarning',
|
||||||
|
})<{ hasWarning?: boolean }>(({ theme, hasWarning }) => ({
|
||||||
|
flexGrow: 0,
|
||||||
|
...flexRow,
|
||||||
|
justifyContent: 'center',
|
||||||
|
...(hasWarning && {
|
||||||
|
'::before': {
|
||||||
|
content: '""',
|
||||||
|
display: 'block',
|
||||||
|
width: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const createFeatureToggleCell =
|
||||||
|
(
|
||||||
|
projectId: string,
|
||||||
|
environmentName: string,
|
||||||
|
isChangeRequestEnabled: boolean,
|
||||||
|
refetch: () => void,
|
||||||
|
onFeatureToggleSwitch: ReturnType<
|
||||||
|
typeof useFeatureToggleSwitch
|
||||||
|
>['onToggle'],
|
||||||
|
) =>
|
||||||
|
({
|
||||||
|
value,
|
||||||
|
row: { original: feature },
|
||||||
|
}: {
|
||||||
|
value: boolean;
|
||||||
|
row: { original: ListItemType };
|
||||||
|
}) => {
|
||||||
|
const environment = feature.environments[environmentName];
|
||||||
|
|
||||||
|
const hasWarning = useMemo(
|
||||||
|
() =>
|
||||||
|
feature.someEnabledEnvironmentHasVariants &&
|
||||||
|
environment.variantCount === 0 &&
|
||||||
|
environment.enabled,
|
||||||
|
[feature, environment],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onToggle = (newState: boolean, onRollback: () => void) => {
|
||||||
|
onFeatureToggleSwitch(newState, {
|
||||||
|
projectId,
|
||||||
|
featureId: feature.name,
|
||||||
|
environmentName,
|
||||||
|
environmentType: environment?.type,
|
||||||
|
hasStrategies: environment?.hasStrategies,
|
||||||
|
hasEnabledStrategies: environment?.hasEnabledStrategies,
|
||||||
|
isChangeRequestEnabled,
|
||||||
|
onError: onRollback,
|
||||||
|
onSuccess: refetch,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledSwitchContainer hasWarning={hasWarning}>
|
||||||
|
<FeatureToggleSwitch
|
||||||
|
projectId={projectId}
|
||||||
|
value={value}
|
||||||
|
featureId={feature.name}
|
||||||
|
environmentName={environmentName}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasWarning}
|
||||||
|
show={<VariantsWarningTooltip />}
|
||||||
|
/>
|
||||||
|
</StyledSwitchContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -48,7 +48,11 @@ import {
|
|||||||
ProjectEnvironmentType,
|
ProjectEnvironmentType,
|
||||||
useEnvironmentsRef,
|
useEnvironmentsRef,
|
||||||
} from './hooks/useEnvironmentsRef';
|
} from './hooks/useEnvironmentsRef';
|
||||||
import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
|
import {
|
||||||
|
FeatureToggleSwitch,
|
||||||
|
createFeatureToggleCell,
|
||||||
|
useFeatureToggleSwitch,
|
||||||
|
} from './FeatureToggleSwitch/NewFeatureToggleSwitch';
|
||||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||||
@ -65,49 +69,19 @@ import { RowSelectCell } from './RowSelectCell/RowSelectCell';
|
|||||||
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||||
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||||
import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { ListItemType } from './ProjectFeatureToggles.types';
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledSwitchContainer = styled('div', {
|
|
||||||
shouldForwardProp: (prop) => prop !== 'hasWarning',
|
|
||||||
})<{ hasWarning?: boolean }>(({ theme, hasWarning }) => ({
|
|
||||||
flexGrow: 0,
|
|
||||||
...flexRow,
|
|
||||||
justifyContent: 'center',
|
|
||||||
...(hasWarning && {
|
|
||||||
'::before': {
|
|
||||||
content: '""',
|
|
||||||
display: 'block',
|
|
||||||
width: theme.spacing(2),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IProjectFeatureTogglesProps {
|
interface IProjectFeatureTogglesProps {
|
||||||
features: IProject['features'];
|
features: IProject['features'];
|
||||||
environments: IProject['environments'];
|
environments: IProject['environments'];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListItemType = Pick<
|
|
||||||
IProject['features'][number],
|
|
||||||
'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale' | 'favorite'
|
|
||||||
> & {
|
|
||||||
environments: {
|
|
||||||
[key in string]: {
|
|
||||||
name: string;
|
|
||||||
enabled: boolean;
|
|
||||||
variantCount: number;
|
|
||||||
type: string;
|
|
||||||
hasStrategies: boolean;
|
|
||||||
hasEnabledStrategies: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
someEnabledEnvironmentHasVariants: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||||
|
|
||||||
const defaultSort: SortingRule<string> & {
|
const defaultSort: SortingRule<string> & {
|
||||||
@ -135,6 +109,8 @@ export const ProjectFeatureToggles = ({
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
||||||
|
useFeatureToggleSwitch();
|
||||||
|
|
||||||
const { value: storedParams, setValue: setStoredParams } =
|
const { value: storedParams, setValue: setStoredParams } =
|
||||||
createLocalStorage(
|
createLocalStorage(
|
||||||
@ -163,6 +139,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
onChangeRequestToggleConfirm,
|
onChangeRequestToggleConfirm,
|
||||||
changeRequestDialogDetails,
|
changeRequestDialogDetails,
|
||||||
} = useChangeRequestToggle(projectId);
|
} = useChangeRequestToggle(projectId);
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const showEnvironmentLastSeen = Boolean(
|
const showEnvironmentLastSeen = Boolean(
|
||||||
@ -294,6 +271,15 @@ export const ProjectFeatureToggles = ({
|
|||||||
typeof value === 'string'
|
typeof value === 'string'
|
||||||
? value
|
? value
|
||||||
: (value as ProjectEnvironmentType).environment;
|
: (value as ProjectEnvironmentType).environment;
|
||||||
|
const isChangeRequestEnabled = isChangeRequestConfigured(name);
|
||||||
|
const FeatureToggleCell = createFeatureToggleCell(
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
isChangeRequestEnabled,
|
||||||
|
refetch,
|
||||||
|
onFeatureToggle,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Header: loading ? () => '' : name,
|
Header: loading ? () => '' : name,
|
||||||
maxWidth: 90,
|
maxWidth: 90,
|
||||||
@ -301,41 +287,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
accessor: (row: ListItemType) =>
|
accessor: (row: ListItemType) =>
|
||||||
row.environments[name]?.enabled,
|
row.environments[name]?.enabled,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
Cell: ({
|
Cell: FeatureToggleCell,
|
||||||
value,
|
|
||||||
row: { original: feature },
|
|
||||||
}: {
|
|
||||||
value: boolean;
|
|
||||||
row: { original: ListItemType };
|
|
||||||
}) => {
|
|
||||||
const hasWarning =
|
|
||||||
feature.someEnabledEnvironmentHasVariants &&
|
|
||||||
feature.environments[name].variantCount === 0 &&
|
|
||||||
feature.environments[name].enabled;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledSwitchContainer hasWarning={hasWarning}>
|
|
||||||
<FeatureToggleSwitch
|
|
||||||
value={value}
|
|
||||||
projectId={projectId}
|
|
||||||
featureId={feature.name}
|
|
||||||
environmentName={name}
|
|
||||||
type={feature.environments[name].type}
|
|
||||||
hasStrategies={
|
|
||||||
feature.environments[name].hasStrategies
|
|
||||||
}
|
|
||||||
hasEnabledStrategies={
|
|
||||||
feature.environments[name]
|
|
||||||
.hasEnabledStrategies
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasWarning}
|
|
||||||
show={<VariantsWarningTooltip />}
|
|
||||||
/>
|
|
||||||
</StyledSwitchContainer>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
sortType: 'boolean',
|
sortType: 'boolean',
|
||||||
filterName: name,
|
filterName: name,
|
||||||
filterParsing: (value: boolean) =>
|
filterParsing: (value: boolean) =>
|
||||||
@ -725,6 +677,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{featureToggleModals}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
<BatchSelectionActionsBar
|
<BatchSelectionActionsBar
|
||||||
count={Object.keys(selectedRowIds).length}
|
count={Object.keys(selectedRowIds).length}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { IProject } from 'interfaces/project';
|
||||||
|
|
||||||
|
export type ListItemType = Pick<
|
||||||
|
IProject['features'][number],
|
||||||
|
'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale' | 'favorite'
|
||||||
|
> & {
|
||||||
|
environments: {
|
||||||
|
[key in string]: {
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
variantCount: number;
|
||||||
|
type: string;
|
||||||
|
hasStrategies: boolean;
|
||||||
|
hasEnabledStrategies: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
someEnabledEnvironmentHasVariants: boolean;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user