mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
chore: remove featureSwitchRefactor flag (#5329)
Cleanup. This change has been rolled out to significant number of customers already, and we have another parallel version behind a flag.
This commit is contained in:
parent
cdebf9aa28
commit
fd3a7f12cb
@ -1,73 +0,0 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||
import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
interface IEnvironmentStrategyDialogProps {
|
||||
open: boolean;
|
||||
featureId: string;
|
||||
projectId: string;
|
||||
onClose: () => void;
|
||||
environmentName: string;
|
||||
}
|
||||
|
||||
const StyledParagraph = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(0.5),
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
}));
|
||||
const EnvironmentStrategyDialog = ({
|
||||
open,
|
||||
environmentName,
|
||||
featureId,
|
||||
projectId,
|
||||
onClose,
|
||||
}: IEnvironmentStrategyDialogProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createStrategyPath = formatCreateStrategyPath(
|
||||
projectId,
|
||||
featureId,
|
||||
environmentName,
|
||||
'default',
|
||||
);
|
||||
|
||||
const onClick = () => {
|
||||
onClose();
|
||||
navigate(createStrategyPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
open={open}
|
||||
maxWidth='sm'
|
||||
onClose={() => onClose()}
|
||||
title='You need to add a strategy to your toggle'
|
||||
primaryButtonText='Take me directly to add strategy'
|
||||
permissionButton={
|
||||
<PermissionButton
|
||||
type='button'
|
||||
permission={CREATE_FEATURE_STRATEGY}
|
||||
projectId={projectId}
|
||||
environmentId={environmentName}
|
||||
onClick={onClick}
|
||||
>
|
||||
Take me directly to add strategy
|
||||
</PermissionButton>
|
||||
}
|
||||
secondaryButtonText='Cancel'
|
||||
>
|
||||
<StyledParagraph>
|
||||
Before you can enable the toggle in the environment, you need to
|
||||
add an activation strategy.
|
||||
</StyledParagraph>
|
||||
<StyledParagraph>
|
||||
You can add the activation strategy by selecting the toggle,
|
||||
open the environment accordion and add the activation strategy.
|
||||
</StyledParagraph>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentStrategyDialog;
|
@ -4,7 +4,9 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { styled } from '@mui/material';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { FeatureOverviewSidePanelEnvironmentHider } from './FeatureOverviewSidePanelEnvironmentHider';
|
||||
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch';
|
||||
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
||||
import { useFeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
marginLeft: theme.spacing(-1.5),
|
||||
@ -24,7 +26,6 @@ const StyledLabel = styled('label')(() => ({
|
||||
interface IFeatureOverviewSidePanelEnvironmentSwitchProps {
|
||||
environment: IFeatureEnvironment;
|
||||
callback?: () => void;
|
||||
showInfoBox: () => void;
|
||||
children?: React.ReactNode;
|
||||
hiddenEnvironments: Set<String>;
|
||||
setHiddenEnvironments: (environment: string) => void;
|
||||
@ -33,30 +34,49 @@ interface IFeatureOverviewSidePanelEnvironmentSwitchProps {
|
||||
export const FeatureOverviewSidePanelEnvironmentSwitch = ({
|
||||
environment,
|
||||
callback,
|
||||
showInfoBox,
|
||||
children,
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
}: IFeatureOverviewSidePanelEnvironmentSwitchProps) => {
|
||||
const { name, enabled } = environment;
|
||||
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
|
||||
const defaultContent = (
|
||||
<>
|
||||
{' '}
|
||||
<span data-loading>{enabled ? 'enabled' : 'disabled'} in</span>
|
||||
<span data-loading>
|
||||
{environment.enabled ? 'enabled' : 'disabled'} in
|
||||
</span>
|
||||
|
||||
<StringTruncator text={name} maxWidth='120' maxLength={15} />
|
||||
<StringTruncator
|
||||
text={environment.name}
|
||||
maxWidth='120'
|
||||
maxLength={15}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
||||
useFeatureToggleSwitch(projectId);
|
||||
|
||||
const handleToggle = () => {
|
||||
refetchFeature();
|
||||
if (callback) callback();
|
||||
};
|
||||
const handleToggle = (newState: boolean, onRollback: () => void) =>
|
||||
onFeatureToggle(newState, {
|
||||
projectId,
|
||||
featureId,
|
||||
environmentName: environment.name,
|
||||
environmentType: environment.type,
|
||||
hasStrategies: environment.strategies.length > 0,
|
||||
hasEnabledStrategies: environment.strategies.some(
|
||||
(strategy) => !strategy.disabled,
|
||||
),
|
||||
isChangeRequestEnabled: isChangeRequestConfigured(environment.name),
|
||||
onRollback,
|
||||
onSuccess: () => {
|
||||
if (callback) callback();
|
||||
refetchFeature();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
@ -66,8 +86,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
|
||||
projectId={projectId}
|
||||
environmentName={environment.name}
|
||||
onToggle={handleToggle}
|
||||
onError={showInfoBox}
|
||||
value={enabled}
|
||||
value={environment.enabled}
|
||||
/>
|
||||
{children ?? defaultContent}
|
||||
</StyledLabel>
|
||||
@ -76,6 +95,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
/>
|
||||
{featureToggleModals}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,4 @@
|
||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { useState } from 'react';
|
||||
import { type IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { FeatureOverviewSidePanelEnvironmentSwitch } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch';
|
||||
import { Link, styled, Tooltip } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
@ -53,8 +51,6 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
}: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => {
|
||||
const [showInfoBox, setShowInfoBox] = useState(false);
|
||||
const [environmentName, setEnvironmentName] = useState('');
|
||||
const someEnabledEnvironmentHasVariants = feature.environments.some(
|
||||
(environment) => environment.enabled && environment.variants?.length,
|
||||
);
|
||||
@ -96,10 +92,6 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
||||
environment={environment}
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
showInfoBox={() => {
|
||||
setEnvironmentName(environment.name);
|
||||
setShowInfoBox(true);
|
||||
}}
|
||||
>
|
||||
<StyledSwitchLabel>
|
||||
<StyledLabel>{environment.name}</StyledLabel>
|
||||
@ -120,13 +112,6 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
||||
</FeatureOverviewSidePanelEnvironmentSwitch>
|
||||
);
|
||||
})}
|
||||
<EnvironmentStrategyDialog
|
||||
open={showInfoBox}
|
||||
onClose={() => setShowInfoBox(false)}
|
||||
projectId={feature.project}
|
||||
featureId={feature.name}
|
||||
environmentName={environmentName}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,257 +0,0 @@
|
||||
import React, { useState, 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 { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||
import { EnableEnvironmentDialog } from './EnableEnvironmentDialog/EnableEnvironmentDialog';
|
||||
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||
import {
|
||||
FeatureStrategyProdGuard,
|
||||
useFeatureStrategyProdGuard,
|
||||
} from '../../../../feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard';
|
||||
|
||||
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
|
||||
mx: 'auto',
|
||||
...flexRow,
|
||||
}));
|
||||
|
||||
interface IFeatureToggleSwitchProps {
|
||||
featureId: string;
|
||||
environmentName: string;
|
||||
projectId: string;
|
||||
value: boolean;
|
||||
onError?: () => void;
|
||||
onToggle?: (
|
||||
projectId: string,
|
||||
feature: string,
|
||||
env: string,
|
||||
state: boolean,
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated remove when flag `featureSwitchRefactor` is removed
|
||||
*/
|
||||
export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
||||
projectId,
|
||||
featureId,
|
||||
environmentName,
|
||||
value,
|
||||
onToggle,
|
||||
onError,
|
||||
}) => {
|
||||
const { loading, toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||
useFeatureApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
const {
|
||||
onChangeRequestToggle,
|
||||
onChangeRequestToggleClose,
|
||||
onChangeRequestToggleConfirm,
|
||||
changeRequestDialogDetails,
|
||||
} = useChangeRequestToggle(projectId);
|
||||
const [isChecked, setIsChecked, rollbackIsChecked] =
|
||||
useOptimisticUpdate<boolean>(value);
|
||||
|
||||
const [showEnabledDialog, setShowEnabledDialog] = useState(false);
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const enableProdGuard = useFeatureStrategyProdGuard(
|
||||
feature,
|
||||
environmentName,
|
||||
);
|
||||
const [showProdGuard, setShowProdGuard] = useState(false);
|
||||
|
||||
const disabledStrategiesCount =
|
||||
feature?.environments
|
||||
.find((env) => env.name === environmentName)
|
||||
?.strategies.filter((strategy) => strategy.disabled).length ?? 0;
|
||||
|
||||
const handleToggleEnvironmentOn = async (
|
||||
shouldActivateDisabled = false,
|
||||
) => {
|
||||
try {
|
||||
setIsChecked(!isChecked);
|
||||
await toggleFeatureEnvironmentOn(
|
||||
projectId,
|
||||
feature.name,
|
||||
environmentName,
|
||||
shouldActivateDisabled,
|
||||
);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: `Available in ${environmentName}`,
|
||||
text: `${feature.name} is now available in ${environmentName} based on its defined strategies.`,
|
||||
});
|
||||
onToggle?.(projectId, feature.name, environmentName, !isChecked);
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === ENVIRONMENT_STRATEGY_ERROR
|
||||
) {
|
||||
onError?.();
|
||||
} else {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
rollbackIsChecked();
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleEnvironmentOff = async () => {
|
||||
try {
|
||||
setIsChecked(!isChecked);
|
||||
await toggleFeatureEnvironmentOff(
|
||||
projectId,
|
||||
feature.name,
|
||||
environmentName,
|
||||
);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: `Unavailable in ${environmentName}`,
|
||||
text: `${feature.name} is unavailable in ${environmentName} and its strategies will no longer have any effect.`,
|
||||
});
|
||||
onToggle?.(projectId, feature.name, environmentName, !isChecked);
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
rollbackIsChecked();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = async () => {
|
||||
setShowProdGuard(false);
|
||||
if (isChangeRequestConfigured(environmentName)) {
|
||||
if (featureHasOnlyDisabledStrategies()) {
|
||||
setShowEnabledDialog(true);
|
||||
} else {
|
||||
onChangeRequestToggle(
|
||||
feature.name,
|
||||
environmentName,
|
||||
!isChecked,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isChecked) {
|
||||
await handleToggleEnvironmentOff();
|
||||
return;
|
||||
}
|
||||
|
||||
if (featureHasOnlyDisabledStrategies()) {
|
||||
setShowEnabledDialog(true);
|
||||
} else {
|
||||
await handleToggleEnvironmentOn();
|
||||
}
|
||||
};
|
||||
|
||||
const onClick = async () => {
|
||||
if (enableProdGuard && !isChangeRequestConfigured(environmentName)) {
|
||||
setShowProdGuard(true);
|
||||
} else {
|
||||
await handleClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onActivateStrategies = async () => {
|
||||
if (isChangeRequestConfigured(environmentName)) {
|
||||
onChangeRequestToggle(
|
||||
feature.name,
|
||||
environmentName,
|
||||
!isChecked,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
await handleToggleEnvironmentOn(true);
|
||||
}
|
||||
setShowEnabledDialog(false);
|
||||
};
|
||||
|
||||
const onAddDefaultStrategy = async () => {
|
||||
if (isChangeRequestConfigured(environmentName)) {
|
||||
onChangeRequestToggle(
|
||||
feature.name,
|
||||
environmentName,
|
||||
!isChecked,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
await handleToggleEnvironmentOn();
|
||||
}
|
||||
setShowEnabledDialog(false);
|
||||
};
|
||||
|
||||
const featureHasOnlyDisabledStrategies = () => {
|
||||
const featureEnvironment = feature?.environments?.find(
|
||||
(env) => env.name === environmentName,
|
||||
);
|
||||
return (
|
||||
featureEnvironment?.strategies &&
|
||||
featureEnvironment?.strategies?.length > 0 &&
|
||||
featureEnvironment?.strategies?.every(
|
||||
(strategy) => strategy.disabled,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const key = `${feature.name}-${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'}
|
||||
/>
|
||||
</StyledBoxContainer>
|
||||
<EnableEnvironmentDialog
|
||||
isOpen={showEnabledDialog}
|
||||
onClose={() => setShowEnabledDialog(false)}
|
||||
environment={environmentName}
|
||||
disabledStrategiesCount={disabledStrategiesCount}
|
||||
onActivateDisabledStrategies={onActivateStrategies}
|
||||
onAddDefaultStrategy={onAddDefaultStrategy}
|
||||
/>
|
||||
<ChangeRequestDialogue
|
||||
isOpen={changeRequestDialogDetails.isOpen}
|
||||
onClose={onChangeRequestToggleClose}
|
||||
environment={changeRequestDialogDetails?.environment}
|
||||
onConfirm={onChangeRequestToggleConfirm}
|
||||
messageComponent={
|
||||
<UpdateEnabledMessage
|
||||
enabled={changeRequestDialogDetails?.enabled!}
|
||||
featureName={changeRequestDialogDetails?.featureName!}
|
||||
environment={changeRequestDialogDetails.environment!}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FeatureStrategyProdGuard
|
||||
open={showProdGuard}
|
||||
onClose={() => setShowProdGuard(false)}
|
||||
onClick={handleClick}
|
||||
loading={loading}
|
||||
label={`${isChecked ? 'Disable' : 'Enable'} Environment`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -96,7 +96,11 @@ export const useFeatureToggleSwitch: UseFeatureToggleSwitchType = (
|
||||
};
|
||||
|
||||
const ensureActiveStrategies: Middleware = (next) => {
|
||||
if (!config.hasStrategies || config.hasEnabledStrategies) {
|
||||
if (
|
||||
newState === false ||
|
||||
!config.hasStrategies ||
|
||||
config.hasEnabledStrategies
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
@ -1,736 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Checkbox,
|
||||
IconButton,
|
||||
styled,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
SortingRule,
|
||||
useFlexLayout,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import type { FeatureSchema } from 'openapi';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||
import { getCreateTogglePath } from 'utils/routePathHelpers';
|
||||
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||
import { IProject } from 'interfaces/project';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||
import {
|
||||
ProjectEnvironmentType,
|
||||
useEnvironmentsRef,
|
||||
} from './hooks/useEnvironmentsRef';
|
||||
import { FeatureToggleSwitch } from './FeatureToggleSwitch/LegacyFeatureToggleSwitch';
|
||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
||||
import { flexRow } from 'themes/themeStyles';
|
||||
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
|
||||
import FileDownload from '@mui/icons-material/FileDownload';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||
import { RowSelectCell } from './RowSelectCell/RowSelectCell';
|
||||
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||
import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||
import useToast from 'hooks/useToast';
|
||||
|
||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||
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 {
|
||||
features: IProject['features'];
|
||||
environments: IProject['environments'];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
type ListItemType = Pick<
|
||||
IProject['features'][number],
|
||||
'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale' | 'favorite'
|
||||
> & {
|
||||
environments: {
|
||||
[key in string]: {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
variantCount: number;
|
||||
};
|
||||
};
|
||||
someEnabledEnvironmentHasVariants: boolean;
|
||||
};
|
||||
|
||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||
|
||||
const defaultSort: SortingRule<string> & {
|
||||
columns?: string[];
|
||||
} = { id: 'createdAt' };
|
||||
|
||||
/**
|
||||
* @deprecated remove when flag `featureSwitchRefactor` is removed
|
||||
*/
|
||||
export const ProjectFeatureToggles = ({
|
||||
features,
|
||||
loading,
|
||||
environments: newEnvironments = [],
|
||||
}: IProjectFeatureTogglesProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [strategiesDialogState, setStrategiesDialogState] = useState({
|
||||
open: false,
|
||||
featureId: '',
|
||||
environmentName: '',
|
||||
});
|
||||
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
|
||||
featureId?: string;
|
||||
stale?: boolean;
|
||||
}>({});
|
||||
const [featureArchiveState, setFeatureArchiveState] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { setToastApiError } = useToast();
|
||||
const { value: storedParams, setValue: setStoredParams } =
|
||||
createLocalStorage(
|
||||
`${projectId}:FeatureToggleListTable:v1`,
|
||||
defaultSort,
|
||||
);
|
||||
const { value: globalStore, setValue: setGlobalStore } =
|
||||
useGlobalLocalStorage();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const environments = useEnvironmentsRef(
|
||||
loading
|
||||
? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }]
|
||||
: newEnvironments,
|
||||
);
|
||||
const { refetch } = useProject(projectId);
|
||||
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
|
||||
usePinnedFavorites(
|
||||
searchParams.has('favorites')
|
||||
? searchParams.get('favorites') === 'true'
|
||||
: globalStore.favorites,
|
||||
);
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const {
|
||||
onChangeRequestToggleClose,
|
||||
onChangeRequestToggleConfirm,
|
||||
changeRequestDialogDetails,
|
||||
} = useChangeRequestToggle(projectId);
|
||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||
const { uiConfig } = useUiConfig();
|
||||
const showEnvironmentLastSeen = Boolean(
|
||||
uiConfig.flags.lastSeenByEnvironment,
|
||||
);
|
||||
|
||||
const onFavorite = useCallback(
|
||||
async (feature: IFeatureToggleListItem) => {
|
||||
try {
|
||||
if (feature?.favorite) {
|
||||
await unfavorite(projectId, feature.name);
|
||||
} else {
|
||||
await favorite(projectId, feature.name);
|
||||
}
|
||||
refetch();
|
||||
} catch (error) {
|
||||
setToastApiError(
|
||||
'Something went wrong, could not update favorite',
|
||||
);
|
||||
}
|
||||
},
|
||||
[projectId, refetch, setToastApiError],
|
||||
);
|
||||
|
||||
const showTagsColumn = useMemo(
|
||||
() => features.some((feature) => feature?.tags?.length),
|
||||
[features],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'Select',
|
||||
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
||||
<Checkbox {...getToggleAllRowsSelectedProps()} />
|
||||
),
|
||||
Cell: ({ row }: any) => (
|
||||
<RowSelectCell {...row?.getToggleRowSelectedProps?.()} />
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
{
|
||||
id: 'favorite',
|
||||
Header: (
|
||||
<FavoriteIconHeader
|
||||
isActive={isFavoritesPinned}
|
||||
onClick={onChangeIsFavoritePinned}
|
||||
/>
|
||||
),
|
||||
accessor: 'favorite',
|
||||
Cell: ({ row: { original: feature } }: any) => (
|
||||
<FavoriteIconCell
|
||||
value={feature?.favorite}
|
||||
onClick={() => onFavorite(feature)}
|
||||
/>
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
{
|
||||
Header: 'Seen',
|
||||
accessor: 'lastSeenAt',
|
||||
Cell: ({ value, row: { original: feature } }: any) => {
|
||||
return showEnvironmentLastSeen ? (
|
||||
<FeatureEnvironmentSeenCell feature={feature} />
|
||||
) : (
|
||||
<FeatureSeenCell value={value} />
|
||||
);
|
||||
},
|
||||
align: 'center',
|
||||
maxWidth: 80,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
Cell: FeatureTypeCell,
|
||||
align: 'center',
|
||||
filterName: 'type',
|
||||
maxWidth: 80,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
Cell: ({ value }: { value: string }) => (
|
||||
<Tooltip title={value} arrow describeChild>
|
||||
<span>
|
||||
<LinkCell
|
||||
title={value}
|
||||
to={`/projects/${projectId}/features/${value}`}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
minWidth: 100,
|
||||
sortType: 'alphanumeric',
|
||||
searchable: true,
|
||||
},
|
||||
...(showTagsColumn
|
||||
? [
|
||||
{
|
||||
id: 'tags',
|
||||
Header: 'Tags',
|
||||
accessor: (row: IFeatureToggleListItem) =>
|
||||
row.tags
|
||||
?.map(({ type, value }) => `${type}:${value}`)
|
||||
.join('\n') || '',
|
||||
Cell: FeatureTagCell,
|
||||
width: 80,
|
||||
searchable: true,
|
||||
filterName: 'tags',
|
||||
filterBy(
|
||||
row: IFeatureToggleListItem,
|
||||
values: string[],
|
||||
) {
|
||||
return includesFilter(
|
||||
getColumnValues(this, row),
|
||||
values,
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
minWidth: 120,
|
||||
},
|
||||
...environments.map((value: ProjectEnvironmentType | string) => {
|
||||
const name =
|
||||
typeof value === 'string'
|
||||
? value
|
||||
: (value as ProjectEnvironmentType).environment;
|
||||
return {
|
||||
Header: loading ? () => '' : name,
|
||||
maxWidth: 90,
|
||||
id: `environments.${name}`,
|
||||
accessor: (row: ListItemType) =>
|
||||
row.environments[name]?.enabled,
|
||||
align: 'center',
|
||||
Cell: ({
|
||||
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}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasWarning}
|
||||
show={<VariantsWarningTooltip />}
|
||||
/>
|
||||
</StyledSwitchContainer>
|
||||
);
|
||||
},
|
||||
sortType: 'boolean',
|
||||
filterName: name,
|
||||
filterParsing: (value: boolean) =>
|
||||
value ? 'enabled' : 'disabled',
|
||||
};
|
||||
}),
|
||||
|
||||
{
|
||||
id: 'Actions',
|
||||
maxWidth: 56,
|
||||
width: 56,
|
||||
Cell: (props: { row: { original: ListItemType } }) => (
|
||||
<ActionsCell
|
||||
projectId={projectId}
|
||||
onOpenArchiveDialog={setFeatureArchiveState}
|
||||
onOpenStaleDialog={setFeatureStaleDialogState}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
],
|
||||
[projectId, environments, loading],
|
||||
);
|
||||
|
||||
const [searchValue, setSearchValue] = useState(
|
||||
searchParams.get('search') || '',
|
||||
);
|
||||
|
||||
const [showTitle, setShowTitle] = useState(true);
|
||||
|
||||
const featuresData = useMemo(
|
||||
() =>
|
||||
features.map((feature) => ({
|
||||
...feature,
|
||||
environments: Object.fromEntries(
|
||||
environments.map((env) => {
|
||||
const thisEnv = feature?.environments.find(
|
||||
(featureEnvironment) =>
|
||||
featureEnvironment?.name === env,
|
||||
);
|
||||
return [
|
||||
env,
|
||||
{
|
||||
name: env,
|
||||
enabled: thisEnv?.enabled || false,
|
||||
variantCount: thisEnv?.variantCount || 0,
|
||||
lastSeenAt: thisEnv?.lastSeenAt,
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
someEnabledEnvironmentHasVariants:
|
||||
feature.environments?.some(
|
||||
(featureEnvironment) =>
|
||||
featureEnvironment.variantCount > 0 &&
|
||||
featureEnvironment.enabled,
|
||||
) || false,
|
||||
})),
|
||||
[features, environments],
|
||||
);
|
||||
|
||||
const {
|
||||
data: searchedData,
|
||||
getSearchText,
|
||||
getSearchContext,
|
||||
} = useSearch(columns, searchValue, featuresData);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (loading) {
|
||||
return Array(6).fill({
|
||||
type: '-',
|
||||
name: 'Feature name',
|
||||
createdAt: new Date(),
|
||||
environments: {
|
||||
production: { name: 'production', enabled: false },
|
||||
},
|
||||
}) as FeatureSchema[];
|
||||
}
|
||||
return searchedData;
|
||||
}, [loading, searchedData]);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => {
|
||||
const allColumnIds = columns
|
||||
.map(
|
||||
(column: any) =>
|
||||
(column?.id as string) ||
|
||||
(typeof column?.accessor === 'string'
|
||||
? (column?.accessor as string)
|
||||
: ''),
|
||||
)
|
||||
.filter(Boolean);
|
||||
let hiddenColumns = environments
|
||||
.filter((_, index) => index >= 3)
|
||||
.map((environment) => `environments.${environment}`);
|
||||
|
||||
if (searchParams.has('columns')) {
|
||||
const columnsInParams =
|
||||
searchParams.get('columns')?.split(',') || [];
|
||||
const visibleColumns = [...staticColumns, ...columnsInParams];
|
||||
hiddenColumns = allColumnIds.filter(
|
||||
(columnId) => !visibleColumns.includes(columnId),
|
||||
);
|
||||
} else if (storedParams.columns) {
|
||||
const visibleColumns = [
|
||||
...staticColumns,
|
||||
...storedParams.columns,
|
||||
];
|
||||
hiddenColumns = allColumnIds.filter(
|
||||
(columnId) => !visibleColumns.includes(columnId),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
sortBy: [
|
||||
{
|
||||
id: searchParams.get('sort') || 'createdAt',
|
||||
desc: searchParams.has('order')
|
||||
? searchParams.get('order') === 'desc'
|
||||
: storedParams.desc,
|
||||
},
|
||||
],
|
||||
hiddenColumns,
|
||||
selectedRowIds: {},
|
||||
};
|
||||
},
|
||||
[environments], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
const getRowId = useCallback((row: any) => row.name, []);
|
||||
const {
|
||||
allColumns,
|
||||
headerGroups,
|
||||
rows,
|
||||
state: { selectedRowIds, sortBy, hiddenColumns },
|
||||
prepareRow,
|
||||
setHiddenColumns,
|
||||
toggleAllRowsSelected,
|
||||
} = useTable(
|
||||
{
|
||||
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||
data,
|
||||
initialState,
|
||||
sortTypes,
|
||||
autoResetHiddenColumns: false,
|
||||
autoResetSelectedRows: false,
|
||||
disableSortRemove: true,
|
||||
autoResetSortBy: false,
|
||||
getRowId,
|
||||
},
|
||||
useFlexLayout,
|
||||
useSortBy,
|
||||
useRowSelect,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
const tableState: Record<string, string> = {};
|
||||
tableState.sort = sortBy[0].id;
|
||||
if (sortBy[0].desc) {
|
||||
tableState.order = 'desc';
|
||||
}
|
||||
if (searchValue) {
|
||||
tableState.search = searchValue;
|
||||
}
|
||||
if (isFavoritesPinned) {
|
||||
tableState.favorites = 'true';
|
||||
}
|
||||
tableState.columns = allColumns
|
||||
.map(({ id }) => id)
|
||||
.filter(
|
||||
(id) =>
|
||||
!staticColumns.includes(id) && !hiddenColumns?.includes(id),
|
||||
)
|
||||
.join(',');
|
||||
|
||||
setSearchParams(tableState, {
|
||||
replace: true,
|
||||
});
|
||||
setStoredParams((params) => ({
|
||||
...params,
|
||||
id: sortBy[0].id,
|
||||
desc: sortBy[0].desc || false,
|
||||
columns: tableState.columns.split(','),
|
||||
}));
|
||||
setGlobalStore((params) => ({
|
||||
...params,
|
||||
favorites: Boolean(isFavoritesPinned),
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
loading,
|
||||
sortBy,
|
||||
hiddenColumns,
|
||||
searchValue,
|
||||
setSearchParams,
|
||||
isFavoritesPinned,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
className={styles.container}
|
||||
header={
|
||||
<PageHeader
|
||||
titleElement={
|
||||
showTitle
|
||||
? `Feature toggles (${rows.length})`
|
||||
: null
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={!isSmallScreen}
|
||||
show={
|
||||
<Search
|
||||
placeholder='Search and Filter'
|
||||
expandable
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onFocus={() => setShowTitle(false)}
|
||||
onBlur={() => setShowTitle(true)}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
id='projectFeatureToggles'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ColumnsMenu
|
||||
allColumns={allColumns}
|
||||
staticColumns={staticColumns}
|
||||
dividerAfter={['createdAt']}
|
||||
dividerBefore={['Actions']}
|
||||
isCustomized={Boolean(storedParams.columns)}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
/>
|
||||
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
uiConfig?.flags?.featuresExportImport,
|
||||
)}
|
||||
show={
|
||||
<Tooltip
|
||||
title='Export toggles visible in the table below'
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setShowExportDialog(true)
|
||||
}
|
||||
sx={(theme) => ({
|
||||
marginRight:
|
||||
theme.spacing(2),
|
||||
})}
|
||||
>
|
||||
<FileDownload />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<StyledResponsiveButton
|
||||
onClick={() =>
|
||||
navigate(getCreateTogglePath(projectId))
|
||||
}
|
||||
maxWidth='960px'
|
||||
Icon={Add}
|
||||
projectId={projectId}
|
||||
permission={CREATE_FEATURE}
|
||||
data-testid='NAVIGATE_TO_CREATE_FEATURE'
|
||||
>
|
||||
New feature toggle
|
||||
</StyledResponsiveButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={isSmallScreen}
|
||||
show={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
id='projectFeatureToggles'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageHeader>
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={searchValue?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No feature toggles found matching “
|
||||
{searchValue}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No feature toggles available. Get started by
|
||||
adding a new feature toggle.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<EnvironmentStrategyDialog
|
||||
onClose={() =>
|
||||
setStrategiesDialogState((prev) => ({
|
||||
...prev,
|
||||
open: false,
|
||||
}))
|
||||
}
|
||||
projectId={projectId}
|
||||
{...strategiesDialogState}
|
||||
/>
|
||||
<FeatureStaleDialog
|
||||
isStale={featureStaleDialogState.stale === true}
|
||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||
onClose={() => {
|
||||
setFeatureStaleDialogState({});
|
||||
refetch();
|
||||
}}
|
||||
featureId={featureStaleDialogState.featureId || ''}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<FeatureArchiveDialog
|
||||
isOpen={Boolean(featureArchiveState)}
|
||||
onConfirm={() => {
|
||||
refetch();
|
||||
}}
|
||||
onClose={() => {
|
||||
setFeatureArchiveState(undefined);
|
||||
}}
|
||||
featureIds={[featureArchiveState || '']}
|
||||
projectId={projectId}
|
||||
/>{' '}
|
||||
<ChangeRequestDialogue
|
||||
isOpen={changeRequestDialogDetails.isOpen}
|
||||
onClose={onChangeRequestToggleClose}
|
||||
environment={changeRequestDialogDetails?.environment}
|
||||
onConfirm={onChangeRequestToggleConfirm}
|
||||
messageComponent={
|
||||
<UpdateEnabledMessage
|
||||
featureName={
|
||||
changeRequestDialogDetails.featureName!
|
||||
}
|
||||
enabled={changeRequestDialogDetails.enabled!}
|
||||
environment={
|
||||
changeRequestDialogDetails?.environment!
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
Boolean(uiConfig?.flags?.featuresExportImport) &&
|
||||
!loading
|
||||
}
|
||||
show={
|
||||
<ExportDialog
|
||||
showExportDialog={showExportDialog}
|
||||
data={data}
|
||||
onClose={() => setShowExportDialog(false)}
|
||||
environments={environments}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
<BatchSelectionActionsBar
|
||||
count={Object.keys(selectedRowIds).length}
|
||||
>
|
||||
<ProjectFeaturesBatchActions
|
||||
selectedIds={Object.keys(selectedRowIds)}
|
||||
data={features}
|
||||
projectId={projectId}
|
||||
onResetSelection={() => toggleAllRowsSelected(false)}
|
||||
/>
|
||||
</BatchSelectionActionsBar>
|
||||
</>
|
||||
);
|
||||
};
|
@ -33,7 +33,6 @@ import { IProject } from 'interfaces/project';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
||||
@ -103,11 +102,6 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
const headerLoadingRef = useLoading(initialLoad);
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [strategiesDialogState, setStrategiesDialogState] = useState({
|
||||
open: false,
|
||||
featureId: '',
|
||||
environmentName: '',
|
||||
});
|
||||
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
|
||||
featureId?: string;
|
||||
stale?: boolean;
|
||||
@ -675,16 +669,6 @@ export const PaginatedProjectFeatureToggles = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<EnvironmentStrategyDialog
|
||||
onClose={() =>
|
||||
setStrategiesDialogState((prev) => ({
|
||||
...prev,
|
||||
open: false,
|
||||
}))
|
||||
}
|
||||
projectId={projectId}
|
||||
{...strategiesDialogState}
|
||||
/>
|
||||
<FeatureStaleDialog
|
||||
isStale={featureStaleDialogState.stale === true}
|
||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||
|
@ -31,9 +31,7 @@ import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/Fe
|
||||
import { IProject } from 'interfaces/project';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
||||
@ -97,11 +95,6 @@ export const ProjectFeatureToggles = ({
|
||||
const theme = useTheme();
|
||||
const { setToastApiError } = useToast();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [strategiesDialogState, setStrategiesDialogState] = useState({
|
||||
open: false,
|
||||
featureId: '',
|
||||
environmentName: '',
|
||||
});
|
||||
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
|
||||
featureId?: string;
|
||||
stale?: boolean;
|
||||
@ -621,16 +614,6 @@ export const ProjectFeatureToggles = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<EnvironmentStrategyDialog
|
||||
onClose={() =>
|
||||
setStrategiesDialogState((prev) => ({
|
||||
...prev,
|
||||
open: false,
|
||||
}))
|
||||
}
|
||||
projectId={projectId}
|
||||
{...strategiesDialogState}
|
||||
/>
|
||||
<FeatureStaleDialog
|
||||
isStale={featureStaleDialogState.stale === true}
|
||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||
|
@ -3,7 +3,6 @@ import useProject, {
|
||||
useProjectNameOrId,
|
||||
} from 'hooks/api/getters/useProject/useProject';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { ProjectFeatureToggles as LegacyProjectFeatureToggles } from './ProjectFeatureToggles/LegacyProjectFeatureToggles';
|
||||
import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles';
|
||||
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
@ -168,7 +167,6 @@ const ProjectOverview = () => {
|
||||
project;
|
||||
usePageTitle(`Project overview – ${projectName}`);
|
||||
const { setLastViewed } = useLastViewedProject();
|
||||
const featureSwitchRefactor = useUiFlag('featureSwitchRefactor');
|
||||
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
||||
|
||||
useEffect(() => {
|
||||
@ -190,25 +188,12 @@ const ProjectOverview = () => {
|
||||
<StyledContentContainer>
|
||||
<ProjectStats stats={project.stats} />
|
||||
<StyledProjectToggles>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(featureSwitchRefactor)}
|
||||
show={() => (
|
||||
<ProjectFeatureToggles
|
||||
key={loading ? 'loading' : 'ready'}
|
||||
features={features}
|
||||
environments={environments}
|
||||
loading={loading}
|
||||
onChange={refetch}
|
||||
/>
|
||||
)}
|
||||
elseShow={() => (
|
||||
<LegacyProjectFeatureToggles
|
||||
key={loading ? 'loading' : 'ready'}
|
||||
features={features}
|
||||
environments={environments}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
<ProjectFeatureToggles
|
||||
key={loading ? 'loading' : 'ready'}
|
||||
features={features}
|
||||
environments={environments}
|
||||
loading={loading}
|
||||
onChange={refetch}
|
||||
/>
|
||||
</StyledProjectToggles>
|
||||
</StyledContentContainer>
|
||||
|
@ -42,7 +42,7 @@ interface IUseAPI {
|
||||
|
||||
const timeApiCallStart = (requestId: string) => {
|
||||
// Store the start time in milliseconds
|
||||
console.log(`Starting timing for request: ${requestId}`);
|
||||
console.log(`[DEVELOPMENT LOG] Starting timing for request: ${requestId}`);
|
||||
return Date.now();
|
||||
};
|
||||
|
||||
@ -50,11 +50,13 @@ const timeApiCallEnd = (startTime: number, requestId: string) => {
|
||||
// Calculate the end time and subtract the start time
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
console.log(`Timing for request ${requestId}: ${duration} ms`);
|
||||
console.log(
|
||||
`[DEVELOPMENT LOG] Timing for request ${requestId}: ${duration} ms`,
|
||||
);
|
||||
|
||||
if (duration > 500) {
|
||||
console.error(
|
||||
'API call took over 500ms. This may indicate a rendering performance problem in your React component.',
|
||||
'[DEVELOPMENT LOG] API call took over 500ms. This may indicate a rendering performance problem in your React component.',
|
||||
requestId,
|
||||
duration,
|
||||
);
|
||||
|
@ -69,7 +69,6 @@ export type UiFlags = {
|
||||
banners?: boolean;
|
||||
disableEnvsOnRevive?: boolean;
|
||||
playgroundImprovements?: boolean;
|
||||
featureSwitchRefactor?: boolean;
|
||||
scheduledConfigurationChanges?: boolean;
|
||||
featureSearchAPI?: boolean;
|
||||
featureSearchFrontend?: boolean;
|
||||
|
@ -88,7 +88,6 @@ exports[`should create default config 1`] = `
|
||||
"featureNamingPattern": false,
|
||||
"featureSearchAPI": false,
|
||||
"featureSearchFrontend": false,
|
||||
"featureSwitchRefactor": false,
|
||||
"featuresExportImport": true,
|
||||
"filterInvalidClientMetrics": false,
|
||||
"googleAuthEnabled": false,
|
||||
|
@ -689,17 +689,15 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
];
|
||||
}
|
||||
|
||||
if (this.flagResolver.isEnabled('featureSwitchRefactor')) {
|
||||
selectColumns = [
|
||||
...selectColumns,
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment) as has_strategies',
|
||||
),
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment AND (feature_strategies.disabled IS NULL OR feature_strategies.disabled = false)) as has_enabled_strategies',
|
||||
),
|
||||
];
|
||||
}
|
||||
selectColumns = [
|
||||
...selectColumns,
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment) as has_strategies',
|
||||
),
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment AND (feature_strategies.disabled IS NULL OR feature_strategies.disabled = false)) as has_enabled_strategies',
|
||||
),
|
||||
];
|
||||
|
||||
const sortByMapping = {
|
||||
name: 'feature_name',
|
||||
@ -845,17 +843,15 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
];
|
||||
}
|
||||
|
||||
if (this.flagResolver.isEnabled('featureSwitchRefactor')) {
|
||||
selectColumns = [
|
||||
...selectColumns,
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment) as has_strategies',
|
||||
),
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment AND (feature_strategies.disabled IS NULL OR feature_strategies.disabled = false)) as has_enabled_strategies',
|
||||
),
|
||||
];
|
||||
}
|
||||
selectColumns = [
|
||||
...selectColumns,
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment) as has_strategies',
|
||||
),
|
||||
this.db.raw(
|
||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment AND (feature_strategies.disabled IS NULL OR feature_strategies.disabled = false)) as has_enabled_strategies',
|
||||
),
|
||||
];
|
||||
|
||||
query = query.select(selectColumns);
|
||||
const rows = await query;
|
||||
|
@ -33,7 +33,6 @@ export type IFlagKey =
|
||||
| 'banners'
|
||||
| 'disableEnvsOnRevive'
|
||||
| 'playgroundImprovements'
|
||||
| 'featureSwitchRefactor'
|
||||
| 'featureSearchAPI'
|
||||
| 'featureSearchFrontend'
|
||||
| 'scheduledConfigurationChanges'
|
||||
@ -157,10 +156,6 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_PLAYGROUND_IMPROVEMENTS,
|
||||
false,
|
||||
),
|
||||
featureSwitchRefactor: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SWITCH_REFACTOR,
|
||||
false,
|
||||
),
|
||||
featureSearchAPI: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
|
||||
false,
|
||||
|
@ -46,7 +46,6 @@ process.nextTick(async () => {
|
||||
useLastSeenRefactor: true,
|
||||
disableEnvsOnRevive: true,
|
||||
playgroundImprovements: true,
|
||||
featureSwitchRefactor: true,
|
||||
featureSearchAPI: true,
|
||||
featureSearchFrontend: true,
|
||||
},
|
||||
|
@ -23,7 +23,6 @@ beforeAll(async () => {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
featureSwitchRefactor: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -46,9 +45,7 @@ test('should report has strategies and enabled strategies', async () => {
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
featureSwitchRefactor: true,
|
||||
},
|
||||
flags: {},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
|
@ -63,6 +63,8 @@ test('Can connect environment to project', async () => {
|
||||
type: 'production',
|
||||
variantCount: 0,
|
||||
lastSeenAt: null,
|
||||
hasStrategies: false,
|
||||
hasEnabledStrategies: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@ -91,6 +93,8 @@ test('Can remove environment from project', async () => {
|
||||
type: 'production',
|
||||
variantCount: 0,
|
||||
lastSeenAt: null,
|
||||
hasStrategies: false,
|
||||
hasEnabledStrategies: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user