diff --git a/frontend/cypress/integration/projects/overview.spec.ts b/frontend/cypress/integration/projects/overview.spec.ts
index 20dc503128..bf0551f007 100644
--- a/frontend/cypress/integration/projects/overview.spec.ts
+++ b/frontend/cypress/integration/projects/overview.spec.ts
@@ -14,8 +14,7 @@ describe('project overview', () => {
const featureToggleName = `${featureTogglePrefix}-${randomId}`;
const projectName = `unleash-e2e-project-overview-${randomId}`;
const baseUrl = Cypress.config().baseUrl;
- const selectAll =
- '[title="Toggle All Rows Selected"] input[type="checkbox"]';
+ const selectAll = '[title="Select all rows"] input[type="checkbox"]';
before(() => {
cy.runBefore();
diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx
index bc8e69da8f..05f36cef13 100644
--- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx
+++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx
@@ -13,6 +13,7 @@ import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/use
import { UserStats } from './UserStats/UserStats';
import { FlagStats } from './FlagStats/FlagStats';
import { Widget } from './Widget/Widget';
+import { FlagsProjectChart } from './FlagsProjectChart/FlagsProjectChart';
const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid',
@@ -107,6 +108,10 @@ export const ExecutiveDashboard: VFC = () => {
/>
+
+
>
);
};
diff --git a/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx b/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx
index e818ea5f0b..bcb52b0db9 100644
--- a/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx
+++ b/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx
@@ -64,7 +64,11 @@ const createOptions = (theme: Theme, locationSettings: ILocationSettings) =>
const date =
item?.chart?.data?.labels?.[item.dataIndex];
return date
- ? formatDateYMD(date, locationSettings.locale)
+ ? formatDateYMD(
+ date,
+ locationSettings.locale,
+ 'UTC',
+ )
: '';
},
},
diff --git a/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx
new file mode 100644
index 0000000000..0200b68fd5
--- /dev/null
+++ b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx
@@ -0,0 +1,5 @@
+import { lazy } from 'react';
+
+export const FlagsProjectChart = lazy(
+ () => import('./FlagsProjectChartComponent'),
+);
diff --git a/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChartComponent.tsx b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChartComponent.tsx
new file mode 100644
index 0000000000..07b5424126
--- /dev/null
+++ b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChartComponent.tsx
@@ -0,0 +1,162 @@
+import { useMemo, type VFC } from 'react';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+ TimeScale,
+} from 'chart.js';
+import { Line } from 'react-chartjs-2';
+import 'chartjs-adapter-date-fns';
+import { Paper, Theme, Typography, useTheme } from '@mui/material';
+import {
+ useLocationSettings,
+ type ILocationSettings,
+} from 'hooks/useLocationSettings';
+import { formatDateYMD } from 'utils/formatDate';
+import {
+ ExecutiveSummarySchema,
+ ExecutiveSummarySchemaProjectFlagTrendsItem,
+} from 'openapi';
+
+const getRandomColor = () => {
+ const letters = '0123456789ABCDEF';
+ let color = '#';
+ for (let i = 0; i < 6; i++) {
+ color += letters[Math.floor(Math.random() * 16)];
+ }
+ return color;
+};
+
+const createData = (
+ theme: Theme,
+ flagTrends: ExecutiveSummarySchema['projectFlagTrends'] = [],
+) => {
+ const groupedFlagTrends = flagTrends.reduce<
+ Record
+ >((groups, item) => {
+ if (!groups[item.project]) {
+ groups[item.project] = [];
+ }
+ groups[item.project].push(item);
+ return groups;
+ }, {});
+
+ const datasets = Object.entries(groupedFlagTrends).map(
+ ([project, trends]) => {
+ const color = getRandomColor();
+ return {
+ label: project,
+ data: trends.map((item) => item.total),
+ borderColor: color,
+ backgroundColor: color,
+ fill: true,
+ };
+ },
+ );
+
+ return {
+ labels: flagTrends.map((item) => item.date),
+ datasets,
+ };
+};
+
+const createOptions = (theme: Theme, locationSettings: ILocationSettings) =>
+ ({
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ },
+ tooltip: {
+ callbacks: {
+ title: (tooltipItems: any) => {
+ const item = tooltipItems?.[0];
+ const date =
+ item?.chart?.data?.labels?.[item.dataIndex];
+ return date
+ ? formatDateYMD(
+ date,
+ locationSettings.locale,
+ 'UTC',
+ )
+ : '';
+ },
+ },
+ },
+ },
+ locale: locationSettings.locale,
+ interaction: {
+ intersect: false,
+ axis: 'x',
+ },
+ color: theme.palette.text.secondary,
+ scales: {
+ y: {
+ type: 'linear',
+ grid: {
+ color: theme.palette.divider,
+ borderColor: theme.palette.divider,
+ },
+ ticks: { color: theme.palette.text.secondary },
+ },
+ x: {
+ type: 'time',
+ time: {
+ unit: 'month',
+ },
+ grid: {
+ color: theme.palette.divider,
+ borderColor: theme.palette.divider,
+ },
+ ticks: {
+ color: theme.palette.text.secondary,
+ },
+ },
+ },
+ }) as const;
+
+interface IFlagsChartComponentProps {
+ projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
+}
+
+const FlagsProjectChart: VFC = ({
+ projectFlagTrends,
+}) => {
+ const theme = useTheme();
+ const { locationSettings } = useLocationSettings();
+ const data = useMemo(
+ () => createData(theme, projectFlagTrends),
+ [theme, projectFlagTrends],
+ );
+ const options = createOptions(theme, locationSettings);
+
+ return (
+ ({ padding: theme.spacing(4) })}>
+ ({ marginBottom: theme.spacing(3) })}
+ >
+ Number of flags per project
+
+
+
+ );
+};
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ TimeScale,
+ Title,
+ Tooltip,
+ Legend,
+);
+
+export default FlagsProjectChart;
diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx
index de08ae46ad..b3f2d15269 100644
--- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx
+++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx
@@ -35,12 +35,6 @@ const setupNoFeaturesReturned = () =>
});
const setupApi = (features: APIFeature[], projects: APIProject[]) => {
- testServerRoute(server, '/api/admin/ui-config', {
- flags: {
- featureSearchFrontend: true,
- },
- });
-
testServerRoute(server, '/api/admin/projects', {
projects,
});
@@ -146,6 +140,6 @@ test('Filter table by project', async () => {
'No feature toggles found matching your criteria. Get started by adding a new feature toggle.',
);
expect(window.location.href).toContain(
- '?sort=createdAt&order=desc&offset=0&columns=&project=IS%3Aproject-b',
+ '?offset=0&columns=&project=IS%3Aproject-b',
);
}, 10000);
diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx
index b7bb020369..f7c94583f9 100644
--- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx
+++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx
@@ -50,7 +50,6 @@ import { usePersistentTableState } from 'hooks/usePersistentTableState';
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentCell/FeatureSegmentCell';
import { useUiFlag } from 'hooks/useUiFlag';
-import { FeatureToggleListTable as LegacyFeatureToggleListTable } from './LegacyFeatureToggleListTable';
import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions';
import useLoading from 'hooks/useLoading';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
@@ -68,7 +67,7 @@ export const featuresPlaceholder = Array(15).fill({
const columnHelper = createColumnHelper();
const feedbackCategory = 'search';
-const FeatureToggleListTableComponent: VFC = () => {
+export const FeatureToggleListTable: VFC = () => {
const theme = useTheme();
const { openFeedback } = useFeedback(feedbackCategory, 'automatic');
const { trackEvent } = usePlausibleTracker();
@@ -428,11 +427,3 @@ const FeatureToggleListTableComponent: VFC = () => {
);
};
-
-export const FeatureToggleListTable: VFC = () => {
- const featureSearchFrontend = useUiFlag('featureSearchFrontend');
-
- if (featureSearchFrontend) return ;
-
- return ;
-};
diff --git a/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx
deleted file mode 100644
index 5806b83136..0000000000
--- a/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx
+++ /dev/null
@@ -1,413 +0,0 @@
-import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
-import {
- IconButton,
- Link,
- Tooltip,
- useMediaQuery,
- useTheme,
-} from '@mui/material';
-import { Link as RouterLink, useSearchParams } from 'react-router-dom';
-import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
-import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
-import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
-import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
-import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
-import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
-import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
-import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
-import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
-import { PageContent } from 'component/common/PageContent/PageContent';
-import { PageHeader } from 'component/common/PageHeader/PageHeader';
-import { createLocalStorage } from 'utils/createLocalStorage';
-import { FeatureSchema } from 'openapi';
-import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
-import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
-import { useSearch } from 'hooks/useSearch';
-import { Search } from 'component/common/Search/Search';
-import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
-import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
-import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
-import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
-import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
-import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
-import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
-import FileDownload from '@mui/icons-material/FileDownload';
-import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
-import { ExportDialog } from './ExportDialog';
-import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
-import { focusable } from 'themes/themeStyles';
-import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
-import useToast from 'hooks/useToast';
-
-export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
- name: 'Name of the feature',
- description: 'Short description of the feature',
- type: '-',
- createdAt: new Date(2022, 1, 1),
- project: 'projectID',
-});
-
-export type PageQueryType = Partial<
- Record<'sort' | 'order' | 'search' | 'favorites', string>
->;
-
-const defaultSort: SortingRule = { id: 'createdAt', desc: true };
-
-const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
- 'FeatureToggleListTable:v1',
- defaultSort,
-);
-
-/**
- * @deprecated remove with flag `featureSearchFrontend`
- */
-export const FeatureToggleListTable: VFC = () => {
- const theme = useTheme();
- const { environments } = useEnvironments();
- const enabledEnvironments = environments
- .filter((env) => env.enabled)
- .map((env) => env.name);
- const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
- const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
- const [showExportDialog, setShowExportDialog] = useState(false);
- const { features = [], loading, refetchFeatures } = useFeatures();
- const [searchParams, setSearchParams] = useSearchParams();
- const { setToastApiError } = useToast();
- const { uiConfig } = useUiConfig();
-
- const [initialState] = useState(() => ({
- sortBy: [
- {
- id: searchParams.get('sort') || storedParams.id,
- desc: searchParams.has('order')
- ? searchParams.get('order') === 'desc'
- : storedParams.desc,
- },
- ],
- hiddenColumns: ['description'],
- globalFilter: searchParams.get('search') || '',
- }));
- const { value: globalStore, setValue: setGlobalStore } =
- useGlobalLocalStorage();
- const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
- usePinnedFavorites(
- searchParams.has('favorites')
- ? searchParams.get('favorites') === 'true'
- : globalStore.favorites,
- );
- const [searchValue, setSearchValue] = useState(initialState.globalFilter);
- const { favorite, unfavorite } = useFavoriteFeaturesApi();
- const onFavorite = useCallback(
- async (feature: any) => {
- try {
- if (feature?.favorite) {
- await unfavorite(feature.project, feature.name);
- } else {
- await favorite(feature.project, feature.name);
- }
- refetchFeatures();
- } catch (error) {
- setToastApiError(
- 'Something went wrong, could not update favorite',
- );
- }
- },
- [favorite, refetchFeatures, unfavorite, setToastApiError],
- );
-
- const columns = useMemo(
- () => [
- {
- Header: (
-
- ),
- accessor: 'favorite',
- Cell: ({ row: { original: feature } }: any) => (
- onFavorite(feature)}
- />
- ),
- maxWidth: 50,
- disableSortBy: true,
- },
- {
- Header: 'Seen',
- accessor: 'lastSeenAt',
- Cell: ({ value, row: { original: feature } }: any) => {
- return ;
- },
- align: 'center',
- maxWidth: 80,
- },
- {
- Header: 'Type',
- accessor: 'type',
- Cell: FeatureTypeCell,
- align: 'center',
- maxWidth: 85,
- },
- {
- Header: 'Name',
- accessor: 'name',
- minWidth: 150,
- Cell: FeatureNameCell,
- sortType: 'alphanumeric',
- searchable: true,
- },
- {
- id: 'tags',
- Header: 'Tags',
- accessor: (row: FeatureSchema) =>
- row.tags
- ?.map(({ type, value }) => `${type}:${value}`)
- .join('\n') || '',
- Cell: FeatureTagCell,
- width: 80,
- searchable: true,
- },
- {
- Header: 'Created',
- accessor: 'createdAt',
- Cell: DateCell,
- maxWidth: 150,
- },
- {
- Header: 'Project ID',
- accessor: 'project',
- Cell: ({ value }: { value: string }) => (
-
- ),
- sortType: 'alphanumeric',
- maxWidth: 150,
- filterName: 'project',
- searchable: true,
- },
- {
- Header: 'State',
- accessor: 'stale',
- Cell: FeatureStaleCell,
- sortType: 'boolean',
- maxWidth: 120,
- filterName: 'state',
- filterParsing: (value: any) => (value ? 'stale' : 'active'),
- },
- // Always hidden -- for search
- {
- accessor: 'description',
- Header: 'Description',
- searchable: true,
- },
- ],
- [isFavoritesPinned],
- );
-
- const {
- data: searchedData,
- getSearchText,
- getSearchContext,
- } = useSearch(columns, searchValue, features);
-
- const data = useMemo(
- () =>
- searchedData?.length === 0 && loading
- ? featuresPlaceholder
- : searchedData,
- [searchedData, loading],
- );
-
- const {
- headerGroups,
- rows,
- prepareRow,
- state: { sortBy },
- setHiddenColumns,
- } = useTable(
- {
- columns: columns as any[],
- data,
- initialState,
- sortTypes,
- autoResetHiddenColumns: false,
- autoResetSortBy: false,
- disableSortRemove: true,
- disableMultiSort: true,
- },
- useSortBy,
- useFlexLayout,
- );
-
- useConditionallyHiddenColumns(
- [
- {
- condition: !features.some(({ tags }) => tags?.length),
- columns: ['tags'],
- },
- {
- condition: isSmallScreen,
- columns: ['type', 'createdAt', 'tags'],
- },
- {
- condition: isMediumScreen,
- columns: ['lastSeenAt', 'stale'],
- },
- ],
- setHiddenColumns,
- columns,
- );
-
- useEffect(() => {
- const tableState: PageQueryType = {};
- tableState.sort = sortBy[0].id;
- if (sortBy[0].desc) {
- tableState.order = 'desc';
- }
- if (searchValue) {
- tableState.search = searchValue;
- }
- if (isFavoritesPinned) {
- tableState.favorites = 'true';
- }
-
- setSearchParams(tableState, {
- replace: true,
- });
- setStoredParams({
- id: sortBy[0].id,
- desc: sortBy[0].desc || false,
- });
- setGlobalStore((params) => ({
- ...params,
- favorites: Boolean(isFavoritesPinned),
- }));
- }, [sortBy, searchValue, setSearchParams, isFavoritesPinned]);
-
- if (!(environments.length > 0)) {
- return null;
- }
-
- return (
-
-
-
-
- >
- }
- />
-
- View archive
-
-
-
- setShowExportDialog(true)
- }
- sx={(theme) => ({
- marginRight: theme.spacing(2),
- })}
- >
-
-
-
- }
- />
-
-
- >
- }
- >
-
- }
- />
-
- }
- >
-
-
-
- 0}
- show={
-
- No feature toggles found matching “
- {searchValue}
- ”
-
- }
- elseShow={
-
- No feature toggles available. Get started by
- adding a new feature toggle.
-
- }
- />
- }
- />
- setShowExportDialog(false)}
- environments={enabledEnvironments}
- />
- }
- />
-
- );
-};
diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx
index 6eff96bc6c..5d4ade9f08 100644
--- a/frontend/src/component/menu/Header/Header.tsx
+++ b/frontend/src/component/menu/Header/Header.tsx
@@ -157,7 +157,6 @@ const Header: VFC = () => {
const [configRef, setConfigRef] = useState(null);
const disableNotifications = useUiFlag('disableNotifications');
- const hasSearch = useUiFlag('featureSearchFrontend');
const { uiConfig, isOss } = useUiConfig();
const smallScreen = useMediaQuery(theme.breakpoints.down('md'));
const [openDrawer, setOpenDrawer] = useState(false);
@@ -249,17 +248,7 @@ const Header: VFC = () => {
Projects
- Search
- }
- elseShow={
-
- Feature toggles
-
- }
- />
+ Search
Playground
setConfigRef(e.currentTarget)}
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
index d2821aabe8..6110b83b6a 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
@@ -119,17 +119,6 @@ exports[`returns all baseRoutes 1`] = `
"menu": {
"mobile": true,
},
- "notFlag": "featureSearchFrontend",
- "path": "/features",
- "title": "Feature toggles",
- "type": "protected",
- },
- {
- "component": [Function],
- "flag": "featureSearchFrontend",
- "menu": {
- "mobile": true,
- },
"path": "/search",
"title": "Search",
"type": "protected",
diff --git a/frontend/src/component/menu/__tests__/routes.test.tsx b/frontend/src/component/menu/__tests__/routes.test.tsx
index f0d7f11330..a456924b9c 100644
--- a/frontend/src/component/menu/__tests__/routes.test.tsx
+++ b/frontend/src/component/menu/__tests__/routes.test.tsx
@@ -5,6 +5,6 @@ test('returns all baseRoutes', () => {
});
test('getRoute() returns named route', () => {
- const featuresRoute = getRoute('/features');
- expect(featuresRoute?.path).toEqual('/features');
+ const featuresRoute = getRoute('/search');
+ expect(featuresRoute?.path).toEqual('/search');
});
diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts
index 26be4509c5..3315f8aaaf 100644
--- a/frontend/src/component/menu/routes.ts
+++ b/frontend/src/component/menu/routes.ts
@@ -142,21 +142,12 @@ export const routes: IRoute[] = [
type: 'protected',
menu: {},
},
- {
- path: '/features',
- title: 'Feature toggles',
- component: FeatureToggleListTable,
- type: 'protected',
- menu: { mobile: true },
- notFlag: 'featureSearchFrontend',
- },
{
path: '/search',
title: 'Search',
component: FeatureToggleListTable,
type: 'protected',
menu: { mobile: true },
- flag: 'featureSearchFrontend',
},
// Playground
diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
similarity index 99%
rename from frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx
rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
index 8ce636f7d0..1abbb92efb 100644
--- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx
+++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
@@ -64,7 +64,7 @@ const formatEnvironmentColumnId = (environment: string) =>
const columnHelper = createColumnHelper();
const getRowId = (row: { name: string }) => row.name;
-export const PaginatedProjectFeatureToggles = ({
+export const ProjectFeatureToggles = ({
environments,
refreshInterval = 15 * 1000,
storageKey = 'project-feature-toggles-v2',
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.types.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.types.ts
similarity index 100%
rename from frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.types.ts
rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.types.ts
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx
index 0df836335e..3a07eb9f5e 100644
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx
@@ -4,8 +4,8 @@ import { flexRow } from 'themes/themeStyles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
import { FeatureToggleSwitch } from './FeatureToggleSwitch';
-import type { ListItemType } from '../ProjectFeatureToggles.types';
import type { UseFeatureToggleSwitchType } from './FeatureToggleSwitch.types';
+import { ListItemType } from '../../PaginatedProjectFeatureToggles/ProjectFeatureToggles.types';
const StyledSwitchContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'hasWarning',
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
deleted file mode 100644
index 661943344f..0000000000
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
+++ /dev/null
@@ -1,700 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import {
- Checkbox,
- IconButton,
- styled,
- Tooltip,
- useMediaQuery,
- useTheme,
- Box,
-} from '@mui/material';
-import { Add } from '@mui/icons-material';
-import { useNavigate, useSearchParams } from 'react-router-dom';
-import {
- SortingRule,
- useFlexLayout,
- useRowSelect,
- useSortBy,
- useTable,
-} from 'react-table';
-import type { FeatureSchema } from 'openapi';
-import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
-import { PageHeader } from 'component/common/PageHeader/PageHeader';
-import { PageContent } from 'component/common/PageContent/PageContent';
-import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
-import { getCreateTogglePath } from 'utils/routePathHelpers';
-import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
-import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
-import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
-import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
-import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
-import { IProject } from 'interfaces/project';
-import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
-import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
-import { createLocalStorage } from 'utils/createLocalStorage';
-import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
-import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
-import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
-import { Search } from 'component/common/Search/Search';
-import { IFeatureToggleListItem } from 'interfaces/featureToggle';
-import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
-import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
-import {
- ProjectEnvironmentType,
- useEnvironmentsRef,
-} from './hooks/useEnvironmentsRef';
-import { ActionsCell } from './ActionsCell/ActionsCell';
-import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
-import { useStyles } from './ProjectFeatureToggles.styles';
-import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
-import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
-import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
-import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
-import FileDownload from '@mui/icons-material/FileDownload';
-import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
-import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
-import { MemoizedRowSelectCell } from './RowSelectCell/RowSelectCell';
-import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
-import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
-import { MemoizedFeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
-import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
-import { ListItemType } from './ProjectFeatureToggles.types';
-import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
-import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
-import useToast from 'hooks/useToast';
-
-const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
- whiteSpace: 'nowrap',
-}));
-
-interface IProjectFeatureTogglesProps {
- features: IProject['features'];
- environments: IProject['environments'];
- loading: boolean;
- onChange: () => void;
- total?: number;
- style?: React.CSSProperties;
-}
-
-const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
-
-const defaultSort: SortingRule & {
- columns?: string[];
-} = { id: 'createdAt', desc: true };
-
-/**
- * @deprecated remove when flag `featureSearchFrontend` is removed
- */
-export const ProjectFeatureToggles = ({
- features,
- loading,
- environments: newEnvironments = [],
- onChange,
- total,
- style = {},
-}: IProjectFeatureTogglesProps) => {
- const { classes: styles } = useStyles();
- const theme = useTheme();
- const { setToastApiError } = useToast();
- const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
- const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
- featureId?: string;
- stale?: boolean;
- }>({});
- const [featureArchiveState, setFeatureArchiveState] = useState<
- string | undefined
- >();
- const projectId = useRequiredPathParam('projectId');
- const { onToggle: onFeatureToggle, modals: featureToggleModals } =
- useFeatureToggleSwitch(projectId);
-
- const { value: storedParams, setValue: setStoredParams } =
- createLocalStorage(
- `${projectId}:FeatureToggleListTable:v1`,
- defaultSort,
- );
- const { value: globalStore, setValue: setGlobalStore } =
- useGlobalLocalStorage();
- const navigate = useNavigate();
- const [searchParams, setSearchParams] = useSearchParams();
- const environments = useEnvironmentsRef(
- loading
- ? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }]
- : newEnvironments,
- );
- const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
- usePinnedFavorites(
- searchParams.has('favorites')
- ? searchParams.get('favorites') === 'true'
- : globalStore.favorites,
- );
- const { favorite, unfavorite } = useFavoriteFeaturesApi();
- const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
- const [showExportDialog, setShowExportDialog] = useState(false);
- const { uiConfig } = useUiConfig();
-
- const onFavorite = useCallback(
- async (feature: IFeatureToggleListItem) => {
- try {
- if (feature?.favorite) {
- await unfavorite(projectId, feature.name);
- } else {
- await favorite(projectId, feature.name);
- }
- onChange();
- } catch (error) {
- setToastApiError(
- 'Something went wrong, could not update favorite',
- );
- }
- },
- [projectId, onChange],
- );
-
- const showTagsColumn = useMemo(
- () => features.some((feature) => feature?.tags?.length),
- [features],
- );
-
- const columns = useMemo(
- () => [
- {
- id: 'Select',
- Header: ({ getToggleAllRowsSelectedProps }: any) => (
-
- ),
- Cell: ({ row }: any) => (
-
- ),
- maxWidth: 50,
- disableSortBy: true,
- hideInMenu: true,
- styles: {
- borderRadius: 0,
- },
- },
- {
- id: 'favorite',
- Header: (
-
- ),
- accessor: 'favorite',
- Cell: ({ row: { original: feature } }: any) => (
- onFavorite(feature)}
- />
- ),
- maxWidth: 50,
- disableSortBy: true,
- hideInMenu: true,
- },
- {
- Header: 'Seen',
- accessor: 'lastSeenAt',
- Cell: ({ value, row: { original: feature } }: any) => {
- return (
-
- );
- },
- align: 'center',
- maxWidth: 80,
- },
- {
- Header: 'Type',
- accessor: 'type',
- Cell: FeatureTypeCell,
- align: 'center',
- filterName: 'type',
- maxWidth: 80,
- },
- {
- Header: 'Name',
- accessor: 'name',
- Cell: ({ value }: { value: string }) => (
-
-
-
-
-
- ),
- minWidth: 100,
- sortType: 'alphanumeric',
- searchable: true,
- },
- ...(showTagsColumn
- ? [
- {
- id: 'tags',
- Header: 'Tags',
- accessor: (row: IFeatureToggleListItem) =>
- row.tags
- ?.map(({ type, value }) => `${type}:${value}`)
- .join('\n') || '',
- Cell: FeatureTagCell,
- width: 80,
- searchable: true,
- filterName: 'tags',
- filterBy(
- row: IFeatureToggleListItem,
- values: string[],
- ) {
- return includesFilter(
- getColumnValues(this, row),
- values,
- );
- },
- },
- ]
- : []),
- {
- Header: 'Created',
- accessor: 'createdAt',
- Cell: DateCell,
- minWidth: 120,
- },
- ...environments.map((value: ProjectEnvironmentType | string) => {
- const name =
- typeof value === 'string'
- ? value
- : (value as ProjectEnvironmentType).environment;
- const isChangeRequestEnabled = isChangeRequestConfigured(name);
- const FeatureToggleCell = createFeatureToggleCell(
- projectId,
- name,
- isChangeRequestEnabled,
- onChange,
- onFeatureToggle,
- );
-
- return {
- Header: loading ? () => '' : name,
- maxWidth: 90,
- id: `environments.${name}`,
- accessor: (row: ListItemType) =>
- row.environments[name]?.enabled,
- align: 'center',
- Cell: FeatureToggleCell,
- sortType: 'boolean',
- filterName: name,
- filterParsing: (value: boolean) =>
- value ? 'enabled' : 'disabled',
- };
- }),
-
- {
- id: 'Actions',
- maxWidth: 56,
- width: 56,
- Cell: (props: { row: { original: ListItemType } }) => (
-
- ),
- disableSortBy: true,
- hideInMenu: true,
- styles: {
- borderRadius: 0,
- },
- },
- ],
- [projectId, environments, loading],
- );
-
- const [searchValue, setSearchValue] = useState(
- searchParams.get('search') || '',
- );
-
- const [showTitle, setShowTitle] = useState(true);
-
- const featuresData = useMemo(
- () =>
- features.map((feature) => ({
- ...feature,
- environments: Object.fromEntries(
- environments.map((env) => {
- const thisEnv = feature?.environments.find(
- (featureEnvironment) =>
- featureEnvironment?.name === env,
- );
- return [
- env,
- {
- name: env,
- enabled: thisEnv?.enabled || false,
- variantCount: thisEnv?.variantCount || 0,
- lastSeenAt: thisEnv?.lastSeenAt,
- type: thisEnv?.type,
- hasStrategies: thisEnv?.hasStrategies,
- hasEnabledStrategies:
- thisEnv?.hasEnabledStrategies,
- },
- ];
- }),
- ),
- someEnabledEnvironmentHasVariants:
- feature.environments?.some(
- (featureEnvironment) =>
- featureEnvironment.variantCount > 0 &&
- featureEnvironment.enabled,
- ) || false,
- })),
- [features, environments],
- );
-
- const {
- data: searchedData,
- getSearchText,
- getSearchContext,
- } = useSearch(columns, searchValue, featuresData);
-
- const data = useMemo(() => {
- if (loading) {
- return Array(6).fill({
- type: '-',
- name: 'Feature name',
- createdAt: new Date(),
- environments: {
- production: { name: 'production', enabled: false },
- },
- }) as FeatureSchema[];
- }
- return searchedData;
- }, [loading, searchedData]);
-
- const initialState = useMemo(
- () => {
- const allColumnIds = columns
- .map(
- (column: any) =>
- (column?.id as string) ||
- (typeof column?.accessor === 'string'
- ? (column?.accessor as string)
- : ''),
- )
- .filter(Boolean);
- let hiddenColumns = environments
- .filter((_, index) => index >= 3)
- .map((environment) => `environment:${environment}`);
-
- if (searchParams.has('columns')) {
- const columnsInParams =
- searchParams.get('columns')?.split(',') || [];
- const visibleColumns = [...staticColumns, ...columnsInParams];
- hiddenColumns = allColumnIds.filter(
- (columnId) => !visibleColumns.includes(columnId),
- );
- } else if (storedParams.columns) {
- const visibleColumns = [
- ...staticColumns,
- ...storedParams.columns,
- ];
- hiddenColumns = allColumnIds.filter(
- (columnId) => !visibleColumns.includes(columnId),
- );
- }
-
- return {
- sortBy: [
- {
- id:
- searchParams.get('sort') ||
- storedParams.id ||
- 'createdAt',
- desc: searchParams.has('order')
- ? searchParams.get('order') === 'desc'
- : storedParams.desc,
- },
- ],
- hiddenColumns,
- selectedRowIds: {},
- };
- },
- [environments], // eslint-disable-line react-hooks/exhaustive-deps
- );
-
- const getRowId = useCallback((row: any) => row.name, []);
- const {
- allColumns,
- headerGroups,
- rows,
- state: { selectedRowIds, sortBy, hiddenColumns },
- prepareRow,
- setHiddenColumns,
- toggleAllRowsSelected,
- } = useTable(
- {
- columns: columns as any[], // TODO: fix after `react-table` v8 update
- data,
- initialState,
- sortTypes,
- autoResetHiddenColumns: false,
- autoResetSelectedRows: false,
- disableSortRemove: true,
- autoResetSortBy: false,
- getRowId,
- },
- useFlexLayout,
- useSortBy,
- useRowSelect,
- );
-
- useEffect(() => {
- if (loading) {
- return;
- }
- const tableState: Record = {};
- tableState.sort = sortBy[0].id;
- if (sortBy[0].desc) {
- tableState.order = 'desc';
- }
- if (searchValue) {
- tableState.search = searchValue;
- }
- if (isFavoritesPinned) {
- tableState.favorites = 'true';
- }
- tableState.columns = allColumns
- .map(({ id }) => id)
- .filter(
- (id) =>
- !staticColumns.includes(id) && !hiddenColumns?.includes(id),
- )
- .join(',');
-
- setSearchParams(tableState, {
- replace: true,
- });
- setStoredParams((params) => ({
- ...params,
- id: sortBy[0].id,
- desc: sortBy[0].desc || false,
- columns: tableState.columns.split(','),
- }));
- setGlobalStore((params) => ({
- ...params,
- favorites: Boolean(isFavoritesPinned),
- }));
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- loading,
- sortBy,
- hiddenColumns,
- searchValue,
- setSearchParams,
- isFavoritesPinned,
- ]);
-
- return (
- <>
- ({
- padding: `${theme.spacing(2.5)} ${theme.spacing(
- 3.125,
- )}`,
- })}
- >
-
-
- setShowTitle(false)
- }
- onBlur={() =>
- setShowTitle(true)
- }
- hasFilters
- getSearchContext={
- getSearchContext
- }
- id='projectFeatureToggles'
- />
- }
- />
-
-
-
-
- setShowExportDialog(
- true,
- )
- }
- sx={(theme) => ({
- marginRight:
- theme.spacing(2),
- })}
- >
-
-
-
- }
- />
-
- navigate(
- getCreateTogglePath(projectId),
- )
- }
- maxWidth='960px'
- Icon={Add}
- projectId={projectId}
- permission={CREATE_FEATURE}
- data-testid='NAVIGATE_TO_CREATE_FEATURE'
- >
- New feature toggle
-
- >
- }
- >
-
- }
- />
-
-
- }
- >
-
-
-
- 0}
- show={
-
-
- No feature toggles found matching
- “
- {searchValue}
- ”
-
-
- }
- elseShow={
-
-
- No feature toggles found matching your
- criteria. Get started by adding a new
- feature toggle.
-
-
- }
- />
- }
- />
- {
- setFeatureStaleDialogState({});
- onChange();
- }}
- featureId={featureStaleDialogState.featureId || ''}
- projectId={projectId}
- />
- {
- setFeatureArchiveState(undefined);
- }}
- featureIds={[featureArchiveState || '']}
- projectId={projectId}
- />
- setShowExportDialog(false)}
- environments={environments}
- />
- }
- />
- {featureToggleModals}
-
-
- toggleAllRowsSelected(false)}
- onChange={onChange}
- />
-
- >
- );
-};
diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx
index 1b78820794..9857543ebd 100644
--- a/frontend/src/component/project/Project/ProjectOverview.tsx
+++ b/frontend/src/component/project/Project/ProjectOverview.tsx
@@ -1,18 +1,14 @@
import { FC, useEffect } from 'react';
-import useProject, {
- useProjectNameOrId,
-} from 'hooks/api/getters/useProject/useProject';
import { Box, styled } from '@mui/material';
-import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles';
import ProjectInfo from './ProjectInfo/ProjectInfo';
-import { usePageTitle } from 'hooks/usePageTitle';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
-import { useLastViewedProject } from 'hooks/useLastViewedProject';
import { ProjectStats } from './ProjectStats/ProjectStats';
-import { useUiFlag } from 'hooks/useUiFlag';
-import { PaginatedProjectFeatureToggles } from './PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles';
-import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
-import { type FeatureTypeCount } from '../../../interfaces/project';
+import { ProjectFeatureToggles } from './PaginatedProjectFeatureToggles/ProjectFeatureToggles';
+import useProjectOverview, {
+ useProjectOverviewNameOrId,
+} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
+import { usePageTitle } from 'hooks/usePageTitle';
+import { useLastViewedProject } from 'hooks/useLastViewedProject';
const refreshInterval = 15 * 1000;
@@ -37,13 +33,19 @@ const StyledContentContainer = styled(Box)(({ theme }) => ({
minWidth: 0,
}));
-const PaginatedProjectOverview: FC<{
+const ProjectOverview: FC<{
storageKey?: string;
}> = ({ storageKey = 'project-overview-v2' }) => {
const projectId = useRequiredPathParam('projectId');
+ const projectName = useProjectOverviewNameOrId(projectId);
const { project } = useProjectOverview(projectId, {
refreshInterval,
});
+ usePageTitle(`Project overview – ${projectName}`);
+ const { setLastViewed } = useLastViewedProject();
+ useEffect(() => {
+ setLastViewed(projectId);
+ }, [projectId, setLastViewed]);
const {
members,
@@ -67,7 +69,7 @@ const PaginatedProjectOverview: FC<{
- {
- const projectId = useRequiredPathParam('projectId');
- const projectName = useProjectNameOrId(projectId);
- const { project, loading, refetch } = useProject(projectId, {
- refreshInterval,
- });
- const { members, features, health, description, environments, stats } =
- project;
- usePageTitle(`Project overview – ${projectName}`);
- const { setLastViewed } = useLastViewedProject();
- const featureSearchFrontend = useUiFlag('featureSearchFrontend');
-
- useEffect(() => {
- setLastViewed(projectId);
- }, [projectId, setLastViewed]);
-
- if (featureSearchFrontend) return ;
-
- const featureTypeCounts = features.reduce(
- (acc: FeatureTypeCount[], feature) => {
- const existingEntry = acc.find(
- (entry) => entry.type === feature.type,
- );
- if (existingEntry) {
- existingEntry.count += 1;
- } else {
- acc.push({ type: feature.type, count: 1 });
- }
- return acc;
- },
- [],
- );
-
- return (
-
-
-
-
-
-
-
-
-
- );
-};
-
export default ProjectOverview;
diff --git a/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts b/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts
index 9a29d66071..eda954d196 100644
--- a/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts
+++ b/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts
@@ -32,6 +32,7 @@ export const useExecutiveDashboard = (
flags: { total: 0 },
userTrends: [],
flagTrends: [],
+ projectFlagTrends: [],
},
refetchExecutiveDashboard,
loading: !error && !data,
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 4e008270ba..d696dd3658 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -65,7 +65,6 @@ export type UiFlags = {
dependentFeatures?: boolean;
scheduledConfigurationChanges?: boolean;
featureSearchAPI?: boolean;
- featureSearchFrontend?: boolean;
newStrategyConfiguration?: boolean;
incomingWebhooks?: boolean;
automatedActions?: boolean;
diff --git a/frontend/src/openapi/models/executiveSummarySchema.ts b/frontend/src/openapi/models/executiveSummarySchema.ts
index fec4155686..65dcad10bc 100644
--- a/frontend/src/openapi/models/executiveSummarySchema.ts
+++ b/frontend/src/openapi/models/executiveSummarySchema.ts
@@ -5,6 +5,7 @@
*/
import type { ExecutiveSummarySchemaFlags } from './executiveSummarySchemaFlags';
import type { ExecutiveSummarySchemaFlagTrendsItem } from './executiveSummarySchemaFlagTrendsItem';
+import type { ExecutiveSummarySchemaProjectFlagTrendsItem } from './executiveSummarySchemaProjectFlagTrendsItem';
import type { ExecutiveSummarySchemaUsers } from './executiveSummarySchemaUsers';
import type { ExecutiveSummarySchemaUserTrendsItem } from './executiveSummarySchemaUserTrendsItem';
@@ -16,6 +17,8 @@ export interface ExecutiveSummarySchema {
flags: ExecutiveSummarySchemaFlags;
/** How number of flags changed over time */
flagTrends: ExecutiveSummarySchemaFlagTrendsItem[];
+ /** How number of flags per project changed over time */
+ projectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[];
/** High level user count statistics */
users: ExecutiveSummarySchemaUsers;
/** How number of users changed over time */
diff --git a/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts b/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts
index 72de2a7849..a3e8360041 100644
--- a/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts
+++ b/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts
@@ -10,7 +10,7 @@ export type ExecutiveSummarySchemaFlagTrendsItem = {
/** A UTC date when the stats were captured. Time is the very end of a given day. */
date: string;
/** The number of time calculated potentially stale flags on a particular day */
- potentiallyStale?: number;
+ potentiallyStale: number;
/** The number of user marked stale flags on a particular day */
stale: number;
/** The number of all flags on a particular day */
diff --git a/frontend/src/openapi/models/executiveSummarySchemaProjectFlagTrendsItem.ts b/frontend/src/openapi/models/executiveSummarySchemaProjectFlagTrendsItem.ts
new file mode 100644
index 0000000000..75fde6c2fe
--- /dev/null
+++ b/frontend/src/openapi/models/executiveSummarySchemaProjectFlagTrendsItem.ts
@@ -0,0 +1,24 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+
+export type ExecutiveSummarySchemaProjectFlagTrendsItem = {
+ /** The number of active flags on a particular day */
+ active: number;
+ /** A UTC date when the stats were captured. Time is the very end of a given day. */
+ date: string;
+ /** An indicator of the [project's health](https://docs.getunleash.io/reference/technical-debt#health-rating) on a scale from 0 to 100 */
+ health?: number;
+ /** The number of time calculated potentially stale flags on a particular day */
+ potentiallyStale: number;
+ /** Project id of the project the flag trends belong to */
+ project: string;
+ /** The number of user marked stale flags on a particular day */
+ stale: number;
+ /** The average time from when a feature was created to when it was enabled in the "production" environment during the current window */
+ timeToProduction?: number;
+ /** The number of all flags on a particular day */
+ total: number;
+};
diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts
index 3732a99d7e..55b9ed1edf 100644
--- a/frontend/src/openapi/models/index.ts
+++ b/frontend/src/openapi/models/index.ts
@@ -497,6 +497,7 @@ export * from './eventsSchemaVersion';
export * from './executiveSummarySchema';
export * from './executiveSummarySchemaFlagTrendsItem';
export * from './executiveSummarySchemaFlags';
+export * from './executiveSummarySchemaProjectFlagTrendsItem';
export * from './executiveSummarySchemaUserTrendsItem';
export * from './executiveSummarySchemaUsers';
export * from './exportFeatures404';
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index 7ea030febc..f819e3f03f 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -95,10 +95,9 @@ exports[`should create default config 1`] = `
"executiveDashboard": false,
"extendedUsageMetrics": false,
"extendedUsageMetricsUI": false,
- "featureSearchAPI": false,
+ "featureSearchAPI": true,
"featureSearchFeedback": false,
"featureSearchFeedbackPosting": false,
- "featureSearchFrontend": false,
"featuresExportImport": true,
"feedbackComments": {
"enabled": false,
diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts
index d00990d142..a84182cbf9 100644
--- a/src/lib/types/experimental.ts
+++ b/src/lib/types/experimental.ts
@@ -28,7 +28,6 @@ export type IFlagKey =
| 'customRootRolesKillSwitch'
| 'disableMetrics'
| 'featureSearchAPI'
- | 'featureSearchFrontend'
| 'scheduledConfigurationChanges'
| 'detectSegmentUsageInChangeRequests'
| 'stripClientHeadersOn304'
@@ -133,11 +132,7 @@ const flags: IFlags = {
),
featureSearchAPI: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
- false,
- ),
- featureSearchFrontend: parseEnvVarBoolean(
- process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_FRONTEND,
- false,
+ true,
),
scheduledConfigurationChanges: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_SCHEDULED_CONFIGURATION_CHANGES,
diff --git a/src/server-dev.ts b/src/server-dev.ts
index 9625c4b9d9..f9972953f2 100644
--- a/src/server-dev.ts
+++ b/src/server-dev.ts
@@ -41,7 +41,6 @@ process.nextTick(async () => {
anonymiseEventLog: false,
responseTimeWithAppNameKillSwitch: false,
featureSearchAPI: true,
- featureSearchFrontend: true,
stripClientHeadersOn304: true,
newStrategyConfiguration: true,
stripHeadersOnAPI: true,