mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: context field usage frontend (#3938)
This commit is contained in:
parent
4599e5cc06
commit
9f0d94287e
@ -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)})`;
|
||||
};
|
@ -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<IContextForm> = ({
|
||||
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<IContextForm> = ({
|
||||
/>
|
||||
<Typography>{stickiness ? 'On' : 'Off'}</Typography>
|
||||
</StyledSwitchContainer>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(uiConfig.flags.segmentContextFieldUsage)}
|
||||
show={<ContextFieldUsage contextName={contextName} />}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<StyledButtonContainer>
|
||||
{children}
|
||||
|
@ -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());
|
||||
};
|
@ -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 {
|
||||
|
@ -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 {}
|
||||
|
Loading…
Reference in New Issue
Block a user