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:
parent
4fd7035888
commit
72cca4f450
@ -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={
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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');
|
||||||
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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',
|
||||||
|
},
|
||||||
|
}));
|
@ -13,4 +13,6 @@ export const emptyFeature: IFeatureToggle = {
|
|||||||
description: '',
|
description: '',
|
||||||
favorite: false,
|
favorite: false,
|
||||||
impressionData: false,
|
impressionData: false,
|
||||||
|
dependencies: [],
|
||||||
|
children: [],
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user