diff --git a/frontend/src/component/context/ContextFieldUsage/ContextFieldUsage.test.tsx b/frontend/src/component/context/ContextFieldUsage/ContextFieldUsage.test.tsx
new file mode 100644
index 0000000000..ad7b22c9a5
--- /dev/null
+++ b/frontend/src/component/context/ContextFieldUsage/ContextFieldUsage.test.tsx
@@ -0,0 +1,55 @@
+import { render } from '../../../utils/testRenderer';
+import { screen } from '@testing-library/react';
+import React from 'react';
+import { testServerRoute, testServerSetup } from '../../../utils/testServer';
+import { UIProviderContainer } from '../../providers/UIProvider/UIProviderContainer';
+import { ContextFieldUsage } from './ContextFieldUsage';
+
+const server = testServerSetup();
+const contextFieldName = 'appName';
+
+const setupRoutes = () => {
+ testServerRoute(
+ server,
+ `api/admin/context/${contextFieldName}/strategies`,
+ {
+ strategies: [
+ {
+ id: '4b3ad603-4727-4782-bd61-efc530e37209',
+ projectId: 'faaa',
+ featureName: 'tests',
+ strategyName: 'flexibleRollout',
+ environment: 'development',
+ },
+ ],
+ }
+ );
+ testServerRoute(server, '/api/admin/ui-config', {
+ flags: {
+ segmentContextFieldUsage: true,
+ },
+ });
+
+ testServerRoute(server, '/api/admin/projects', {
+ version: 1,
+ projects: [
+ {
+ id: 'faaa',
+ },
+ ],
+ });
+};
+
+test('should show usage of context field', async () => {
+ setupRoutes();
+
+ const contextFieldName = 'appName';
+ render(
+
+
+
+ );
+
+ await screen.findByText('Usage of this context field:');
+ await screen.findByText('tests (Gradual rollout)');
+});
diff --git a/frontend/src/component/context/ContextFieldUsage/ContextFieldUsage.tsx b/frontend/src/component/context/ContextFieldUsage/ContextFieldUsage.tsx
new file mode 100644
index 0000000000..59f793ec1e
--- /dev/null
+++ b/frontend/src/component/context/ContextFieldUsage/ContextFieldUsage.tsx
@@ -0,0 +1,89 @@
+import { Alert, styled } from '@mui/material';
+import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
+import { IFeatureStrategy } from 'interfaces/strategy';
+import { Link } from 'react-router-dom';
+import { formatStrategyName } from 'utils/strategyNames';
+import { useStrategiesByContext } from 'hooks/api/getters/useStrategiesByContext/useStrategiesByContext';
+import useProjects from 'hooks/api/getters/useProjects/useProjects';
+
+const StyledUl = styled('ul')(({ theme }) => ({
+ listStyle: 'none',
+ paddingLeft: 0,
+}));
+
+const StyledAlert = styled(Alert)(({ theme }) => ({
+ marginTop: theme.spacing(1),
+}));
+
+interface IContextFieldUsageProps {
+ contextName: string;
+}
+
+export const ContextFieldUsage = ({ contextName }: IContextFieldUsageProps) => {
+ const { strategies } = useStrategiesByContext(contextName);
+ const { projects } = useProjects();
+
+ const projectsUsed = Array.from(
+ new Set(
+ strategies.map(({ projectId }) => projectId!).filter(Boolean)
+ )
+ );
+
+ const projectList = (
+
+ {projectsUsed.map(projectId => (
+
+
+ {projects.find(({ id }) => id === projectId)?.name ??
+ projectId}
+
+
+ {strategies
+ ?.filter(
+ strategy => strategy.projectId === projectId
+ )
+ .map(strategy => (
+ -
+
+ {strategy.featureName!}{' '}
+ {formatStrategyNameParens(strategy)}
+
+
+ ))}
+
+
+ ))}
+
+ );
+ if (projectsUsed.length > 0) {
+ return (
+
+ Usage of this context field:
+ {projectList}
+
+ );
+ }
+
+ return null;
+};
+
+const formatStrategyNameParens = (strategy: IFeatureStrategy): string => {
+ if (!strategy.strategyName) {
+ return '';
+ }
+
+ return `(${formatStrategyName(strategy.strategyName)})`;
+};
diff --git a/frontend/src/component/context/ContextForm/ContextForm.tsx b/frontend/src/component/context/ContextForm/ContextForm.tsx
index aa3b3a55c3..106a77c6ed 100644
--- a/frontend/src/component/context/ContextForm/ContextForm.tsx
+++ b/frontend/src/component/context/ContextForm/ContextForm.tsx
@@ -13,6 +13,9 @@ import { Add } from '@mui/icons-material';
import { ILegalValue } from 'interfaces/context';
import { ContextFormChip } from 'component/context/ContectFormChip/ContextFormChip';
import { ContextFormChipList } from 'component/context/ContectFormChip/ContextFormChipList';
+import { ContextFieldUsage } from '../ContextFieldUsage/ContextFieldUsage';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IContextForm {
contextName: string;
@@ -101,6 +104,7 @@ export const ContextForm: React.FC = ({
const [value, setValue] = useState('');
const [valueDesc, setValueDesc] = useState('');
const [valueFocused, setValueFocused] = useState(false);
+ const { uiConfig } = useUiConfig();
const isMissingValue = valueDesc.trim() && !value.trim();
@@ -263,6 +267,10 @@ export const ContextForm: React.FC = ({
/>
{stickiness ? 'On' : 'Off'}
+ }
+ />
{children}
diff --git a/frontend/src/hooks/api/getters/useStrategiesByContext/useStrategiesByContext.ts b/frontend/src/hooks/api/getters/useStrategiesByContext/useStrategiesByContext.ts
new file mode 100644
index 0000000000..67eb1f04c8
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useStrategiesByContext/useStrategiesByContext.ts
@@ -0,0 +1,39 @@
+import { mutate } from 'swr';
+import { useCallback } from 'react';
+import { formatApiPath } from 'utils/formatPath';
+import handleErrorResponses from '../httpErrorResponseHandler';
+import { IFeatureStrategy } from 'interfaces/strategy';
+import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
+
+export interface IUseStrategiesByContextOutput {
+ strategies: IFeatureStrategy[];
+ refetchUsedSegments: () => void;
+ loading: boolean;
+ error?: Error;
+}
+
+export const useStrategiesByContext = (
+ id?: string | number
+): IUseStrategiesByContextOutput => {
+ const path = formatApiPath(`api/admin/context/${id}/strategies`);
+ const { data, error } = useConditionalSWR(id, [], path, () =>
+ fetchUsedSegment(path)
+ );
+
+ const refetchUsedSegments = useCallback(() => {
+ mutate(path).catch(console.warn);
+ }, [path]);
+
+ return {
+ strategies: data?.strategies || [],
+ refetchUsedSegments,
+ loading: !error && !data,
+ error,
+ };
+};
+
+const fetchUsedSegment = (path: string) => {
+ return fetch(path, { method: 'GET' })
+ .then(handleErrorResponses('Strategies by context'))
+ .then(res => res.json());
+};
diff --git a/src/lib/db/context-field-store.ts b/src/lib/db/context-field-store.ts
index 40fdecfcca..698c239f8d 100644
--- a/src/lib/db/context-field-store.ts
+++ b/src/lib/db/context-field-store.ts
@@ -40,8 +40,12 @@ const mapRow = (row: ContextFieldDB): IContextField => ({
sortOrder: row.sort_order,
legalValues: row.legal_values || [],
createdAt: row.created_at,
- usedInProjects: row.used_in_projects ? Number(row.used_in_projects) : 0,
- usedInFeatures: row.used_in_projects ? Number(row.used_in_features) : 0,
+ ...(row.used_in_projects && {
+ usedInProjects: Number(row.used_in_projects),
+ }),
+ ...(row.used_in_features && {
+ usedInFeatures: Number(row.used_in_features),
+ }),
});
interface ICreateContextField {
diff --git a/src/lib/db/segment-store.ts b/src/lib/db/segment-store.ts
index aa2ae88e18..a44835cfd0 100644
--- a/src/lib/db/segment-store.ts
+++ b/src/lib/db/segment-store.ts
@@ -240,7 +240,7 @@ export default class SegmentStore implements ISegmentStore {
throw new NotFoundError('No row');
}
- const segment: ISegment = {
+ return {
id: row.id,
name: row.name,
description: row.description,
@@ -248,15 +248,13 @@ export default class SegmentStore implements ISegmentStore {
constraints: row.constraints,
createdBy: row.created_by,
createdAt: row.created_at,
- usedInProjects: row.used_in_projects
- ? Number(row.used_in_projects)
- : 0,
- usedInFeatures: row.used_in_projects
- ? Number(row.used_in_features)
- : 0,
+ ...(row.used_in_projects && {
+ usedInProjects: Number(row.used_in_projects),
+ }),
+ ...(row.used_in_features && {
+ usedInFeatures: Number(row.used_in_features),
+ }),
};
-
- return segment;
}
destroy(): void {}