1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-10-28 19:06:12 +01:00

feat: fetch change request overview in project overview (#6683)

This commit is contained in:
Mateusz Kwasniewski 2024-03-25 14:02:06 +01:00 committed by GitHub
parent 9ecd81ebb4
commit 501da974d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 279 additions and 86 deletions

View File

@ -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 (
<FeatureToggleCell
value={environment?.enabled || false}
featureId={featureId}
someEnabledEnvironmentHasVariants={
someEnabledEnvironmentHasVariants
}
environment={environment}
projectId={projectId}
environmentName={name}
isChangeRequestEnabled={
isChangeRequestEnabled
}
refetch={refetch}
onFeatureToggleSwitch={onFeatureToggle}
/>
);
},
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 (
<FeatureToggleCell
value={environment?.enabled || false}
featureId={featureId}
someEnabledEnvironmentHasVariants={
someEnabledEnvironmentHasVariants
}
environment={environment}
projectId={projectId}
environmentName={name}
isChangeRequestEnabled={
isChangeRequestEnabled
}
refetch={refetch}
onFeatureToggleSwitch={onFeatureToggle}
/>
);
},
},
);
}),
columnHelper.display({
id: 'actions',
header: '',
@ -391,9 +385,7 @@ export const ProjectFeatureToggles = ({
setTableState({ query });
}}
dataToExport={data}
environmentsToExport={environments.map(
({ environment }) => environment,
)}
environmentsToExport={environments}
actions={
<ColumnsMenu
columns={[
@ -426,7 +418,7 @@ export const ProjectFeatureToggles = ({
{
id: 'divider',
},
...environments.map(({ environment }) => ({
...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}
/>
}
/>

View File

@ -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 <NewProjectOverview storageKey={storageKey} />;
} else {
return <OldProjectOverview storageKey={storageKey} />;
}
};
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 (
<StyledContainer key={projectId}>
<ConditionallyRender
condition={!projectOverviewRefactor}
show={
<ProjectInfo
id={projectId}
description={description}
memberCount={members}
health={health}
featureTypeCounts={featureTypeCounts}
stats={stats}
/>
}
<ProjectInfo
id={projectId}
description={description}
memberCount={members}
health={health}
featureTypeCounts={featureTypeCounts}
stats={stats}
/>
<StyledContentContainer>
<ConditionallyRender
condition={!projectOverviewRefactor}
show={<ProjectStats stats={project.stats} />}
/>
<ProjectStats stats={project.stats} />
<StyledProjectToggles>
<ProjectFeatureToggles
environments={environments}
environments={environments.map(
(environment) => environment.environment,
)}
refreshInterval={refreshInterval}
storageKey={storageKey}
/>
</StyledProjectToggles>
</StyledContentContainer>
</StyledContainer>
);
};
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 (
<StyledContainer key={projectId}>
<StyledContentContainer>
<ProjectOverviewChangeRequests project={projectId} />
<StyledProjectToggles>
<ProjectFeatureToggles
environments={project.environments.map(
(environment) => environment.environment,
)}
refreshInterval={refreshInterval}
storageKey={storageKey}
/>

View File

@ -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(<ProjectOverviewChangeRequests project='default' />);
await screen.findByText('4');
await screen.findByText('10');
await screen.findByText('View change requests');
});

View File

@ -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 (
<ChangeRequestContainer>
<Box>Open change requests</Box>
<ApplyBox>
<span>To be applied</span>
<ChangeRequestCount>10</ChangeRequestCount>
<ChangeRequestCount>{toBeApplied}</ChangeRequestCount>
</ApplyBox>
<ReviewBox>
<span>To be reviewed</span>
<ChangeRequestCount>20</ChangeRequestCount>
<ChangeRequestCount>{toBeReviewed}</ChangeRequestCount>
</ReviewBox>
<Link to={`/projects/${project}/change-requests`}>
View change requests

View File

@ -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<ChangeRequestsCountSchema>(
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());
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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`. */

View File

@ -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';