1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +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 GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi'; import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions'; import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
interface IAddDependencyDialogueProps { interface IAddDependencyDialogueProps {
project: string; project: string;
@ -32,6 +33,7 @@ export const AddDependencyDialogue = ({
const { addDependency, removeDependencies } = const { addDependency, removeDependencies } =
useDependentFeaturesApi(project); useDependentFeaturesApi(project);
const { parentOptions, loading } = useParentOptions(project, featureId); const { parentOptions, loading } = useParentOptions(project, featureId);
const { refetchFeature } = useFeature(project, featureId);
const options = parentOptions const options = parentOptions
? [ ? [
REMOVE_DEPENDENCY_OPTION, REMOVE_DEPENDENCY_OPTION,
@ -50,6 +52,7 @@ export const AddDependencyDialogue = ({
} else { } else {
await addDependency(featureId, { feature: parent }); await addDependency(featureId, { feature: parent });
} }
await refetchFeature();
onClose(); onClose();
}} }}
primaryButtonText={ 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 { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails'; import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails';
import { IFeatureToggle } from 'interfaces/featureToggle'; import { IDependency, IFeatureToggle } from 'interfaces/featureToggle';
import { testServerRoute, testServerSetup } from 'utils/testServer'; import { testServerRoute, testServerSetup } from 'utils/testServer';
const server = testServerSetup(); const server = testServerSetup();
testServerRoute(server, '/api/admin/ui-config', { const setupApi = () => {
flags: { testServerRoute(server, '/api/admin/ui-config', {
dependentFeatures: true, flags: {
}, dependentFeatures: true,
},
});
};
beforeEach(() => {
setupApi();
}); });
test('show dependency dialogue', async () => { test('show dependency dialogue', async () => {
render( render(
<FeatureOverviewSidePanelDetails <FeatureOverviewSidePanelDetails
feature={{ name: 'feature', project: 'default' } as IFeatureToggle} feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: [] as string[],
} as IFeatureToggle
}
header={''} header={''}
/> />
); );
@ -28,3 +41,60 @@ test('show dependency dialogue', async () => {
screen.getByText('Add parent feature dependency') screen.getByText('Add parent feature dependency')
).toBeInTheDocument(); ).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 { IFeatureToggle } from 'interfaces/featureToggle';
import { Button, styled, Box } from '@mui/material'; import { styled } from '@mui/material';
import { useLocationSettings } from 'hooks/useLocationSettings'; import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMD } from 'utils/formatDate'; import { formatDateYMD } from 'utils/formatDate';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureEnvironmentSeen'; import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; 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 { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import { useState } from 'react';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -19,27 +18,10 @@ const StyledContainer = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
const StyledLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
marginRight: theme.spacing(1),
}));
interface IFeatureOverviewSidePanelDetailsProps { interface IFeatureOverviewSidePanelDetailsProps {
feature: IFeatureToggle; feature: IFeatureToggle;
header: React.ReactNode; 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 = ({ export const FeatureOverviewSidePanelDetails = ({
feature, feature,
header, header,
@ -52,8 +34,6 @@ export const FeatureOverviewSidePanelDetails = ({
uiConfig.flags.lastSeenByEnvironment uiConfig.flags.lastSeenByEnvironment
); );
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
return ( return (
<StyledContainer> <StyledContainer>
{header} {header}
@ -76,35 +56,8 @@ export const FeatureOverviewSidePanelDetails = ({
)} )}
</FlexRow> </FlexRow>
<ConditionallyRender <ConditionallyRender
condition={dependentFeatures && Boolean(feature.project)} condition={dependentFeatures}
show={ show={<DependencyRow feature={feature} />}
<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
}
/>
}
/> />
</StyledContainer> </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: '', description: '',
favorite: false, favorite: false,
impressionData: false, impressionData: false,
dependencies: [],
children: [],
}; };

View File

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

View File

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