Skip to content

Commit

Permalink
feat: context field usage frontend (#3938)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaanus committed Jun 12, 2023
1 parent 4599e5c commit 9f0d942
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 11 deletions.
@@ -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(
<UIProviderContainer>
<ContextFieldUsage contextName={contextFieldName} />
</UIProviderContainer>
);

await screen.findByText('Usage of this context field:');
await screen.findByText('tests (Gradual rollout)');
});
@@ -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<string>(
strategies.map(({ projectId }) => projectId!).filter(Boolean)
)
);

const projectList = (
<StyledUl>
{projectsUsed.map(projectId => (
<li key={projectId}>
<Link
to={`/projects/${projectId}`}
target="_blank"
rel="noreferrer"
>
{projects.find(({ id }) => id === projectId)?.name ??
projectId}
</Link>
<ul>
{strategies
?.filter(
strategy => strategy.projectId === projectId
)
.map(strategy => (
<li key={strategy.id}>
<Link
to={formatEditStrategyPath(
strategy.projectId!,
strategy.featureName!,
strategy.environment!,
strategy.id
)}
target="_blank"
rel="noreferrer"
>
{strategy.featureName!}{' '}
{formatStrategyNameParens(strategy)}
</Link>
</li>
))}
</ul>
</li>
))}
</StyledUl>
);
if (projectsUsed.length > 0) {
return (
<StyledAlert severity="info">
Usage of this context field:
{projectList}
</StyledAlert>
);
}

return null;
};

const formatStrategyNameParens = (strategy: IFeatureStrategy): string => {
if (!strategy.strategyName) {
return '';
}

return `(${formatStrategyName(strategy.strategyName)})`;
};
8 changes: 8 additions & 0 deletions frontend/src/component/context/ContextForm/ContextForm.tsx
Expand Up @@ -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;
Expand Down Expand Up @@ -101,6 +104,7 @@ export const ContextForm: React.FC<IContextForm> = ({
const [value, setValue] = useState('');
const [valueDesc, setValueDesc] = useState('');
const [valueFocused, setValueFocused] = useState(false);
const { uiConfig } = useUiConfig();

const isMissingValue = valueDesc.trim() && !value.trim();

Expand Down Expand Up @@ -263,6 +267,10 @@ export const ContextForm: React.FC<IContextForm> = ({
/>
<Typography>{stickiness ? 'On' : 'Off'}</Typography>
</StyledSwitchContainer>
<ConditionallyRender
condition={Boolean(uiConfig.flags.segmentContextFieldUsage)}
show={<ContextFieldUsage contextName={contextName} />}
/>
</StyledContainer>
<StyledButtonContainer>
{children}
Expand Down
@@ -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());
};
8 changes: 6 additions & 2 deletions src/lib/db/context-field-store.ts
Expand Up @@ -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 {
Expand Down
16 changes: 7 additions & 9 deletions src/lib/db/segment-store.ts
Expand Up @@ -240,23 +240,21 @@ export default class SegmentStore implements ISegmentStore {
throw new NotFoundError('No row');
}

const segment: ISegment = {
return {
id: row.id,
name: row.name,
description: row.description,
project: row.segment_project_id || undefined,
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 {}
Expand Down

0 comments on commit 9f0d942

Please sign in to comment.