diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
index 79d83140e4..98cb316fba 100644
--- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
+++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
@@ -4,12 +4,10 @@ import { PageContent } from 'component/common/PageContent/PageContent';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
-import type { IProject } from 'interfaces/project';
import { PaginatedTable } from 'component/common/Table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
-import type { ProjectEnvironmentType } from '../ProjectFeatureToggles/hooks/useEnvironmentsRef';
import { ActionsCell } from '../ProjectFeatureToggles/ActionsCell/ActionsCell';
import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
@@ -53,7 +51,7 @@ import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/Feat
import { useSelectedData } from './hooks/useSelectedData';
interface IPaginatedProjectFeatureTogglesProps {
- environments: IProject['environments'];
+ environments: string[];
refreshInterval?: number;
storageKey?: string;
}
@@ -226,63 +224,59 @@ export const ProjectFeatureToggles = ({
header: 'Created',
cell: DateCell,
}),
- ...environments.map(
- (projectEnvironment: ProjectEnvironmentType) => {
- const name = projectEnvironment.environment;
- const isChangeRequestEnabled =
- isChangeRequestConfigured(name);
+ ...environments.map((name: string) => {
+ const isChangeRequestEnabled = isChangeRequestConfigured(name);
- return columnHelper.accessor(
- (row) => ({
- featureId: row.name,
- environment: row.environments?.find(
+ return columnHelper.accessor(
+ (row) => ({
+ featureId: row.name,
+ environment: row.environments?.find(
+ (featureEnvironment) =>
+ featureEnvironment.name === name,
+ ),
+ someEnabledEnvironmentHasVariants:
+ row.environments?.some(
(featureEnvironment) =>
- featureEnvironment.name === name,
- ),
- someEnabledEnvironmentHasVariants:
- row.environments?.some(
- (featureEnvironment) =>
- featureEnvironment.variantCount &&
- featureEnvironment.variantCount > 0 &&
- featureEnvironment.enabled,
- ) || false,
- }),
- {
- id: formatEnvironmentColumnId(name),
- header: name,
- meta: {
- align: 'center',
- width: 90,
- },
- cell: ({ getValue }) => {
- const {
- featureId,
- environment,
- someEnabledEnvironmentHasVariants,
- } = getValue();
-
- return (
-
- );
- },
+ featureEnvironment.variantCount &&
+ featureEnvironment.variantCount > 0 &&
+ featureEnvironment.enabled,
+ ) || false,
+ }),
+ {
+ id: formatEnvironmentColumnId(name),
+ header: name,
+ meta: {
+ align: 'center',
+ width: 90,
},
- );
- },
- ),
+ cell: ({ getValue }) => {
+ const {
+ featureId,
+ environment,
+ someEnabledEnvironmentHasVariants,
+ } = getValue();
+
+ return (
+
+ );
+ },
+ },
+ );
+ }),
columnHelper.display({
id: 'actions',
header: '',
@@ -391,9 +385,7 @@ export const ProjectFeatureToggles = ({
setTableState({ query });
}}
dataToExport={data}
- environmentsToExport={environments.map(
- ({ environment }) => environment,
- )}
+ environmentsToExport={environments}
actions={
({
+ ...environments.map((environment) => ({
header: environment,
id: formatEnvironmentColumnId(
environment,
@@ -478,9 +470,7 @@ export const ProjectFeatureToggles = ({
showExportDialog={showExportDialog}
data={data}
onClose={() => setShowExportDialog(false)}
- environments={environments.map(
- ({ environment }) => environment,
- )}
+ environments={environments}
/>
}
/>
diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx
index bea86baf84..0fb14ce56d 100644
--- a/frontend/src/component/project/Project/ProjectOverview.tsx
+++ b/frontend/src/component/project/Project/ProjectOverview.tsx
@@ -10,7 +10,7 @@ import useProjectOverview, {
import { usePageTitle } from 'hooks/usePageTitle';
import { useLastViewedProject } from 'hooks/useLastViewedProject';
import { useUiFlag } from 'hooks/useUiFlag';
-import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { ProjectOverviewChangeRequests } from './ProjectOverviewChangeRequests';
const refreshInterval = 15 * 1000;
@@ -39,6 +39,17 @@ const ProjectOverview: FC<{
storageKey?: string;
}> = ({ storageKey = 'project-overview-v2' }) => {
const projectOverviewRefactor = useUiFlag('projectOverviewRefactor');
+
+ if (projectOverviewRefactor) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+const OldProjectOverview: FC<{
+ storageKey?: string;
+}> = ({ storageKey = 'project-overview-v2' }) => {
const projectId = useRequiredPathParam('projectId');
const projectName = useProjectOverviewNameOrId(projectId);
const { project } = useProjectOverview(projectId, {
@@ -61,29 +72,58 @@ const ProjectOverview: FC<{
return (
-
- }
+
- }
- />
+
environment.environment,
+ )}
+ refreshInterval={refreshInterval}
+ storageKey={storageKey}
+ />
+
+
+
+ );
+};
+
+const NewProjectOverview: 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]);
+
+ return (
+
+
+
+
+
+ environment.environment,
+ )}
refreshInterval={refreshInterval}
storageKey={storageKey}
/>
diff --git a/frontend/src/component/project/Project/ProjectOverviewChangeRequests.test.tsx b/frontend/src/component/project/Project/ProjectOverviewChangeRequests.test.tsx
new file mode 100644
index 0000000000..93e3d3df2f
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectOverviewChangeRequests.test.tsx
@@ -0,0 +1,45 @@
+import { screen } from '@testing-library/react';
+import { render } from 'utils/testRenderer';
+import { testServerRoute, testServerSetup } from 'utils/testServer';
+import { ProjectOverviewChangeRequests } from './ProjectOverviewChangeRequests';
+
+const server = testServerSetup();
+
+const setupEnterpriseApi = () => {
+ testServerRoute(server, '/api/admin/ui-config', {
+ versionInfo: {
+ current: { enterprise: 'present' },
+ },
+ });
+ testServerRoute(
+ server,
+ '/api/admin/projects/default/change-requests/config',
+ [
+ {
+ environment: 'default',
+ changeRequestEnabled: true,
+ },
+ ],
+ );
+ testServerRoute(
+ server,
+ '/api/admin/projects/default/change-requests/count',
+ {
+ total: 14,
+ approved: 2,
+ applied: 0,
+ rejected: 0,
+ reviewRequired: 10,
+ scheduled: 2,
+ },
+ );
+};
+
+test('Show change requests count', async () => {
+ setupEnterpriseApi();
+ render();
+
+ await screen.findByText('4');
+ await screen.findByText('10');
+ await screen.findByText('View change requests');
+});
diff --git a/frontend/src/component/project/Project/ProjectOverviewChangeRequests.tsx b/frontend/src/component/project/Project/ProjectOverviewChangeRequests.tsx
index fa6182d6d5..4c97bd3c4d 100644
--- a/frontend/src/component/project/Project/ProjectOverviewChangeRequests.tsx
+++ b/frontend/src/component/project/Project/ProjectOverviewChangeRequests.tsx
@@ -1,6 +1,8 @@
import { Box, styled, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
import type { FC } from 'react';
+import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
+import { useChangeRequestsCount } from 'hooks/api/getters/useChangeRequestsCount/useChangeRequestsCount';
export const ChangeRequestContainer = styled(Box)(({ theme }) => ({
margin: '0',
@@ -42,16 +44,27 @@ const ChangeRequestCount = styled(Typography)(({ theme }) => ({
export const ProjectOverviewChangeRequests: FC<{ project: string }> = ({
project,
}) => {
+ const { isChangeRequestConfiguredInAnyEnv } =
+ useChangeRequestsEnabled(project);
+ const { data } = useChangeRequestsCount(project);
+
+ if (!isChangeRequestConfiguredInAnyEnv) {
+ return null;
+ }
+
+ const toBeApplied = data.scheduled + data.approved;
+ const toBeReviewed = data.reviewRequired;
+
return (
Open change requests
To be applied
- 10
+ {toBeApplied}
To be reviewed
- 20
+ {toBeReviewed}
View change requests
diff --git a/frontend/src/hooks/api/getters/useChangeRequestsCount/useChangeRequestsCount.ts b/frontend/src/hooks/api/getters/useChangeRequestsCount/useChangeRequestsCount.ts
new file mode 100644
index 0000000000..525fde6f7a
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useChangeRequestsCount/useChangeRequestsCount.ts
@@ -0,0 +1,39 @@
+import { formatApiPath } from 'utils/formatPath';
+import handleErrorResponses from '../httpErrorResponseHandler';
+import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
+import useUiConfig from '../useUiConfig/useUiConfig';
+import type { ChangeRequestsCountSchema } from '../../../../openapi';
+
+const fallback: ChangeRequestsCountSchema = {
+ applied: 0,
+ approved: 0,
+ rejected: 0,
+ scheduled: 0,
+ reviewRequired: 0,
+ total: 0,
+};
+
+export const useChangeRequestsCount = (projectId: string) => {
+ const { isEnterprise } = useUiConfig();
+ const { data, error, mutate } =
+ useConditionalSWR(
+ Boolean(projectId) && isEnterprise(),
+ fallback,
+ formatApiPath(
+ `api/admin/projects/${projectId}/change-requests/count`,
+ ),
+ fetcher,
+ );
+ return {
+ data: data || fallback,
+ loading: !error && !data,
+ refetchChangeRequestConfig: mutate,
+ error,
+ };
+};
+
+const fetcher = (path: string) => {
+ return fetch(path)
+ .then(handleErrorResponses('Request changes'))
+ .then((res) => res.json());
+};
diff --git a/frontend/src/openapi/models/changeRequestsCountSchema.ts b/frontend/src/openapi/models/changeRequestsCountSchema.ts
new file mode 100644
index 0000000000..51c99477d8
--- /dev/null
+++ b/frontend/src/openapi/models/changeRequestsCountSchema.ts
@@ -0,0 +1,23 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+
+/**
+ * Count of change requests in different stages of the [process](https://docs.getunleash.io/reference/change-requests#change-request-flow).
+ */
+export interface ChangeRequestsCountSchema {
+ /** The number of applied change requests */
+ applied: number;
+ /** The number of approved change requests */
+ approved: number;
+ /** The number of rejected change requests */
+ rejected: number;
+ /** The number of change requests awaiting the review */
+ reviewRequired: number;
+ /** The number of scheduled change requests */
+ scheduled: number;
+ /** The number of total change requests in this project */
+ total: number;
+}
diff --git a/frontend/src/openapi/models/featureSearchEnvironmentSchema.ts b/frontend/src/openapi/models/featureSearchEnvironmentSchema.ts
new file mode 100644
index 0000000000..4a085a0be4
--- /dev/null
+++ b/frontend/src/openapi/models/featureSearchEnvironmentSchema.ts
@@ -0,0 +1,41 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+import type { FeatureStrategySchema } from './featureStrategySchema';
+import type { VariantSchema } from './variantSchema';
+
+/**
+ * A detailed description of the feature environment
+ */
+export interface FeatureSearchEnvironmentSchema {
+ /** `true` if the feature is enabled for the environment, otherwise `false`. */
+ enabled: boolean;
+ /** The name of the environment */
+ environment?: string;
+ /** The name of the feature */
+ featureName?: string;
+ /** Whether the feature has any enabled strategies defined. */
+ hasEnabledStrategies?: boolean;
+ /** Whether the feature has any strategies defined. */
+ hasStrategies?: boolean;
+ /** The date when metrics where last collected for the feature environment */
+ lastSeenAt?: string | null;
+ /** The name of the environment */
+ name: string;
+ /** How many times the toggle evaluated to false in last hour bucket */
+ no?: number;
+ /** The sort order of the feature environment in the feature environments list */
+ sortOrder?: number;
+ /** A list of activation strategies for the feature environment */
+ strategies?: FeatureStrategySchema[];
+ /** The type of the environment */
+ type?: string;
+ /** The number of defined variants */
+ variantCount?: number;
+ /** A list of variants for the feature environment */
+ variants?: VariantSchema[];
+ /** How many times the toggle evaluated to true in last hour bucket */
+ yes?: number;
+}
diff --git a/frontend/src/openapi/models/featureSearchResponseSchema.ts b/frontend/src/openapi/models/featureSearchResponseSchema.ts
index d11d5ac826..9b45f9e132 100644
--- a/frontend/src/openapi/models/featureSearchResponseSchema.ts
+++ b/frontend/src/openapi/models/featureSearchResponseSchema.ts
@@ -4,7 +4,7 @@
* See `gen:api` script in package.json
*/
import type { FeatureSearchResponseSchemaDependenciesItem } from './featureSearchResponseSchemaDependenciesItem';
-import type { FeatureEnvironmentSchema } from './featureEnvironmentSchema';
+import type { FeatureSearchEnvironmentSchema } from './featureSearchEnvironmentSchema';
import type { FeatureSearchResponseSchemaStrategiesItem } from './featureSearchResponseSchemaStrategiesItem';
import type { TagSchema } from './tagSchema';
import type { VariantSchema } from './variantSchema';
@@ -28,7 +28,7 @@ export interface FeatureSearchResponseSchema {
/** `true` if the feature is enabled, otherwise `false`. */
enabled?: boolean;
/** The list of environments where the feature can be used */
- environments?: FeatureEnvironmentSchema[];
+ environments?: FeatureSearchEnvironmentSchema[];
/** `true` if the feature was favorited, otherwise `false`. */
favorite?: boolean;
/** `true` if the impression data collection is enabled for the feature, otherwise `false`. */
diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts
index 98adb64b0f..0e91fcaff1 100644
--- a/frontend/src/openapi/models/index.ts
+++ b/frontend/src/openapi/models/index.ts
@@ -271,6 +271,7 @@ export * from './changeRequestStateSchemaOneOfState';
export * from './changeRequestStateSchemaOneOfThree';
export * from './changeRequestStateSchemaOneOfThreeState';
export * from './changeRequestUpdateTitleSchema';
+export * from './changeRequestsCountSchema';
export * from './changeRequestsSchema';
export * from './changeRoleForGroup401';
export * from './changeRoleForGroup403';
@@ -538,6 +539,7 @@ export * from './featureMetricsSchema';
export * from './featureSchema';
export * from './featureSchemaDependenciesItem';
export * from './featureSchemaStrategiesItem';
+export * from './featureSearchEnvironmentSchema';
export * from './featureSearchResponseSchema';
export * from './featureSearchResponseSchemaDependenciesItem';
export * from './featureSearchResponseSchemaStrategiesItem';