mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
feat: Lifecycle in project overview (#7024)
This commit is contained in:
parent
476959df8e
commit
3fc7714e78
@ -1,6 +1,8 @@
|
||||
import React, { type VFC } from 'react';
|
||||
import { FeatureEnvironmentSeen } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||
import type { FeatureSearchEnvironmentSchema } from 'openapi';
|
||||
import { FeatureLifecycle } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
interface IFeatureSeenCellProps {
|
||||
feature: {
|
||||
@ -26,6 +28,46 @@ export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface IFeatureLifecycleProps {
|
||||
feature: {
|
||||
environments?: FeatureSearchEnvironmentSchema[];
|
||||
lastSeenAt?: string | null;
|
||||
project: string;
|
||||
name: string;
|
||||
};
|
||||
onComplete: () => void;
|
||||
onUncomplete: () => void;
|
||||
onArchive: () => void;
|
||||
}
|
||||
|
||||
export const FeatureLifecycleCell: VFC<IFeatureLifecycleProps> = ({
|
||||
feature,
|
||||
onComplete,
|
||||
onUncomplete,
|
||||
onArchive,
|
||||
...rest
|
||||
}) => {
|
||||
const environments = feature.environments
|
||||
? Object.values(feature.environments)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<FeatureEnvironmentSeen
|
||||
featureLastSeen={feature.lastSeenAt || undefined}
|
||||
environments={environments}
|
||||
{...rest}
|
||||
/>
|
||||
<FeatureLifecycle
|
||||
onArchive={onArchive}
|
||||
onComplete={onComplete}
|
||||
onUncomplete={onUncomplete}
|
||||
feature={feature}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const MemoizedFeatureEnvironmentSeenCell = React.memo(
|
||||
FeatureEnvironmentSeenCell,
|
||||
);
|
||||
|
@ -2,17 +2,25 @@ import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon';
|
||||
import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip';
|
||||
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
||||
import { populateCurrentStage } from './populateCurrentStage';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import type { FC } from 'react';
|
||||
import type { Lifecycle } from 'interfaces/featureToggle';
|
||||
|
||||
export interface LifecycleFeature {
|
||||
lifecycle?: Lifecycle;
|
||||
project: string;
|
||||
name: string;
|
||||
environments?: Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
lastSeenAt?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const FeatureLifecycle: FC<{
|
||||
onArchive: () => void;
|
||||
onComplete: () => void;
|
||||
onUncomplete: () => void;
|
||||
feature: Pick<
|
||||
IFeatureToggle,
|
||||
'lifecycle' | 'environments' | 'project' | 'name'
|
||||
>;
|
||||
feature: LifecycleFeature;
|
||||
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
||||
const currentStage = populateCurrentStage(feature);
|
||||
|
||||
|
@ -504,6 +504,6 @@ export const FeatureLifecycleTooltip: FC<{
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box>{children}</Box>
|
||||
<CenteredBox>{children}</CenteredBox>
|
||||
</HtmlTooltip>
|
||||
);
|
||||
|
@ -1,13 +1,13 @@
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import type { LifecycleStage } from './LifecycleStage';
|
||||
import type { LifecycleFeature } from './FeatureLifecycle';
|
||||
|
||||
export const populateCurrentStage = (
|
||||
feature: Pick<IFeatureToggle, 'lifecycle' | 'environments'>,
|
||||
feature: Pick<LifecycleFeature, 'lifecycle' | 'environments'>,
|
||||
): LifecycleStage | undefined => {
|
||||
if (!feature.lifecycle) return undefined;
|
||||
|
||||
const getFilteredEnvironments = (condition: (type: string) => boolean) => {
|
||||
return feature.environments
|
||||
return (feature.environments || [])
|
||||
.filter((env) => condition(env.type) && Boolean(env.lastSeenAt))
|
||||
.map((env) => ({
|
||||
name: env.name,
|
||||
|
@ -13,7 +13,10 @@ import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi
|
||||
import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
||||
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||
import { ProjectFeaturesBatchActions } from '../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||
import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||
import {
|
||||
FeatureLifecycleCell,
|
||||
MemoizedFeatureEnvironmentSeenCell,
|
||||
} from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||
import useLoading from 'hooks/useLoading';
|
||||
@ -48,6 +51,7 @@ import { TableEmptyState } from './TableEmptyState/TableEmptyState';
|
||||
import { useRowActions } from './hooks/useRowActions';
|
||||
import { useSelectedData } from './hooks/useSelectedData';
|
||||
import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
interface IPaginatedProjectFeatureTogglesProps {
|
||||
environments: string[];
|
||||
@ -126,6 +130,8 @@ export const ProjectFeatureToggles = ({
|
||||
|
||||
const isPlaceholder = Boolean(initialLoad || (loading && total));
|
||||
|
||||
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
@ -195,10 +201,26 @@ export const ProjectFeatureToggles = ({
|
||||
id: 'lastSeenAt',
|
||||
header: 'Last seen',
|
||||
cell: ({ row: { original } }) => (
|
||||
<ConditionallyRender
|
||||
condition={featureLifecycleEnabled}
|
||||
show={
|
||||
<FeatureLifecycleCell
|
||||
feature={original}
|
||||
onComplete={refetch}
|
||||
onUncomplete={refetch}
|
||||
onArchive={() =>
|
||||
setFeatureArchiveState(original.name)
|
||||
}
|
||||
data-loading
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<MemoizedFeatureEnvironmentSeenCell
|
||||
feature={original}
|
||||
data-loading
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
size: 50,
|
||||
meta: {
|
||||
@ -308,10 +330,12 @@ export const ProjectFeatureToggles = ({
|
||||
{
|
||||
name: 'development',
|
||||
enabled: false,
|
||||
type: 'development',
|
||||
},
|
||||
{
|
||||
name: 'production',
|
||||
enabled: false,
|
||||
type: 'production',
|
||||
},
|
||||
],
|
||||
})),
|
||||
|
@ -32,6 +32,11 @@ export type ILastSeenEnvironments = Pick<
|
||||
'name' | 'enabled' | 'lastSeenAt' | 'yes' | 'no'
|
||||
>;
|
||||
|
||||
export type Lifecycle = {
|
||||
stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
|
||||
enteredStageAt: string;
|
||||
};
|
||||
|
||||
export interface IFeatureToggle {
|
||||
stale: boolean;
|
||||
archived: boolean;
|
||||
@ -49,10 +54,7 @@ export interface IFeatureToggle {
|
||||
impressionData: boolean;
|
||||
strategies?: IFeatureStrategy[];
|
||||
dependencies: Array<IDependency>;
|
||||
lifecycle?: {
|
||||
stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
|
||||
enteredStageAt: string;
|
||||
};
|
||||
lifecycle?: Lifecycle;
|
||||
children: Array<string>;
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ export interface FeatureSearchEnvironmentSchema {
|
||||
/** A list of activation strategies for the feature environment */
|
||||
strategies?: FeatureStrategySchema[];
|
||||
/** The type of the environment */
|
||||
type?: string;
|
||||
type: string;
|
||||
/** The number of defined variants */
|
||||
variantCount?: number;
|
||||
/** A list of variants for the feature environment */
|
||||
|
@ -5,6 +5,7 @@
|
||||
*/
|
||||
import type { FeatureSearchResponseSchemaDependencyType } from './featureSearchResponseSchemaDependencyType';
|
||||
import type { FeatureSearchEnvironmentSchema } from './featureSearchEnvironmentSchema';
|
||||
import type { FeatureSearchResponseSchemaLifecycle } from './featureSearchResponseSchemaLifecycle';
|
||||
import type { FeatureSearchResponseSchemaStrategiesItem } from './featureSearchResponseSchemaStrategiesItem';
|
||||
import type { TagSchema } from './tagSchema';
|
||||
import type { VariantSchema } from './variantSchema';
|
||||
@ -47,6 +48,8 @@ export interface FeatureSearchResponseSchema {
|
||||
* @nullable
|
||||
*/
|
||||
lastSeenAt?: string | null;
|
||||
/** Current lifecycle stage of the feature */
|
||||
lifecycle?: FeatureSearchResponseSchemaLifecycle;
|
||||
/** Unique feature name */
|
||||
name: string;
|
||||
/** Name of the project the feature belongs to */
|
||||
|
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
import type { FeatureSearchResponseSchemaLifecycleStage } from './featureSearchResponseSchemaLifecycleStage';
|
||||
|
||||
/**
|
||||
* Current lifecycle stage of the feature
|
||||
*/
|
||||
export type FeatureSearchResponseSchemaLifecycle = {
|
||||
/** When the feature entered this stage */
|
||||
enteredStageAt: string;
|
||||
/** The name of the current lifecycle stage */
|
||||
stage: FeatureSearchResponseSchemaLifecycleStage;
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* The name of the current lifecycle stage
|
||||
*/
|
||||
export type FeatureSearchResponseSchemaLifecycleStage =
|
||||
(typeof FeatureSearchResponseSchemaLifecycleStage)[keyof typeof FeatureSearchResponseSchemaLifecycleStage];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const FeatureSearchResponseSchemaLifecycleStage = {
|
||||
initial: 'initial',
|
||||
'pre-live': 'pre-live',
|
||||
live: 'live',
|
||||
completed: 'completed',
|
||||
archived: 'archived',
|
||||
} as const;
|
@ -554,6 +554,8 @@ export * from './featureSchemaStrategiesItem';
|
||||
export * from './featureSearchEnvironmentSchema';
|
||||
export * from './featureSearchResponseSchema';
|
||||
export * from './featureSearchResponseSchemaDependencyType';
|
||||
export * from './featureSearchResponseSchemaLifecycle';
|
||||
export * from './featureSearchResponseSchemaLifecycleStage';
|
||||
export * from './featureSearchResponseSchemaStrategiesItem';
|
||||
export * from './featureStrategySchema';
|
||||
export * from './featureStrategySegmentSchema';
|
||||
|
@ -10,7 +10,7 @@ export const featureSearchEnvironmentSchema = {
|
||||
$id: '#/components/schemas/featureSearchEnvironmentSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'enabled'],
|
||||
required: ['name', 'enabled', 'type'],
|
||||
description: 'A detailed description of the feature environment',
|
||||
properties: {
|
||||
...featureEnvironmentSchema.properties,
|
||||
|
Loading…
Reference in New Issue
Block a user