1
0
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:
Jaanus Sellin 2023-06-12 10:55:58 +03:00 committed by GitHub
parent 4599e5cc06
commit 9f0d94287e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 11 deletions

View File

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

View File

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

View File

@ -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}

View File

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

View File

@ -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 {

View File

@ -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 {}