diff --git a/frontend/src/component/common/HtmlTooltip/HtmlTooltip.tsx b/frontend/src/component/common/HtmlTooltip/HtmlTooltip.tsx index c9f90995c5..e85a07b233 100644 --- a/frontend/src/component/common/HtmlTooltip/HtmlTooltip.tsx +++ b/frontend/src/component/common/HtmlTooltip/HtmlTooltip.tsx @@ -7,7 +7,13 @@ const StyledHtmlTooltipBody = styled('div')(({ theme }) => ({ })); const StyledHtmlTooltip = styled( - ({ className, maxWidth, maxHeight, ...props }: IHtmlTooltipProps) => ( + ({ + className, + maxWidth, + maxHeight, + fontSize, + ...props + }: IHtmlTooltipProps) => ( {props.title}} @@ -15,11 +21,21 @@ const StyledHtmlTooltip = styled( /> ), { - shouldForwardProp: prop => prop !== 'maxWidth' && prop !== 'maxHeight', + shouldForwardProp: prop => + prop !== 'maxWidth' && prop !== 'maxHeight' && prop !== 'fontSize', } -)<{ maxWidth?: SpacingArgument; maxHeight?: SpacingArgument }>( - ({ theme, maxWidth, maxHeight }) => ({ - maxWidth: maxWidth || theme.spacing(37.5), +)<{ + maxWidth?: SpacingArgument; + maxHeight?: SpacingArgument; + fontSize?: string; +}>( + ({ + theme, + maxWidth = theme.spacing(37.5), + maxHeight = theme.spacing(37.5), + fontSize = theme.fontSizes.smallerBody, + }) => ({ + maxWidth, [`& .${tooltipClasses.tooltip}`]: { display: 'flex', flexDirection: 'column', @@ -31,7 +47,8 @@ const StyledHtmlTooltip = styled( fontWeight: theme.fontWeight.medium, maxWidth: 'inherit', border: `1px solid ${theme.palette.lightBorder}`, - maxHeight: maxHeight || theme.spacing(37.5), + maxHeight, + fontSize, }, [`& .${tooltipClasses.arrow}`]: { '&:before': { @@ -45,6 +62,7 @@ const StyledHtmlTooltip = styled( export interface IHtmlTooltipProps extends TooltipProps { maxWidth?: SpacingArgument; maxHeight?: SpacingArgument; + fontSize?: string; } export const HtmlTooltip = (props: IHtmlTooltipProps) => ( 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 1359e9b52e..0ca597fae8 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx @@ -4,6 +4,8 @@ import { useState } from 'react'; 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'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning'; const StyledContainer = styled('div')(({ theme }) => ({ padding: theme.spacing(3), @@ -21,6 +23,15 @@ const StyledLabel = styled('p')(({ theme }) => ({ const StyledSubLabel = styled('p')(({ theme }) => ({ fontSize: theme.fontSizes.smallBody, color: theme.palette.text.secondary, + display: 'flex', + alignItems: 'center', +})); + +const StyledSeparator = styled('span')(({ theme }) => ({ + padding: theme.spacing(0, 0.5), + '::after': { + content: '"-"', + }, })); const StyledLink = styled(Link)(() => ({ @@ -44,7 +55,9 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({ }: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => { const [showInfoBox, setShowInfoBox] = useState(false); const [environmentName, setEnvironmentName] = useState(''); - + const someEnabledEnvironmentHasVariants = feature.environments.some( + environment => environment.enabled && environment.variants?.length + ); return ( {header} @@ -58,7 +71,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({ const variantsLink = variants.length > 0 && ( <> - {' - '} + ); + const hasWarning = + environment.enabled && + variants.length == 0 && + someEnabledEnvironmentHasVariants; return ( {strategiesLabel} {variantsLink} + + + + + } + /> diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning.tsx new file mode 100644 index 0000000000..9c4acab2be --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning.tsx @@ -0,0 +1,33 @@ +import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; +import { WarningAmber } from '@mui/icons-material'; +import { styled } from '@mui/material'; + +const StyledWarningAmber = styled(WarningAmber)(({ theme }) => ({ + color: theme.palette.warning.main, + fontSize: theme.fontSizes.bodySize, +})); + +const VariantsWarningTooltip = () => { + return ( + + This environment has no variants enabled. If you check this + feature's variants in this environment, you will get the{' '} + + disabled variant + + . + + } + > + + + ); +}; + +export default VariantsWarningTooltip; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index c68b7e35bc..3441eb2ac6 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -45,11 +45,28 @@ import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; +import { flexRow } from 'themes/themeStyles'; +import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning'; 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']; @@ -64,8 +81,10 @@ type ListItemType = Pick< [key in string]: { name: string; enabled: boolean; + variantCount: number; }; }; + someEnabledEnvironmentHasVariants: boolean; }; const staticColumns = ['Actions', 'name', 'favorite']; @@ -273,15 +292,28 @@ export const ProjectFeatureToggles = ({ }: { 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) => @@ -311,38 +343,31 @@ export const ProjectFeatureToggles = ({ const featuresData = useMemo( () => - features.map( - ({ - name, - lastSeenAt, - createdAt, - type, - stale, - tags, - favorite, - environments: featureEnvironments, - }) => ({ - name, - lastSeenAt, - createdAt, - type, - stale, - tags, - favorite, - environments: Object.fromEntries( - environments.map(env => [ + features.map(feature => ({ + ...feature, + environments: Object.fromEntries( + environments.map(env => { + const thisEnv = feature?.environments.find( + featureEnvironment => + featureEnvironment?.name === env + ); + return [ env, { name: env, - enabled: - featureEnvironments?.find( - feature => feature?.name === env - )?.enabled || false, + enabled: thisEnv?.enabled || false, + variantCount: thisEnv?.variantCount || 0, }, - ]) - ), - }) - ), + ]; + }) + ), + someEnabledEnvironmentHasVariants: + feature.environments?.some( + featureEnvironment => + featureEnvironment.variantCount > 0 && + featureEnvironment.enabled + ) || false, + })), [features, environments] ); diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index ef784e1174..c28097ca44 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -15,6 +15,7 @@ export interface IFeatureToggleListItem { export interface IEnvironments { name: string; enabled: boolean; + variantCount: number; } export interface IFeatureToggle { diff --git a/src/lib/db/feature-strategy-store.ts b/src/lib/db/feature-strategy-store.ts index 2d69e165d8..5c6e442268 100644 --- a/src/lib/db/feature-strategy-store.ts +++ b/src/lib/db/feature-strategy-store.ts @@ -392,6 +392,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { enabled: r.enabled, type: r.environment_type, sortOrder: r.environment_sort_order, + variantCount: r.variants?.length || 0, }; } @@ -469,6 +470,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { 'features.stale as stale', 'feature_environments.enabled as enabled', 'feature_environments.environment as environment', + 'feature_environments.variants as variants', 'environments.type as environment_type', 'environments.sort_order as environment_sort_order', 'ft.tag_value as tag_value', diff --git a/src/lib/openapi/spec/feature-environment-schema.ts b/src/lib/openapi/spec/feature-environment-schema.ts index a2a63133ce..47a96dd45e 100644 --- a/src/lib/openapi/spec/feature-environment-schema.ts +++ b/src/lib/openapi/spec/feature-environment-schema.ts @@ -27,6 +27,9 @@ export const featureEnvironmentSchema = { sortOrder: { type: 'number', }, + variantCount: { + type: 'number', + }, strategies: { type: 'array', items: { diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index fd967de7ac..7bd91f983a 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -91,7 +91,7 @@ export interface FeatureToggleLegacy extends FeatureToggle { enabled: boolean; } -export interface IEnvironmentDetail extends IEnvironmentOverview { +export interface IEnvironmentDetail extends IEnvironmentBase { strategies: IStrategyConfig[]; variants: IVariant[]; } @@ -152,13 +152,17 @@ export interface IEnvironmentClone { clonePermissions?: boolean; } -export interface IEnvironmentOverview { +export interface IEnvironmentBase { name: string; enabled: boolean; type: string; sortOrder: number; } +export interface IEnvironmentOverview extends IEnvironmentBase { + variantCount: number; +} + export interface IFeatureOverview { name: string; type: string; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index e059bea6d3..d180594fc3 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -1161,6 +1161,9 @@ exports[`should serve the OpenAPI spec 1`] = ` "type": { "type": "string", }, + "variantCount": { + "type": "number", + }, }, "required": [ "name", diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts index 23a33268fe..8c7de595fc 100644 --- a/src/test/e2e/services/environment-service.test.ts +++ b/src/test/e2e/services/environment-service.test.ts @@ -61,6 +61,7 @@ test('Can connect environment to project', async () => { enabled: false, sortOrder: 9999, type: 'production', + variantCount: 0, }, ]); }); @@ -87,6 +88,7 @@ test('Can remove environment from project', async () => { enabled: false, sortOrder: 9999, type: 'production', + variantCount: 0, }, ]); }); diff --git a/src/test/fixtures/fake-feature-environment-store.ts b/src/test/fixtures/fake-feature-environment-store.ts index 507a06e09d..0f4c5e5419 100644 --- a/src/test/fixtures/fake-feature-environment-store.ts +++ b/src/test/fixtures/fake-feature-environment-store.ts @@ -39,7 +39,7 @@ export default class FakeFeatureEnvironmentStore .filter( (fe) => fe.featureName === featureName && - environments.indexOf(fe.environment) !== -1, + environments.includes(fe.environment), ) .map((fe) => (fe.variants = variants)); }