From 72cca4f450d10fa08998ecf3274e7cad98565562 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 28 Sep 2023 13:37:52 +0200 Subject: [PATCH] feat: display dependencies and parents in project details (#4859) --- .../Dependencies/AddDependencyDialogue.tsx | 3 + .../DependencyRow.tsx | 96 +++++++++++++++++++ .../FeatureOverviewSidePanelDetails.test.tsx | 82 ++++++++++++++-- .../FeatureOverviewSidePanelDetails.tsx | 57 +---------- .../StyledRow.tsx | 28 ++++++ .../api/getters/useFeature/emptyFeature.ts | 2 + frontend/src/interfaces/featureToggle.ts | 6 ++ .../dependent-features-read-model.ts | 1 + 8 files changed, 217 insertions(+), 58 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow.tsx diff --git a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx index 1f4dbafc26..a9f85f072d 100644 --- a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx +++ b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx @@ -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={ diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx new file mode 100644 index 0000000000..1b3fcfd8c4 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx @@ -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 ( + <> + + + Dependency: + + + + } + /> + + + Dependency: + + {feature.dependencies[0]?.feature} + + + + } + /> + + + Children: + + {feature.children.map(child => ( + +
{child}
+
+ ))} + + } + > + {feature.children.length === 1 + ? '1 feature' + : `${feature.children.length} features`} +
+
+ + } + /> + setShowDependencyDialogue(false)} + showDependencyDialogue={showDependencyDialogue} + /> + } + /> + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx index 8d34dd8a0a..769d5491cf 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx @@ -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( , + 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( + , + children: ['some_child'], + } as IFeatureToggle + } + header={''} + /> + ); + + await screen.findByText('Children:'); + await screen.findByText('1 feature'); +}); + +test('show children', async () => { + render( + , + children: ['some_child', 'some_other_child'], + } as IFeatureToggle + } + header={''} + /> + ); + + await screen.findByText('Children:'); + await screen.findByText('2 features'); +}); + +test('show parent dependencies', async () => { + render( + + ); + + await screen.findByText('Dependency:'); + await screen.findByText('some_parent'); +}); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx index 37abc089bc..2cbfd2c6e0 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx @@ -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 ( {header} @@ -76,35 +56,8 @@ export const FeatureOverviewSidePanelDetails = ({ )} - - Dependency: - - - - } - /> - setShowDependencyDialogue(false)} - showDependencyDialogue={ - dependentFeatures && showDependencyDialogue - } - /> - } + condition={dependentFeatures} + show={} /> ); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow.tsx new file mode 100644 index 0000000000..7c89da7138 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow.tsx @@ -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', + }, +})); diff --git a/frontend/src/hooks/api/getters/useFeature/emptyFeature.ts b/frontend/src/hooks/api/getters/useFeature/emptyFeature.ts index 3530baff4b..b0286c3c7e 100644 --- a/frontend/src/hooks/api/getters/useFeature/emptyFeature.ts +++ b/frontend/src/hooks/api/getters/useFeature/emptyFeature.ts @@ -13,4 +13,6 @@ export const emptyFeature: IFeatureToggle = { description: '', favorite: false, impressionData: false, + dependencies: [], + children: [], }; diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index f3fd6b2f01..e9102e3dd1 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -35,6 +35,12 @@ export interface IFeatureToggle { variants: IFeatureVariant[]; impressionData: boolean; strategies?: IFeatureStrategy[]; + dependencies: Array; + children: Array; +} + +export interface IDependency { + feature: string; } export interface IFeatureEnvironment { diff --git a/src/lib/features/dependent-features/dependent-features-read-model.ts b/src/lib/features/dependent-features/dependent-features-read-model.ts index a95fd3a20b..69dfab1e83 100644 --- a/src/lib/features/dependent-features/dependent-features-read-model.ts +++ b/src/lib/features/dependent-features/dependent-features-read-model.ts @@ -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);