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:
parent
9ecd81ebb4
commit
501da974d6
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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');
|
||||
});
|
@ -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
|
||||
|
@ -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());
|
||||
};
|
23
frontend/src/openapi/models/changeRequestsCountSchema.ts
Normal file
23
frontend/src/openapi/models/changeRequestsCountSchema.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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`. */
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user