From 581fb405540e616c5f32df8e2811753b74391cad Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:28:44 +0200 Subject: [PATCH] refactor: rename old feature toggles list to legacy --- .../FeatureToggleSwitch.tsx | 194 +---- ....types.ts => FeatureToggleSwitch.types.ts} | 0 .../LegacyFeatureToggleSwitch.tsx | 257 ++++++ .../NewFeatureToggleSwitch.tsx | 65 -- .../createFeatureToggleCell.tsx | 4 +- .../useFeatureToggleSwitch.tsx | 2 +- .../LegacyProjectFeatureToggles.tsx | 729 ++++++++++++++++++ .../project/Project/ProjectOverview.tsx | 4 +- 8 files changed, 1004 insertions(+), 251 deletions(-) rename frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/{NewFeatureToggleSwitch.types.ts => FeatureToggleSwitch.types.ts} (100%) create mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch.tsx delete mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/NewFeatureToggleSwitch.tsx create mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx index 642367b036..edb7b1f24f 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx @@ -1,181 +1,38 @@ -import { useState, type VFC } from 'react'; +import { type VFC } from 'react'; import { Box, styled } from '@mui/material'; import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch'; import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions'; import { useOptimisticUpdate } from './hooks/useOptimisticUpdate'; import { flexRow } from 'themes/themeStyles'; -import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors'; -import { formatUnknownError } from 'utils/formatUnknownError'; -import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; -import useToast from 'hooks/useToast'; -import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; -import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle'; -import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage'; -import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog'; -import { - FeatureStrategyProdGuard, - useFeatureStrategyProdGuard, -} from 'component/feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard'; -import { EnableEnvironmentDialog } from './EnableEnvironmentDialog/EnableEnvironmentDialog'; const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({ mx: 'auto', ...flexRow, })); -interface IFeatureToggleSwitchProps { +type FeatureToggleSwitchProps = { featureId: string; - environmentName: string; projectId: string; + environmentName: string; value: boolean; - type: string; - hasStrategies?: boolean; - hasEnabledStrategies?: boolean; - onError?: () => void; - onToggle?: ( - projectId: string, - feature: string, - env: string, - state: boolean, - ) => void; -} + onToggle: (newState: boolean, onRollback: () => void) => void; +}; -export const FeatureToggleSwitch: VFC = ({ +export const FeatureToggleSwitch: VFC = ({ projectId, featureId, environmentName, value, - type, - hasStrategies, - hasEnabledStrategies, 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 enableProdGuard = useFeatureStrategyProdGuard(type, environmentName); - const [showProdGuard, setShowProdGuard] = useState(false); - const featureHasOnlyDisabledStrategies = - hasStrategies && !hasEnabledStrategies; - - const handleToggleEnvironmentOn = async ( - shouldActivateDisabled = false, - ) => { - try { - setIsChecked(!isChecked); - await toggleFeatureEnvironmentOn( - projectId, - featureId, - environmentName, - shouldActivateDisabled, - ); - setToastData({ - type: 'success', - title: `Available in ${environmentName}`, - text: `${featureId} is now available in ${environmentName} based on its defined strategies.`, - }); - onToggle?.(projectId, featureId, 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, - featureId, - environmentName, - ); - setToastData({ - type: 'success', - title: `Unavailable in ${environmentName}`, - text: `${featureId} is unavailable in ${environmentName} and its strategies will no longer have any effect.`, - }); - onToggle?.(projectId, featureId, environmentName, !isChecked); - } catch (error: unknown) { - setToastApiError(formatUnknownError(error)); - rollbackIsChecked(); - } - }; - - const handleClick = async () => { - setShowProdGuard(false); - if (isChangeRequestConfigured(environmentName)) { - if (featureHasOnlyDisabledStrategies) { - setShowEnabledDialog(true); - } else { - onChangeRequestToggle( - featureId, - 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(featureId, environmentName, !isChecked, true); - } else { - await handleToggleEnvironmentOn(true); - } - setShowEnabledDialog(false); - }; - - const onAddDefaultStrategy = async () => { - if (isChangeRequestConfigured(environmentName)) { - onChangeRequestToggle( - featureId, - environmentName, - !isChecked, - false, - ); - } else { - await handleToggleEnvironmentOn(); - } - setShowEnabledDialog(false); + const onClick = () => { + setIsChecked(!isChecked); + requestAnimationFrame(() => { + onToggle(!isChecked, rollbackIsChecked); + }); }; const key = `${featureId}-${environmentName}`; @@ -199,35 +56,10 @@ export const FeatureToggleSwitch: VFC = ({ inputProps={{ 'aria-label': environmentName }} onClick={onClick} data-testid={'permission-switch'} + disableRipple + disabled={value !== isChecked} /> - setShowEnabledDialog(false)} - environment={environmentName} - 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/NewFeatureToggleSwitch.types.ts b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.types.ts similarity index 100% rename from frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/NewFeatureToggleSwitch.types.ts rename to frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.types.ts diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch.tsx new file mode 100644 index 0000000000..9c97a86adc --- /dev/null +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/LegacyFeatureToggleSwitch.tsx @@ -0,0 +1,257 @@ +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 + */ +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/NewFeatureToggleSwitch.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/NewFeatureToggleSwitch.tsx deleted file mode 100644 index edb7b1f24f..0000000000 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/NewFeatureToggleSwitch.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { type VFC } from 'react'; -import { Box, styled } from '@mui/material'; -import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch'; -import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions'; -import { useOptimisticUpdate } from './hooks/useOptimisticUpdate'; -import { flexRow } from 'themes/themeStyles'; - -const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({ - mx: 'auto', - ...flexRow, -})); - -type FeatureToggleSwitchProps = { - featureId: string; - projectId: string; - environmentName: string; - value: boolean; - onToggle: (newState: boolean, onRollback: () => void) => void; -}; - -export const FeatureToggleSwitch: VFC = ({ - projectId, - featureId, - environmentName, - value, - onToggle, -}) => { - const [isChecked, setIsChecked, rollbackIsChecked] = - useOptimisticUpdate(value); - - const onClick = () => { - setIsChecked(!isChecked); - requestAnimationFrame(() => { - onToggle(!isChecked, rollbackIsChecked); - }); - }; - - const key = `${featureId}-${environmentName}`; - - return ( - <> - - - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx index 120c8e73dc..832a87dc8e 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx @@ -3,9 +3,9 @@ import { styled } from '@mui/material'; import { flexRow } from 'themes/themeStyles'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning'; -import { FeatureToggleSwitch } from './NewFeatureToggleSwitch'; +import { FeatureToggleSwitch } from './FeatureToggleSwitch'; import type { ListItemType } from '../ProjectFeatureToggles.types'; -import type { UseFeatureToggleSwitchType } from './NewFeatureToggleSwitch.types'; +import type { UseFeatureToggleSwitchType } from './FeatureToggleSwitch.types'; const StyledSwitchContainer = styled('div', { shouldForwardProp: (prop) => prop !== 'hasWarning', diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx index a8cca49cd6..cfee1f9b44 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx @@ -13,7 +13,7 @@ import { EnableEnvironmentDialog } from './EnableEnvironmentDialog/EnableEnviron import { OnFeatureToggleSwitchArgs, UseFeatureToggleSwitchType, -} from './NewFeatureToggleSwitch.types'; +} from './FeatureToggleSwitch.types'; type Middleware = (next: () => void) => void; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx new file mode 100644 index 0000000000..5f81e662a2 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/LegacyProjectFeatureToggles.tsx @@ -0,0 +1,729 @@ +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'; + +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 + */ +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 { 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) => { + if (feature?.favorite) { + await unfavorite(projectId, feature.name); + } else { + await favorite(projectId, feature.name); + } + refetch(); + }, + [projectId, refetch], + ); + + 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/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx index 1cd17d2b59..f735b82184 100644 --- a/frontend/src/component/project/Project/ProjectOverview.tsx +++ b/frontend/src/component/project/Project/ProjectOverview.tsx @@ -3,7 +3,7 @@ import useProject, { useProjectNameOrId, } from 'hooks/api/getters/useProject/useProject'; import { Box, styled } from '@mui/material'; -import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles'; +import { ProjectFeatureToggles as LegacyProjectFeatureToggles } from './ProjectFeatureToggles/LegacyProjectFeatureToggles'; import ProjectInfo from './ProjectInfo/ProjectInfo'; import { usePageTitle } from 'hooks/usePageTitle'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; @@ -59,7 +59,7 @@ const ProjectOverview = () => { -