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)
 --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai> Co-authored-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
2c05f1a0ce
commit
233b882c7b
@ -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';
|
||||
|
@ -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';
|
@ -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');
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user