1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00

feat: display dependencies and parents in project details (#4859)

This commit is contained in:
Mateusz Kwasniewski 2023-09-28 13:37:52 +02:00 committed by GitHub
parent 4fd7035888
commit 72cca4f450
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 217 additions and 58 deletions

View File

@ -4,6 +4,7 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
interface IAddDependencyDialogueProps {
project: string;
@ -32,6 +33,7 @@ export const AddDependencyDialogue = ({
const { addDependency, removeDependencies } =
useDependentFeaturesApi(project);
const { parentOptions, loading } = useParentOptions(project, featureId);
const { refetchFeature } = useFeature(project, featureId);
const options = parentOptions
? [
REMOVE_DEPENDENCY_OPTION,
@ -50,6 +52,7 @@ export const AddDependencyDialogue = ({
} else {
await addDependency(featureId, { feature: parent });
}
await refetchFeature();
onClose();
}}
primaryButtonText={

View File

@ -0,0 +1,96 @@
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Button } from '@mui/material';
import { Add } from '@mui/icons-material';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import { useUiFlag } from 'hooks/useUiFlag';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { FC, useState } from 'react';
import { FlexRow, StyledDetail, StyledLabel, StyledLink } from './StyledRow';
export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
const canAddParentDependency =
Boolean(feature.project) &&
feature.dependencies.length === 0 &&
feature.children.length === 0;
const hasParentDependency =
Boolean(feature.project) && Boolean(feature.dependencies.length > 0);
const hasChildren = Boolean(feature.project) && feature.children.length > 0;
return (
<>
<ConditionallyRender
condition={canAddParentDependency}
show={
<FlexRow>
<StyledDetail>
<StyledLabel>Dependency:</StyledLabel>
<Button
startIcon={<Add />}
onClick={() => {
setShowDependencyDialogue(true);
}}
>
Add parent feature
</Button>
</StyledDetail>
</FlexRow>
}
/>
<ConditionallyRender
condition={hasParentDependency}
show={
<FlexRow>
<StyledDetail>
<StyledLabel>Dependency:</StyledLabel>
<StyledLink
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
>
{feature.dependencies[0]?.feature}
</StyledLink>
</StyledDetail>
</FlexRow>
}
/>
<ConditionallyRender
condition={hasChildren}
show={
<FlexRow>
<StyledDetail>
<StyledLabel>Children:</StyledLabel>
<TooltipLink
tooltip={
<>
{feature.children.map(child => (
<StyledLink
to={`/projects/${feature.project}/features/${child}`}
>
<div>{child}</div>
</StyledLink>
))}
</>
}
>
{feature.children.length === 1
? '1 feature'
: `${feature.children.length} features`}
</TooltipLink>
</StyledDetail>
</FlexRow>
}
/>
<ConditionallyRender
condition={Boolean(feature.project)}
show={
<AddDependencyDialogue
project={feature.project}
featureId={feature.name}
onClose={() => setShowDependencyDialogue(false)}
showDependencyDialogue={showDependencyDialogue}
/>
}
/>
</>
);
};

View File

@ -1,21 +1,34 @@
import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { IDependency, IFeatureToggle } from 'interfaces/featureToggle';
import { testServerRoute, testServerSetup } from 'utils/testServer';
const server = testServerSetup();
testServerRoute(server, '/api/admin/ui-config', {
flags: {
dependentFeatures: true,
},
const setupApi = () => {
testServerRoute(server, '/api/admin/ui-config', {
flags: {
dependentFeatures: true,
},
});
};
beforeEach(() => {
setupApi();
});
test('show dependency dialogue', async () => {
render(
<FeatureOverviewSidePanelDetails
feature={{ name: 'feature', project: 'default' } as IFeatureToggle}
feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: [] as string[],
} as IFeatureToggle
}
header={''}
/>
);
@ -28,3 +41,60 @@ test('show dependency dialogue', async () => {
screen.getByText('Add parent feature dependency')
).toBeInTheDocument();
});
test('show child', async () => {
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: ['some_child'],
} as IFeatureToggle
}
header={''}
/>
);
await screen.findByText('Children:');
await screen.findByText('1 feature');
});
test('show children', async () => {
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: ['some_child', 'some_other_child'],
} as IFeatureToggle
}
header={''}
/>
);
await screen.findByText('Children:');
await screen.findByText('2 features');
});
test('show parent dependencies', async () => {
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [{ feature: 'some_parent' }],
children: [] as string[],
} as IFeatureToggle
}
header={''}
/>
);
await screen.findByText('Dependency:');
await screen.findByText('some_parent');
});

View File

@ -1,15 +1,14 @@
import { IFeatureToggle } from 'interfaces/featureToggle';
import { Button, styled, Box } from '@mui/material';
import { styled } from '@mui/material';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMD } from 'utils/formatDate';
import { parseISO } from 'date-fns';
import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Add } from '@mui/icons-material';
import { DependencyRow } from './DependencyRow';
import { FlexRow, StyledDetail, StyledLabel } from './StyledRow';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import { useState } from 'react';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
@ -19,27 +18,10 @@ const StyledContainer = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
const StyledLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
marginRight: theme.spacing(1),
}));
interface IFeatureOverviewSidePanelDetailsProps {
feature: IFeatureToggle;
header: React.ReactNode;
}
const FlexRow = styled('div')({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
});
const StyledDetail = styled('div')(({ theme }) => ({
justifyContent: 'center',
paddingTop: theme.spacing(0.75),
}));
export const FeatureOverviewSidePanelDetails = ({
feature,
header,
@ -52,8 +34,6 @@ export const FeatureOverviewSidePanelDetails = ({
uiConfig.flags.lastSeenByEnvironment
);
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
return (
<StyledContainer>
{header}
@ -76,35 +56,8 @@ export const FeatureOverviewSidePanelDetails = ({
)}
</FlexRow>
<ConditionallyRender
condition={dependentFeatures && Boolean(feature.project)}
show={
<FlexRow>
<StyledDetail>
<StyledLabel>Dependency:</StyledLabel>
<Button
startIcon={<Add />}
onClick={() => {
setShowDependencyDialogue(true);
}}
>
Add parent feature
</Button>
</StyledDetail>
</FlexRow>
}
/>
<ConditionallyRender
condition={dependentFeatures && Boolean(feature.project)}
show={
<AddDependencyDialogue
project={feature.project}
featureId={feature.name}
onClose={() => setShowDependencyDialogue(false)}
showDependencyDialogue={
dependentFeatures && showDependencyDialogue
}
/>
}
condition={dependentFeatures}
show={<DependencyRow feature={feature} />}
/>
</StyledContainer>
);

View File

@ -0,0 +1,28 @@
import { styled } from '@mui/material';
import { textTruncated } from 'themes/themeStyles';
import { Link } from 'react-router-dom';
export const FlexRow = styled('div')({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
});
export const StyledDetail = styled('div')(({ theme }) => ({
justifyContent: 'center',
paddingTop: theme.spacing(0.75),
...textTruncated,
}));
export const StyledLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
marginRight: theme.spacing(1),
}));
export const StyledLink = styled(Link)(({ theme }) => ({
maxWidth: '100%',
textDecoration: 'none',
'&:hover, &:focus': {
textDecoration: 'underline',
},
}));

View File

@ -13,4 +13,6 @@ export const emptyFeature: IFeatureToggle = {
description: '',
favorite: false,
impressionData: false,
dependencies: [],
children: [],
};

View File

@ -35,6 +35,12 @@ export interface IFeatureToggle {
variants: IFeatureVariant[];
impressionData: boolean;
strategies?: IFeatureStrategy[];
dependencies: Array<IDependency>;
children: Array<string>;
}
export interface IDependency {
feature: string;
}
export interface IFeatureEnvironment {

View File

@ -44,6 +44,7 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
.where('features.project', result[0].project)
.andWhere('features.name', '!=', child)
.andWhere('dependent_features.child', null)
.andWhere('features.archived_at', null)
.select('features.name');
return rows.map((item) => item.name);