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 { ILegalValue } from 'interfaces/context';
|
||||||
import { ContextFormChip } from 'component/context/ContectFormChip/ContextFormChip';
|
import { ContextFormChip } from 'component/context/ContectFormChip/ContextFormChip';
|
||||||
import { ContextFormChipList } from 'component/context/ContectFormChip/ContextFormChipList';
|
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 {
|
interface IContextForm {
|
||||||
contextName: string;
|
contextName: string;
|
||||||
@ -101,6 +104,7 @@ export const ContextForm: React.FC<IContextForm> = ({
|
|||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [valueDesc, setValueDesc] = useState('');
|
const [valueDesc, setValueDesc] = useState('');
|
||||||
const [valueFocused, setValueFocused] = useState(false);
|
const [valueFocused, setValueFocused] = useState(false);
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
const isMissingValue = valueDesc.trim() && !value.trim();
|
const isMissingValue = valueDesc.trim() && !value.trim();
|
||||||
|
|
||||||
@ -263,6 +267,10 @@ export const ContextForm: React.FC<IContextForm> = ({
|
|||||||
/>
|
/>
|
||||||
<Typography>{stickiness ? 'On' : 'Off'}</Typography>
|
<Typography>{stickiness ? 'On' : 'Off'}</Typography>
|
||||||
</StyledSwitchContainer>
|
</StyledSwitchContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(uiConfig.flags.segmentContextFieldUsage)}
|
||||||
|
show={<ContextFieldUsage contextName={contextName} />}
|
||||||
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
{children}
|
{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,
|
sortOrder: row.sort_order,
|
||||||
legalValues: row.legal_values || [],
|
legalValues: row.legal_values || [],
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
usedInProjects: row.used_in_projects ? Number(row.used_in_projects) : 0,
|
...(row.used_in_projects && {
|
||||||
usedInFeatures: row.used_in_projects ? Number(row.used_in_features) : 0,
|
usedInProjects: Number(row.used_in_projects),
|
||||||
|
}),
|
||||||
|
...(row.used_in_features && {
|
||||||
|
usedInFeatures: Number(row.used_in_features),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ICreateContextField {
|
interface ICreateContextField {
|
||||||
|
@ -240,7 +240,7 @@ export default class SegmentStore implements ISegmentStore {
|
|||||||
throw new NotFoundError('No row');
|
throw new NotFoundError('No row');
|
||||||
}
|
}
|
||||||
|
|
||||||
const segment: ISegment = {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
@ -248,15 +248,13 @@ export default class SegmentStore implements ISegmentStore {
|
|||||||
constraints: row.constraints,
|
constraints: row.constraints,
|
||||||
createdBy: row.created_by,
|
createdBy: row.created_by,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
usedInProjects: row.used_in_projects
|
...(row.used_in_projects && {
|
||||||
? Number(row.used_in_projects)
|
usedInProjects: Number(row.used_in_projects),
|
||||||
: 0,
|
}),
|
||||||
usedInFeatures: row.used_in_projects
|
...(row.used_in_features && {
|
||||||
? Number(row.used_in_features)
|
usedInFeatures: Number(row.used_in_features),
|
||||||
: 0,
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return segment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {}
|
destroy(): void {}
|
||||||
|
Loading…
Reference in New Issue
Block a user