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

feat: merge feature toggle details with feature meta info box (#6977)

![image](https://github.com/Unleash/unleash/assets/964450/5286eb36-311d-42f9-90da-832724cc41d0)

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
Co-authored-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
Jaanus Sellin 2024-05-06 12:17:54 +03:00 committed by GitHub
parent 2c05f1a0ce
commit 233b882c7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 200 additions and 189 deletions

View File

@ -1,4 +1,4 @@
import { StyledLink } from './StyledRow';
import { StyledLink } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import type { FC } from 'react';

View File

@ -2,7 +2,12 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import type { IFeatureToggle } from 'interfaces/featureToggle';
import { type FC, useState } from 'react';
import { FlexRow, StyledDetail, StyledLabel, StyledLink } from './StyledRow';
import {
FlexRow,
StyledDetail,
StyledLabel,
StyledLink,
} from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
import { DependencyActions } from './DependencyActions';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';

View File

@ -1,10 +1,11 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'utils/testRenderer';
import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails';
import type { IDependency, IFeatureToggle } from 'interfaces/featureToggle';
import FeatureOverviewMetaData from './FeatureOverviewMetaData';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { Route, Routes } from 'react-router-dom';
import type { IDependency, IFeatureToggle } from 'interfaces/featureToggle';
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
import userEvent from '@testing-library/user-event';
const server = testServerSetup();
@ -14,7 +15,6 @@ const setupApi = () => {
current: { oss: 'irrelevant', enterprise: 'some value' },
},
});
testServerRoute(server, '/api/admin/projects/default/features/feature', {});
testServerRoute(
server,
'/api/admin/projects/default/features/feature/parents',
@ -54,36 +54,43 @@ const setupChangeRequestApi = () => {
);
testServerRoute(
server,
'api/admin/projects/default/change-requests/pending',
'/api/admin/projects/default/change-requests/pending',
[],
);
testServerRoute(
server,
'api/admin/projects/default/environments/development/change-requests',
'/api/admin/projects/default/environments/development/change-requests',
{},
'post',
200,
);
};
const setupFeatureApi = (feature: IFeatureToggle) => {
testServerRoute(
server,
'/api/admin/projects/default/features/feature',
feature,
);
};
beforeEach(() => {
setupApi();
});
const route = '/projects/default/features/feature';
test('show dependency dialogue', async () => {
setupFeatureApi(feature);
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: [] as string[],
} as IFeatureToggle
}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{
route,
permissions: [
{ permission: 'UPDATE_FEATURE_DEPENDENCY', project: 'default' },
],
@ -101,19 +108,21 @@ test('show dependency dialogue', async () => {
test('show dependency dialogue for OSS with dependencies', async () => {
setupOssWithExistingDependencies();
setupFeatureApi({
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: [] as string[],
} as IFeatureToggle);
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: [] as string[],
} as IFeatureToggle
}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{
route,
permissions: [
{ permission: 'UPDATE_FEATURE_DEPENDENCY', project: 'default' },
],
@ -130,18 +139,20 @@ test('show dependency dialogue for OSS with dependencies', async () => {
});
test('show child', async () => {
setupFeatureApi({
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: ['some_child'],
} as IFeatureToggle);
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: ['some_child'],
} as IFeatureToggle
}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{ route },
);
await screen.findByText('Children:');
@ -149,18 +160,20 @@ test('show child', async () => {
});
test('show children', async () => {
setupFeatureApi({
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: ['some_child', 'some_other_child'],
} as IFeatureToggle);
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [] as Array<{ feature: string }>,
children: ['some_child', 'some_other_child'],
} as IFeatureToggle
}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{ route },
);
await screen.findByText('Children:');
@ -175,18 +188,22 @@ const feature = {
} as IFeatureToggle;
test('delete dependency', async () => {
setupFeatureApi({
...feature,
dependencies: [{ feature: 'some_parent' }],
});
render(
<>
<ToastRenderer />
<FeatureOverviewSidePanelDetails
feature={{
...feature,
dependencies: [{ feature: 'some_parent' }],
}}
header={''}
/>
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>
</>,
{
route,
permissions: [
{ permission: 'UPDATE_FEATURE_DEPENDENCY', project: 'default' },
],
@ -209,18 +226,22 @@ test('delete dependency', async () => {
test('delete dependency with change request', async () => {
setupChangeRequestApi();
setupFeatureApi({
...feature,
dependencies: [{ feature: 'some_parent' }],
});
render(
<>
<ToastRenderer />
<FeatureOverviewSidePanelDetails
feature={{
...feature,
dependencies: [{ feature: 'some_parent' }],
}}
header={''}
/>
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>
</>,
{
route,
permissions: [
/* deliberately no permissions */
],
@ -242,15 +263,19 @@ test('delete dependency with change request', async () => {
});
test('edit dependency', async () => {
setupFeatureApi({
...feature,
dependencies: [{ feature: 'some_parent', enabled: false }],
});
render(
<FeatureOverviewSidePanelDetails
feature={{
...feature,
dependencies: [{ feature: 'some_parent', enabled: false }],
}}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{
route,
permissions: [
{ permission: 'UPDATE_FEATURE_DEPENDENCY', project: 'default' },
],
@ -274,20 +299,24 @@ test('edit dependency', async () => {
});
test('show variant dependencies', async () => {
setupFeatureApi({
...feature,
dependencies: [
{
feature: 'some_parent',
enabled: true,
variants: ['variantA', 'variantB'],
},
],
});
render(
<FeatureOverviewSidePanelDetails
feature={{
...feature,
dependencies: [
{
feature: 'some_parent',
enabled: true,
variants: ['variantA', 'variantB'],
},
],
}}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{ route },
);
const variants = await screen.findByText('2 variants');
@ -299,39 +328,47 @@ test('show variant dependencies', async () => {
});
test('show variant dependency', async () => {
setupFeatureApi({
...feature,
dependencies: [
{
feature: 'some_parent',
enabled: true,
variants: ['variantA'],
},
],
});
render(
<FeatureOverviewSidePanelDetails
feature={{
...feature,
dependencies: [
{
feature: 'some_parent',
enabled: true,
variants: ['variantA'],
},
],
}}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{ route },
);
await screen.findByText('variantA');
});
test('show disabled dependency', async () => {
setupFeatureApi({
...feature,
dependencies: [
{
feature: 'some_parent',
enabled: false,
},
],
});
render(
<FeatureOverviewSidePanelDetails
feature={{
...feature,
dependencies: [
{
feature: 'some_parent',
enabled: false,
},
],
}}
header={''}
/>,
<Routes>
<Route
path={'/projects/:projectId/features/:featureId'}
element={<FeatureOverviewMetaData />}
/>
</Routes>,
{ route },
);
await screen.findByText('disabled');

View File

@ -15,6 +15,14 @@ import { useState } from 'react';
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
import { populateCurrentStage } from '../FeatureLifecycle/populateCurrentStage';
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
import { StyledDetail } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
import { formatDateYMD } from 'utils/formatDate';
import { parseISO } from 'date-fns';
import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import { DependencyRow } from './DependencyRow';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { useShowDependentFeatures } from './useShowDependentFeatures';
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
const StyledContainer = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
@ -68,6 +76,12 @@ const StyledDescriptionContainer = styled('div')(({ theme }) => ({
alignItems: 'center',
}));
const StyledDetailsContainer = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}));
const StyledDescription = styled('p')({
wordBreak: 'break-word',
});
@ -87,6 +101,15 @@ const FeatureOverviewMetaData = () => {
useFeatureLifecycleApi();
const navigate = useNavigate();
const [showDelDialog, setShowDelDialog] = useState(false);
const { locationSettings } = useLocationSettings();
const showDependentFeatures = useShowDependentFeatures(feature.project);
const lastSeenEnvironments: ILastSeenEnvironments[] =
feature.environments?.map((env) => ({
name: env.name,
lastSeenAt: env.lastSeenAt,
enabled: env.enabled,
}));
const IconComponent = getFeatureTypeIcons(type);
@ -189,6 +212,29 @@ const FeatureOverviewMetaData = () => {
</span>
}
/>
<StyledBodyItem>
<StyledDetailsContainer>
<StyledDetail>
<StyledLabel>Created at:</StyledLabel>
<span>
{formatDateYMD(
parseISO(feature.createdAt),
locationSettings.locale,
)}
</span>
</StyledDetail>
<FeatureEnvironmentSeen
featureLastSeen={feature.lastSeenAt}
environments={lastSeenEnvironments}
sx={{ p: 0 }}
/>
</StyledDetailsContainer>
</StyledBodyItem>
<ConditionallyRender
condition={showDependentFeatures}
show={<DependencyRow feature={feature} />}
/>
</StyledBody>
</StyledPaddingContainerTop>
<ConditionallyRender

View File

@ -2,7 +2,6 @@ import { Box, Divider, styled } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails';
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
import { Sticky } from 'component/common/Sticky/Sticky';
@ -77,15 +76,6 @@ export const FeatureOverviewSidePanel = ({
setHiddenEnvironments={setHiddenEnvironments}
/>
<Divider />
<FeatureOverviewSidePanelDetails
header={
<StyledHeader data-loading>
Feature toggle details
</StyledHeader>
}
feature={feature}
/>
<Divider />
<FeatureOverviewSidePanelTags
header={
<StyledHeader data-loading>

View File

@ -1,67 +0,0 @@
import type {
IFeatureToggle,
ILastSeenEnvironments,
} from 'interfaces/featureToggle';
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 { DependencyRow } from './DependencyRow';
import { FlexRow, StyledDetail, StyledLabel } from './StyledRow';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useShowDependentFeatures } from './useShowDependentFeatures';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
justifyItems: 'center',
padding: theme.spacing(3),
fontSize: theme.fontSizes.smallBody,
}));
interface IFeatureOverviewSidePanelDetailsProps {
feature: IFeatureToggle;
header: React.ReactNode;
}
export const FeatureOverviewSidePanelDetails = ({
feature,
header,
}: IFeatureOverviewSidePanelDetailsProps) => {
const { locationSettings } = useLocationSettings();
const showDependentFeatures = useShowDependentFeatures(feature.project);
const lastSeenEnvironments: ILastSeenEnvironments[] =
feature.environments?.map((env) => ({
name: env.name,
lastSeenAt: env.lastSeenAt,
enabled: env.enabled,
}));
return (
<StyledContainer>
{header}
<FlexRow>
<StyledDetail>
<StyledLabel>Created at:</StyledLabel>
<span>
{formatDateYMD(
parseISO(feature.createdAt),
locationSettings.locale,
)}
</span>
</StyledDetail>
<FeatureEnvironmentSeen
featureLastSeen={feature.lastSeenAt}
environments={lastSeenEnvironments}
sx={{ p: 0 }}
/>
</FlexRow>
<ConditionallyRender
condition={showDependentFeatures}
show={<DependencyRow feature={feature} />}
/>
</StyledContainer>
);
};

View File

@ -45,7 +45,7 @@ import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
import { ReactComponent as ChildLinkIcon } from 'assets/icons/link-child.svg';
import { ReactComponent as ParentLinkIcon } from 'assets/icons/link-parent.svg';
import { ChildrenTooltip } from './FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/ChildrenTooltip';
import { ChildrenTooltip } from './FeatureOverview/FeatureOverviewMetaData/ChildrenTooltip';
import copy from 'copy-to-clipboard';
import useToast from 'hooks/useToast';