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));
}