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 React, { type VFC } from 'react';
|
||||||
import { FeatureEnvironmentSeen } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
import { FeatureEnvironmentSeen } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||||
import type { FeatureSearchEnvironmentSchema } from 'openapi';
|
import type { FeatureSearchEnvironmentSchema } from 'openapi';
|
||||||
|
import { FeatureLifecycle } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
interface IFeatureSeenCellProps {
|
interface IFeatureSeenCellProps {
|
||||||
feature: {
|
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(
|
export const MemoizedFeatureEnvironmentSeenCell = React.memo(
|
||||||
FeatureEnvironmentSeenCell,
|
FeatureEnvironmentSeenCell,
|
||||||
);
|
);
|
||||||
|
@ -2,17 +2,25 @@ import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon';
|
|||||||
import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip';
|
import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip';
|
||||||
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
||||||
import { populateCurrentStage } from './populateCurrentStage';
|
import { populateCurrentStage } from './populateCurrentStage';
|
||||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
|
||||||
import type { FC } from 'react';
|
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<{
|
export const FeatureLifecycle: FC<{
|
||||||
onArchive: () => void;
|
onArchive: () => void;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
onUncomplete: () => void;
|
onUncomplete: () => void;
|
||||||
feature: Pick<
|
feature: LifecycleFeature;
|
||||||
IFeatureToggle,
|
|
||||||
'lifecycle' | 'environments' | 'project' | 'name'
|
|
||||||
>;
|
|
||||||
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
||||||
const currentStage = populateCurrentStage(feature);
|
const currentStage = populateCurrentStage(feature);
|
||||||
|
|
||||||
|
@ -504,6 +504,6 @@ export const FeatureLifecycleTooltip: FC<{
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box>{children}</Box>
|
<CenteredBox>{children}</CenteredBox>
|
||||||
</HtmlTooltip>
|
</HtmlTooltip>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
|
||||||
import type { LifecycleStage } from './LifecycleStage';
|
import type { LifecycleStage } from './LifecycleStage';
|
||||||
|
import type { LifecycleFeature } from './FeatureLifecycle';
|
||||||
|
|
||||||
export const populateCurrentStage = (
|
export const populateCurrentStage = (
|
||||||
feature: Pick<IFeatureToggle, 'lifecycle' | 'environments'>,
|
feature: Pick<LifecycleFeature, 'lifecycle' | 'environments'>,
|
||||||
): LifecycleStage | undefined => {
|
): LifecycleStage | undefined => {
|
||||||
if (!feature.lifecycle) return undefined;
|
if (!feature.lifecycle) return undefined;
|
||||||
|
|
||||||
const getFilteredEnvironments = (condition: (type: string) => boolean) => {
|
const getFilteredEnvironments = (condition: (type: string) => boolean) => {
|
||||||
return feature.environments
|
return (feature.environments || [])
|
||||||
.filter((env) => condition(env.type) && Boolean(env.lastSeenAt))
|
.filter((env) => condition(env.type) && Boolean(env.lastSeenAt))
|
||||||
.map((env) => ({
|
.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
|
@ -13,7 +13,10 @@ import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi
|
|||||||
import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
||||||
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||||
import { ProjectFeaturesBatchActions } from '../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
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 { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
@ -48,6 +51,7 @@ import { TableEmptyState } from './TableEmptyState/TableEmptyState';
|
|||||||
import { useRowActions } from './hooks/useRowActions';
|
import { useRowActions } from './hooks/useRowActions';
|
||||||
import { useSelectedData } from './hooks/useSelectedData';
|
import { useSelectedData } from './hooks/useSelectedData';
|
||||||
import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
|
import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
interface IPaginatedProjectFeatureTogglesProps {
|
interface IPaginatedProjectFeatureTogglesProps {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
@ -126,6 +130,8 @@ export const ProjectFeatureToggles = ({
|
|||||||
|
|
||||||
const isPlaceholder = Boolean(initialLoad || (loading && total));
|
const isPlaceholder = Boolean(initialLoad || (loading && total));
|
||||||
|
|
||||||
|
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
@ -195,9 +201,25 @@ export const ProjectFeatureToggles = ({
|
|||||||
id: 'lastSeenAt',
|
id: 'lastSeenAt',
|
||||||
header: 'Last seen',
|
header: 'Last seen',
|
||||||
cell: ({ row: { original } }) => (
|
cell: ({ row: { original } }) => (
|
||||||
<MemoizedFeatureEnvironmentSeenCell
|
<ConditionallyRender
|
||||||
feature={original}
|
condition={featureLifecycleEnabled}
|
||||||
data-loading
|
show={
|
||||||
|
<FeatureLifecycleCell
|
||||||
|
feature={original}
|
||||||
|
onComplete={refetch}
|
||||||
|
onUncomplete={refetch}
|
||||||
|
onArchive={() =>
|
||||||
|
setFeatureArchiveState(original.name)
|
||||||
|
}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<MemoizedFeatureEnvironmentSeenCell
|
||||||
|
feature={original}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
size: 50,
|
size: 50,
|
||||||
@ -308,10 +330,12 @@ export const ProjectFeatureToggles = ({
|
|||||||
{
|
{
|
||||||
name: 'development',
|
name: 'development',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
type: 'development',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'production',
|
name: 'production',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
type: 'production',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
|
@ -32,6 +32,11 @@ export type ILastSeenEnvironments = Pick<
|
|||||||
'name' | 'enabled' | 'lastSeenAt' | 'yes' | 'no'
|
'name' | 'enabled' | 'lastSeenAt' | 'yes' | 'no'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type Lifecycle = {
|
||||||
|
stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
|
||||||
|
enteredStageAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IFeatureToggle {
|
export interface IFeatureToggle {
|
||||||
stale: boolean;
|
stale: boolean;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
@ -49,10 +54,7 @@ export interface IFeatureToggle {
|
|||||||
impressionData: boolean;
|
impressionData: boolean;
|
||||||
strategies?: IFeatureStrategy[];
|
strategies?: IFeatureStrategy[];
|
||||||
dependencies: Array<IDependency>;
|
dependencies: Array<IDependency>;
|
||||||
lifecycle?: {
|
lifecycle?: Lifecycle;
|
||||||
stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
|
|
||||||
enteredStageAt: string;
|
|
||||||
};
|
|
||||||
children: Array<string>;
|
children: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export interface FeatureSearchEnvironmentSchema {
|
|||||||
/** A list of activation strategies for the feature environment */
|
/** A list of activation strategies for the feature environment */
|
||||||
strategies?: FeatureStrategySchema[];
|
strategies?: FeatureStrategySchema[];
|
||||||
/** The type of the environment */
|
/** The type of the environment */
|
||||||
type?: string;
|
type: string;
|
||||||
/** The number of defined variants */
|
/** The number of defined variants */
|
||||||
variantCount?: number;
|
variantCount?: number;
|
||||||
/** A list of variants for the feature environment */
|
/** A list of variants for the feature environment */
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { FeatureSearchResponseSchemaDependencyType } from './featureSearchResponseSchemaDependencyType';
|
import type { FeatureSearchResponseSchemaDependencyType } from './featureSearchResponseSchemaDependencyType';
|
||||||
import type { FeatureSearchEnvironmentSchema } from './featureSearchEnvironmentSchema';
|
import type { FeatureSearchEnvironmentSchema } from './featureSearchEnvironmentSchema';
|
||||||
|
import type { FeatureSearchResponseSchemaLifecycle } from './featureSearchResponseSchemaLifecycle';
|
||||||
import type { FeatureSearchResponseSchemaStrategiesItem } from './featureSearchResponseSchemaStrategiesItem';
|
import type { FeatureSearchResponseSchemaStrategiesItem } from './featureSearchResponseSchemaStrategiesItem';
|
||||||
import type { TagSchema } from './tagSchema';
|
import type { TagSchema } from './tagSchema';
|
||||||
import type { VariantSchema } from './variantSchema';
|
import type { VariantSchema } from './variantSchema';
|
||||||
@ -47,6 +48,8 @@ export interface FeatureSearchResponseSchema {
|
|||||||
* @nullable
|
* @nullable
|
||||||
*/
|
*/
|
||||||
lastSeenAt?: string | null;
|
lastSeenAt?: string | null;
|
||||||
|
/** Current lifecycle stage of the feature */
|
||||||
|
lifecycle?: FeatureSearchResponseSchemaLifecycle;
|
||||||
/** Unique feature name */
|
/** Unique feature name */
|
||||||
name: string;
|
name: string;
|
||||||
/** Name of the project the feature belongs to */
|
/** 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 './featureSearchEnvironmentSchema';
|
||||||
export * from './featureSearchResponseSchema';
|
export * from './featureSearchResponseSchema';
|
||||||
export * from './featureSearchResponseSchemaDependencyType';
|
export * from './featureSearchResponseSchemaDependencyType';
|
||||||
|
export * from './featureSearchResponseSchemaLifecycle';
|
||||||
|
export * from './featureSearchResponseSchemaLifecycleStage';
|
||||||
export * from './featureSearchResponseSchemaStrategiesItem';
|
export * from './featureSearchResponseSchemaStrategiesItem';
|
||||||
export * from './featureStrategySchema';
|
export * from './featureStrategySchema';
|
||||||
export * from './featureStrategySegmentSchema';
|
export * from './featureStrategySegmentSchema';
|
||||||
|
@ -10,7 +10,7 @@ export const featureSearchEnvironmentSchema = {
|
|||||||
$id: '#/components/schemas/featureSearchEnvironmentSchema',
|
$id: '#/components/schemas/featureSearchEnvironmentSchema',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['name', 'enabled'],
|
required: ['name', 'enabled', 'type'],
|
||||||
description: 'A detailed description of the feature environment',
|
description: 'A detailed description of the feature environment',
|
||||||
properties: {
|
properties: {
|
||||||
...featureEnvironmentSchema.properties,
|
...featureEnvironmentSchema.properties,
|
||||||
|
Loading…
Reference in New Issue
Block a user