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