diff --git a/frontend/src/component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog.tsx b/frontend/src/component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog.tsx
deleted file mode 100644
index bc97eca01fec..000000000000
--- a/frontend/src/component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog.tsx
+++ /dev/null
@@ -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 (
- onClose()}
- title='You need to add a strategy to your toggle'
- primaryButtonText='Take me directly to add strategy'
- permissionButton={
-
- Take me directly to add strategy
-
- }
- secondaryButtonText='Cancel'
- >
-
- Before you can enable the toggle in the environment, you need to
- add an activation strategy.
-
-
- You can add the activation strategy by selecting the toggle,
- open the environment accordion and add the activation strategy.
-
-
- );
-};
-
-export default EnvironmentStrategyDialog;
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx
index 972bafe917e9..b9261140d6c5 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx
@@ -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;
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 = (
<>
{' '}
- {enabled ? 'enabled' : 'disabled'} in
+
+ {environment.enabled ? 'enabled' : 'disabled'} in
+
-
+
>
);
+ 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 (
@@ -66,8 +86,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
projectId={projectId}
environmentName={environment.name}
onToggle={handleToggle}
- onError={showInfoBox}
- value={enabled}
+ value={environment.enabled}
/>
{children ?? defaultContent}
@@ -76,6 +95,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
+ {featureToggleModals}
);
};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx
index 166f36508b1d..9fe6d71c7fc7 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx
@@ -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);
- }}
>
{environment.name}
@@ -120,13 +112,6 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
);
})}
- setShowInfoBox(false)}
- projectId={feature.project}
- featureId={feature.name}
- environmentName={environmentName}
- />
);
};
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch.tsx
deleted file mode 100644
index fb92c9aec494..000000000000
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch.tsx
+++ /dev/null
@@ -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 = ({
- 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(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 (
- <>
-
-
-
- setShowEnabledDialog(false)}
- environment={environmentName}
- disabledStrategiesCount={disabledStrategiesCount}
- onActivateDisabledStrategies={onActivateStrategies}
- onAddDefaultStrategy={onAddDefaultStrategy}
- />
-
- }
- />
- setShowProdGuard(false)}
- onClick={handleClick}
- loading={loading}
- label={`${isChecked ? 'Disable' : 'Enable'} Environment`}
- />
- >
- );
-};
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx
index b726e433cc54..7f8248f21e0d 100644
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx
@@ -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();
}
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx
deleted file mode 100644
index 37b3dded0520..000000000000
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx
+++ /dev/null
@@ -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 & {
- 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) => (
-
- ),
- Cell: ({ row }: any) => (
-
- ),
- maxWidth: 50,
- disableSortBy: true,
- hideInMenu: true,
- },
- {
- id: 'favorite',
- Header: (
-
- ),
- accessor: 'favorite',
- Cell: ({ row: { original: feature } }: any) => (
- onFavorite(feature)}
- />
- ),
- maxWidth: 50,
- disableSortBy: true,
- hideInMenu: true,
- },
- {
- Header: 'Seen',
- accessor: 'lastSeenAt',
- Cell: ({ value, row: { original: feature } }: any) => {
- return showEnvironmentLastSeen ? (
-
- ) : (
-
- );
- },
- align: 'center',
- maxWidth: 80,
- },
- {
- Header: 'Type',
- accessor: 'type',
- Cell: FeatureTypeCell,
- align: 'center',
- filterName: 'type',
- maxWidth: 80,
- },
- {
- Header: 'Name',
- accessor: 'name',
- Cell: ({ value }: { value: string }) => (
-
-
-
-
-
- ),
- 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 (
-
-
- }
- />
-
- );
- },
- sortType: 'boolean',
- filterName: name,
- filterParsing: (value: boolean) =>
- value ? 'enabled' : 'disabled',
- };
- }),
-
- {
- id: 'Actions',
- maxWidth: 56,
- width: 56,
- Cell: (props: { row: { original: ListItemType } }) => (
-
- ),
- 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 = {};
- 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 (
- <>
-
- setShowTitle(false)}
- onBlur={() => setShowTitle(true)}
- hasFilters
- getSearchContext={getSearchContext}
- id='projectFeatureToggles'
- />
- }
- />
-
-
-
-
- setShowExportDialog(true)
- }
- sx={(theme) => ({
- marginRight:
- theme.spacing(2),
- })}
- >
-
-
-
- }
- />
-
- navigate(getCreateTogglePath(projectId))
- }
- maxWidth='960px'
- Icon={Add}
- projectId={projectId}
- permission={CREATE_FEATURE}
- data-testid='NAVIGATE_TO_CREATE_FEATURE'
- >
- New feature toggle
-
- >
- }
- >
-
- }
- />
-
- }
- >
-
-
-
- 0}
- show={
-
- No feature toggles found matching “
- {searchValue}
- ”
-
- }
- elseShow={
-
- No feature toggles available. Get started by
- adding a new feature toggle.
-
- }
- />
- }
- />
-
- setStrategiesDialogState((prev) => ({
- ...prev,
- open: false,
- }))
- }
- projectId={projectId}
- {...strategiesDialogState}
- />
- {
- setFeatureStaleDialogState({});
- refetch();
- }}
- featureId={featureStaleDialogState.featureId || ''}
- projectId={projectId}
- />
- {
- refetch();
- }}
- onClose={() => {
- setFeatureArchiveState(undefined);
- }}
- featureIds={[featureArchiveState || '']}
- projectId={projectId}
- />{' '}
-
- }
- />
- setShowExportDialog(false)}
- environments={environments}
- />
- }
- />
-
-
- toggleAllRowsSelected(false)}
- />
-
- >
- );
-};
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx
index af2c37ad4c41..3d6091fff737 100644
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx
@@ -32,7 +32,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';
@@ -102,11 +101,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;
@@ -659,16 +653,6 @@ export const PaginatedProjectFeatureToggles = ({
/>
}
/>
-
- setStrategiesDialogState((prev) => ({
- ...prev,
- open: false,
- }))
- }
- projectId={projectId}
- {...strategiesDialogState}
- />
}
/>
-
- setStrategiesDialogState((prev) => ({
- ...prev,
- open: false,
- }))
- }
- projectId={projectId}
- {...strategiesDialogState}
- />
{
project;
usePageTitle(`Project overview – ${projectName}`);
const { setLastViewed } = useLastViewedProject();
- const featureSwitchRefactor = useUiFlag('featureSwitchRefactor');
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
useEffect(() => {
@@ -190,25 +188,12 @@ const ProjectOverview = () => {
- (
-
- )}
- elseShow={() => (
-
- )}
+
diff --git a/frontend/src/hooks/api/actions/useApi/useApi.ts b/frontend/src/hooks/api/actions/useApi/useApi.ts
index 1bc062787065..697b38af0b8a 100644
--- a/frontend/src/hooks/api/actions/useApi/useApi.ts
+++ b/frontend/src/hooks/api/actions/useApi/useApi.ts
@@ -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,
);
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 6ba426531303..ffb6884c5cfd 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -69,7 +69,6 @@ export type UiFlags = {
banners?: boolean;
disableEnvsOnRevive?: boolean;
playgroundImprovements?: boolean;
- featureSwitchRefactor?: boolean;
scheduledConfigurationChanges?: boolean;
featureSearchAPI?: boolean;
featureSearchFrontend?: boolean;
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index 5c07d1b07efe..82c9eb9daef9 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -88,7 +88,6 @@ exports[`should create default config 1`] = `
"featureNamingPattern": false,
"featureSearchAPI": false,
"featureSearchFrontend": false,
- "featureSwitchRefactor": false,
"featuresExportImport": true,
"filterInvalidClientMetrics": false,
"googleAuthEnabled": false,
diff --git a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts
index 0f072bb0dc0e..9f681f5eaab4 100644
--- a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts
+++ b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts
@@ -688,17 +688,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',
@@ -840,17 +838,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;
diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts
index 5832ff7f64e6..c67d7133cdbb 100644
--- a/src/lib/types/experimental.ts
+++ b/src/lib/types/experimental.ts
@@ -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,
diff --git a/src/server-dev.ts b/src/server-dev.ts
index 198800edd61b..44b8ff84a98a 100644
--- a/src/server-dev.ts
+++ b/src/server-dev.ts
@@ -46,7 +46,6 @@ process.nextTick(async () => {
useLastSeenRefactor: true,
disableEnvsOnRevive: true,
playgroundImprovements: true,
- featureSwitchRefactor: true,
featureSearchAPI: true,
featureSearchFrontend: true,
},
diff --git a/src/test/e2e/api/admin/project/projects.e2e.test.ts b/src/test/e2e/api/admin/project/projects.e2e.test.ts
index 1487e6e72769..a99992f17d19 100644
--- a/src/test/e2e/api/admin/project/projects.e2e.test.ts
+++ b/src/test/e2e/api/admin/project/projects.e2e.test.ts
@@ -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,
diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts
index b94a2c0d92e2..d0463188b32a 100644
--- a/src/test/e2e/services/environment-service.test.ts
+++ b/src/test/e2e/services/environment-service.test.ts
@@ -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,
},
]);
});