diff --git a/frontend/common/lifecycleEnvironmentSlice.ts b/frontend/common/lifecycleEnvironmentSlice.ts new file mode 100644 index 000000000000..0df194402092 --- /dev/null +++ b/frontend/common/lifecycleEnvironmentSlice.ts @@ -0,0 +1,26 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +type LifecycleEnvironmentState = { + // Maps a project id to the environment id selected for lifecycle analysis. + byProject: Record +} + +const initialState: LifecycleEnvironmentState = { + byProject: {}, +} + +const lifecycleEnvironmentSlice = createSlice({ + initialState, + name: 'lifecycleEnvironment', + reducers: { + setLifecycleEnvironment( + state, + action: PayloadAction<{ projectId: number; environmentId: number }>, + ) { + state.byProject[action.payload.projectId] = action.payload.environmentId + }, + }, +}) + +export const { setLifecycleEnvironment } = lifecycleEnvironmentSlice.actions +export default lifecycleEnvironmentSlice.reducer diff --git a/frontend/common/services/useProjectFlag.ts b/frontend/common/services/useProjectFlag.ts index 40e93ad4c780..50fa74a95b6d 100644 --- a/frontend/common/services/useProjectFlag.ts +++ b/frontend/common/services/useProjectFlag.ts @@ -35,7 +35,13 @@ function recursivePageGet( } export const projectFlagService = service .enhanceEndpoints({ - addTagTypes: ['ProjectFlag', 'FeatureList', 'FeatureState', 'Environment'], + addTagTypes: [ + 'ProjectFlag', + 'FeatureList', + 'FeatureState', + 'Environment', + 'LifecycleCounts', + ], }) .injectEndpoints({ endpoints: (builder) => ({ @@ -68,6 +74,7 @@ export const projectFlagService = service invalidatesTags: [ { id: 'LIST', type: 'ProjectFlag' }, { id: 'LIST', type: 'FeatureList' }, + 'LifecycleCounts', ], query: (query: Req['createProjectFlag']) => ({ body: query.body, @@ -132,6 +139,16 @@ export const projectFlagService = service }), }), + getLifecycleStatusCounts: builder.query< + Res['lifecycleStatusCounts'], + Req['getLifecycleStatusCounts'] + >({ + providesTags: ['LifecycleCounts'], + query: ({ environment }) => ({ + url: `environments/${environment}/feature-lifecycle-counts/`, + }), + }), + getProjectFlag: builder.query({ providesTags: (res) => [{ id: res?.id, type: 'ProjectFlag' }], query: (query: Req['getProjectFlag']) => ({ @@ -284,6 +301,7 @@ export const { useAddFlagOwnersMutation, useCreateProjectFlagMutation, useGetFeatureListQuery, + useGetLifecycleStatusCountsQuery, useGetProjectFlagQuery, useGetProjectFlagsQuery, useRemoveFlagGroupOwnersMutation, diff --git a/frontend/common/store.ts b/frontend/common/store.ts index 80045546fb5a..025632a08eb4 100644 --- a/frontend/common/store.ts +++ b/frontend/common/store.ts @@ -14,10 +14,12 @@ import storage from 'redux-persist/lib/storage' import { Persistor } from 'redux-persist/es/types' import { service } from './service' import selectedOrganisationReducer from './selectedOrganisationSlice' +import lifecycleEnvironmentReducer from './lifecycleEnvironmentSlice' // END OF IMPORTS const createStore = () => { const reducer = combineReducers({ [service.reducerPath]: service.reducer, + lifecycleEnvironment: lifecycleEnvironmentReducer, selectedOrganisation: selectedOrganisationReducer, // END OF REDUCERS }) diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index b548720da228..e3126c94fd96 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -32,6 +32,7 @@ import { FlagsmithValue, TagStrategy, FeatureType, + LifecycleStage, } from './responses' import { UtmsType } from './utms' @@ -395,6 +396,10 @@ export type Req = { group_owners?: number[] sort_field?: string sort_direction?: SortOrder + lifecycle_stage?: LifecycleStage + } + getLifecycleStatusCounts: { + environment: number } getProjectFlag: { project: number; id: number } getRolesPermissionUsers: { organisation_id: number; role_id: number } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 0844917b7d59..69370d06e75a 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -816,8 +816,19 @@ export type ProjectFlag = { last_successful_repository_scanned_at: string last_feature_found_at: string }[] + lifecycle_stage?: LifecycleStage | null } +export type LifecycleStage = + | 'new' + | 'live' + | 'permanent' + | 'stale' + | 'needs_monitoring' + | 'to_remove' + +export type LifecycleStatusCounts = Record + export type FeatureListProviderData = { projectFlags: ProjectFlag[] | null environmentFlags: Record | undefined @@ -1344,6 +1355,7 @@ export type Res = { rolePermission: PagedResponse projectFlags: PagedResponse projectFlag: ProjectFlag + lifecycleStatusCounts: LifecycleStatusCounts identityFeatureStatesAll: IdentityFeatureState[] createRolesPermissionUsers: RolePermissionUser rolesPermissionUsers: PagedResponse diff --git a/frontend/web/components/feature-summary/ProjectFeatureRow.tsx b/frontend/web/components/feature-summary/ProjectFeatureRow.tsx index 0a207893f046..73a5f0937f94 100644 --- a/frontend/web/components/feature-summary/ProjectFeatureRow.tsx +++ b/frontend/web/components/feature-summary/ProjectFeatureRow.tsx @@ -11,6 +11,7 @@ interface ProjectFeatureRowProps { index: number isSelected?: boolean onSelect?: (projectFlag: ProjectFlag) => void + onClick?: (projectFlag: ProjectFlag) => void className?: string actions?: React.ReactNode } @@ -20,6 +21,7 @@ const ProjectFeatureRow: FC = ({ className, index, isSelected, + onClick, onSelect, projectFlag, }) => { @@ -40,8 +42,10 @@ const ProjectFeatureRow: FC = ({ className={classNames( 'd-none d-lg-flex align-items-lg-center flex-lg-row list-item py-0 list-item-xs fs-small', className, + { clickable: !!onClick }, )} data-test={`cleanup-feature-item-${index}`} + onClick={onClick ? () => onClick(projectFlag) : undefined} > {onSelect && (
= ({
onClick(projectFlag) : undefined} >
{onSelect && ( diff --git a/frontend/web/components/pages/feature-lifecycle/FeatureLifecyclePage.tsx b/frontend/web/components/pages/feature-lifecycle/FeatureLifecyclePage.tsx index 816dfd168b31..7801ee5b1ad7 100644 --- a/frontend/web/components/pages/feature-lifecycle/FeatureLifecyclePage.tsx +++ b/frontend/web/components/pages/feature-lifecycle/FeatureLifecyclePage.tsx @@ -1,30 +1,31 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react' +import React, { FC, useCallback, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' import { useRouteContext } from 'components/providers/RouteContext' import { usePageTracking } from 'common/hooks/usePageTracking' -import { useProjectEnvironments } from 'common/hooks/useProjectEnvironments' import { hasActiveFilters } from 'common/utils/featureFilterParams' import PageTitle from 'components/PageTitle' import Icon from 'components/icons/Icon' import Button from 'components/base/forms/Button' -import EnvironmentSelect from 'components/EnvironmentTagSelect' import CreateFlagModal from 'components/modals/create-feature' import LifecycleSidebar from './components/LifecycleSidebar' -import EvaluationChecker from './components/EvaluationChecker' +import FeatureUsageModal from './components/FeatureUsageModal' import NewSection from './components/NewSection' import LiveSection from './components/LiveSection' import PermanentSection from './components/PermanentSection' import StaleSection from './components/StaleSection' import MonitorSection from './components/MonitorSection' import RemoveSection from './components/RemoveSection' -import { useLifecycleData } from './hooks/useLifecycleData' -import { useEvaluationCounts } from './hooks/useEvaluationCounts' +import type { ProjectFlag } from 'common/types/responses' +import { useLifecycleEnvironment } from './hooks/useLifecycleEnvironment' +import { + useLifecycleCounts, + useLifecycleSectionFlags, +} from './hooks/useLifecycleData' import { DEFAULT_FILTER_STATE, MONITOR_TOOLTIP, SECTIONS, STALE_TOOLTIP, - buildPeriodOptions, } from './constants' import type { Section } from './types' import type { FilterState } from 'common/types/featureFilters' @@ -46,21 +47,11 @@ function useSectionParam(): Section { const FeatureLifecyclePage: FC = () => { const routeContext = useRouteContext() - const projectId = routeContext.projectId as string - const { environments } = useProjectEnvironments(projectId) - const defaultEnvironmentApiKey = environments[0]?.api_key + const projectId = String(routeContext.projectId) + const projectIdNum = Number(projectId) - const allEnvironmentIds = useMemo( - () => environments.map((e) => `${e.id}`), - [environments], - ) - const [selectedEnvironments, setSelectedEnvironments] = useState([]) - - useEffect(() => { - if (allEnvironmentIds.length > 0 && selectedEnvironments.length === 0) { - setSelectedEnvironments(allEnvironmentIds) - } - }, [allEnvironmentIds, selectedEnvironments.length]) + const { environmentId, setEnvironmentId } = + useLifecycleEnvironment(projectIdNum) const [filters, setFilters] = useState(DEFAULT_FILTER_STATE) const handleFilterChange = useCallback( @@ -69,6 +60,21 @@ const FeatureLifecyclePage: FC = () => { [], ) const clearFilters = useCallback(() => setFilters(DEFAULT_FILTER_STATE), []) + + const handleFeatureClick = useCallback( + (flag: ProjectFlag) => { + openModal( + flag.name, + , + 'side-modal create-feature-modal', + ) + }, + [projectIdNum, environmentId], + ) const hasFilters = hasActiveFilters(filters) const section = useSectionParam() @@ -76,59 +82,31 @@ const FeatureLifecyclePage: FC = () => { (s) => s.key === section, ) as (typeof SECTIONS)[number] - const [monitorPeriod, setMonitorPeriod] = useState(1) - const [removePeriod, setRemovePeriod] = useState(7) + const { counts, isLoading: isLoadingCounts } = useLifecycleCounts({ + environmentId, + }) - // Central data hook — 2 API calls, all filtering done here const { - counts, error, - isLoading, - liveFlags, - newFlags, - permanentFlags, - staleFlags, - staleNoCodeFlags, - } = useLifecycleData({ - environmentApiKey: defaultEnvironmentApiKey, + flags, + isLoading: isLoadingFlags, + } = useLifecycleSectionFlags({ + environmentId, filters, - projectId, - }) - - // Monitor evaluation counts (short period — "has evaluation within") - const { - handleEvaluationResult: handleMonitorResult, - isCheckingEvaluations: isCheckingMonitor, - monitorCount, - monitorFlags, - } = useEvaluationCounts({ - period: monitorPeriod, - selectedEnvironments, - staleNoCodeFlags, - }) - - // Remove evaluation counts (longer period — "no evaluations in") - const { - handleEvaluationResult: handleRemoveResult, - isCheckingEvaluations: isCheckingRemove, - removeCount, - removeFlags, - } = useEvaluationCounts({ - period: removePeriod, - selectedEnvironments, - staleNoCodeFlags, + projectId: projectIdNum, + section, }) usePageTracking({ context: { organisationId: routeContext.organisationId, - projectId, + projectId: projectIdNum, }, pageName: 'CLEANUP', saveToStorage: false, }) - if (!defaultEnvironmentApiKey) { + if (!environmentId) { return (
@@ -137,125 +115,46 @@ const FeatureLifecyclePage: FC = () => { } const filterProps = { + error, filters, + flags, hasFilters, + isLoading: isLoadingFlags, onClearFilters: clearFilters, + onFeatureClick: handleFeatureClick, onFilterChange: handleFilterChange, - projectId, + projectId: projectIdNum, } const renderSection = () => { switch (section) { case 'new': - return ( - - ) + return case 'live': - return ( - - ) + return case 'permanent': - return ( - - ) + return case 'stale': - return ( - - ) + return case 'monitor': - return ( - - ) + return case 'remove': - return ( - - ) + return default: return null } } - const activePeriod = section === 'monitor' ? monitorPeriod : removePeriod - const setActivePeriod = - section === 'monitor' ? setMonitorPeriod : setRemovePeriod - const periodPrefix = - section === 'monitor' ? 'Evaluated within' : 'No evaluations in' - const periodOptions = buildPeriodOptions(periodPrefix) - return (
- {/* Hidden evaluators — monitor period */} - {staleNoCodeFlags.map((flag) => - selectedEnvironments.map((envId) => ( - - )), - )} - {/* Hidden evaluators — remove period */} - {staleNoCodeFlags.map((flag) => - selectedEnvironments.map((envId) => ( - - )), - )}
@@ -269,7 +168,7 @@ const FeatureLifecyclePage: FC = () => { openModal( 'New Feature', , 'side-modal create-feature-modal', @@ -298,7 +197,7 @@ const FeatureLifecyclePage: FC = () => { e.preventDefault()} > @@ -311,35 +210,6 @@ const FeatureLifecyclePage: FC = () => { )}
- {(section === 'monitor' || section === 'remove') && ( - <> -
-