mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
poc: many strategies pagination (#7011)
This fixes the case when a customer have thousands of strategies causing the react UI to crash. We still consider it incorrect to use that amount of strategies and this is more a workaround to help the customer out of a crashing state. We put it behind a flag called `manyStrategiesPagination` and plan to only enable it for the customer in trouble.
This commit is contained in:
parent
cd49ae2a26
commit
64c10f9eff
@ -4,7 +4,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Alert, styled } from '@mui/material';
|
import { Alert, Pagination, styled } from '@mui/material';
|
||||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
@ -17,6 +17,11 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
|||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
import usePagination from 'hooks/usePagination';
|
||||||
|
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
import { StrategyNonDraggableItem } from './StrategyDraggableItem/StrategyNonDraggableItem';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
interface IEnvironmentAccordionBodyProps {
|
interface IEnvironmentAccordionBodyProps {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
@ -50,9 +55,12 @@ const EnvironmentAccordionBody = ({
|
|||||||
usePendingChangeRequests(projectId);
|
usePendingChangeRequests(projectId);
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { refetchFeature } = useFeature(projectId, featureId);
|
const { refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
const manyStrategiesPagination = useUiFlag('manyStrategiesPagination');
|
||||||
const [strategies, setStrategies] = useState(
|
const [strategies, setStrategies] = useState(
|
||||||
featureEnvironment?.strategies || [],
|
featureEnvironment?.strategies || [],
|
||||||
);
|
);
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
const [dragItem, setDragItem] = useState<{
|
const [dragItem, setDragItem] = useState<{
|
||||||
id: string;
|
id: string;
|
||||||
index: number;
|
index: number;
|
||||||
@ -63,10 +71,20 @@ const EnvironmentAccordionBody = ({
|
|||||||
setStrategies(featureEnvironment?.strategies || []);
|
setStrategies(featureEnvironment?.strategies || []);
|
||||||
}, [featureEnvironment?.strategies]);
|
}, [featureEnvironment?.strategies]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (strategies.length > 50) {
|
||||||
|
trackEvent('many-strategies');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!featureEnvironment) {
|
if (!featureEnvironment) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pageSize = 20;
|
||||||
|
const { page, pages, setPageIndex, pageIndex } =
|
||||||
|
usePagination<IFeatureStrategy>(strategies, pageSize);
|
||||||
|
|
||||||
const onReorder = async (payload: { id: string; sortOrder: number }[]) => {
|
const onReorder = async (payload: { id: string; sortOrder: number }[]) => {
|
||||||
try {
|
try {
|
||||||
await setStrategiesSortOrder(
|
await setStrategiesSortOrder(
|
||||||
@ -195,21 +213,68 @@ const EnvironmentAccordionBody = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategies.length > 0}
|
condition={strategies.length > 0}
|
||||||
show={
|
show={
|
||||||
<>
|
<ConditionallyRender
|
||||||
{strategies.map((strategy, index) => (
|
condition={
|
||||||
<StrategyDraggableItem
|
strategies.length < 50 ||
|
||||||
key={strategy.id}
|
!manyStrategiesPagination
|
||||||
strategy={strategy}
|
}
|
||||||
index={index}
|
show={
|
||||||
environmentName={featureEnvironment.name}
|
<>
|
||||||
otherEnvironments={otherEnvironments}
|
{strategies.map((strategy, index) => (
|
||||||
isDragging={dragItem?.id === strategy.id}
|
<StrategyDraggableItem
|
||||||
onDragStartRef={onDragStartRef}
|
key={strategy.id}
|
||||||
onDragOver={onDragOver(strategy.id)}
|
strategy={strategy}
|
||||||
onDragEnd={onDragEnd}
|
index={index}
|
||||||
/>
|
environmentName={
|
||||||
))}
|
featureEnvironment.name
|
||||||
</>
|
}
|
||||||
|
otherEnvironments={
|
||||||
|
otherEnvironments
|
||||||
|
}
|
||||||
|
isDragging={
|
||||||
|
dragItem?.id === strategy.id
|
||||||
|
}
|
||||||
|
onDragStartRef={onDragStartRef}
|
||||||
|
onDragOver={onDragOver(strategy.id)}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<Alert severity='error'>
|
||||||
|
We noticed you're using a high number of
|
||||||
|
activation strategies. To ensure a more
|
||||||
|
targeted approach, consider leveraging
|
||||||
|
constraints or segments.
|
||||||
|
</Alert>
|
||||||
|
<br />
|
||||||
|
{page.map((strategy, index) => (
|
||||||
|
<StrategyNonDraggableItem
|
||||||
|
key={strategy.id}
|
||||||
|
strategy={strategy}
|
||||||
|
index={index + pageIndex * pageSize}
|
||||||
|
environmentName={
|
||||||
|
featureEnvironment.name
|
||||||
|
}
|
||||||
|
otherEnvironments={
|
||||||
|
otherEnvironments
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<br />
|
||||||
|
<Pagination
|
||||||
|
count={pages.length}
|
||||||
|
shape='rounded'
|
||||||
|
page={pageIndex + 1}
|
||||||
|
onChange={(_, page) =>
|
||||||
|
setPageIndex(page - 1)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<FeatureStrategyEmpty
|
<FeatureStrategyEmpty
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
|
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
|
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
import { StrategyItem } from './StrategyItem/StrategyItem';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import {
|
||||||
|
useStrategyChangesFromRequest,
|
||||||
|
type UseStrategyChangeFromRequestResult,
|
||||||
|
} from './StrategyItem/useStrategyChangesFromRequest';
|
||||||
|
import { ChangesScheduledBadge } from 'component/changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge';
|
||||||
|
import type { IFeatureChange } from 'component/changeRequest/changeRequest.types';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import {
|
||||||
|
type ScheduledChangeRequestViewModel,
|
||||||
|
useScheduledChangeRequestsWithStrategy,
|
||||||
|
} from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
|
||||||
|
|
||||||
|
interface IStrategyItemProps {
|
||||||
|
strategy: IFeatureStrategy;
|
||||||
|
environmentName: string;
|
||||||
|
index: number;
|
||||||
|
otherEnvironments?: IFeatureEnvironment['name'][];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
export const StrategyNonDraggableItem = ({
|
||||||
|
strategy,
|
||||||
|
index,
|
||||||
|
environmentName,
|
||||||
|
otherEnvironments,
|
||||||
|
}: IStrategyItemProps) => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const featureId = useRequiredPathParam('featureId');
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const strategyChangesFromRequest = useStrategyChangesFromRequest(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentName,
|
||||||
|
strategy.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { changeRequests: scheduledChangesUsingStrategy } =
|
||||||
|
useScheduledChangeRequestsWithStrategy(projectId, strategy.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={strategy.id} ref={ref} sx={{ opacity: '1' }}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={index > 0}
|
||||||
|
show={<StrategySeparator text='OR' />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StrategyItem
|
||||||
|
strategy={strategy}
|
||||||
|
environmentId={environmentName}
|
||||||
|
otherEnvironments={otherEnvironments}
|
||||||
|
orderNumber={index + 1}
|
||||||
|
headerChildren={renderHeaderChildren(
|
||||||
|
strategyChangesFromRequest,
|
||||||
|
scheduledChangesUsingStrategy,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeRequestStatusBadge = ({
|
||||||
|
change,
|
||||||
|
}: {
|
||||||
|
change: IFeatureChange | undefined;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mr: 1.5 }}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={change?.action === 'updateStrategy'}
|
||||||
|
show={<Badge color='warning'>Modified in draft</Badge>}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={change?.action === 'deleteStrategy'}
|
||||||
|
show={<Badge color='error'>Deleted in draft</Badge>}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHeaderChildren = (
|
||||||
|
changes?: UseStrategyChangeFromRequestResult,
|
||||||
|
scheduledChanges?: ScheduledChangeRequestViewModel[],
|
||||||
|
): JSX.Element[] => {
|
||||||
|
const badges: JSX.Element[] = [];
|
||||||
|
if (changes?.length === 0 && scheduledChanges?.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftChange = changes?.find(
|
||||||
|
({ isScheduledChange }) => !isScheduledChange,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (draftChange) {
|
||||||
|
badges.push(
|
||||||
|
<ChangeRequestStatusBadge
|
||||||
|
key={`draft-change#${draftChange.change.id}`}
|
||||||
|
change={draftChange.change}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledChanges && scheduledChanges.length > 0) {
|
||||||
|
badges.push(
|
||||||
|
<ChangesScheduledBadge
|
||||||
|
key='scheduled-changes'
|
||||||
|
scheduledChangeRequestIds={scheduledChanges.map(
|
||||||
|
(scheduledChange) => scheduledChange.id,
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return badges;
|
||||||
|
};
|
@ -20,7 +20,7 @@ const usePagination = <T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = paginate(dataToPaginate, limit);
|
const result = paginate(dataToPaginate, limit);
|
||||||
setPageIndex(0);
|
// setPageIndex(0);
|
||||||
setPaginatedData(result);
|
setPaginatedData(result);
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [JSON.stringify(data), limit]);
|
}, [JSON.stringify(data), limit]);
|
||||||
|
@ -59,6 +59,7 @@ export type CustomEvents =
|
|||||||
| 'search-bar'
|
| 'search-bar'
|
||||||
| 'sdk-reporting'
|
| 'sdk-reporting'
|
||||||
| 'insights-share'
|
| 'insights-share'
|
||||||
|
| 'many-strategies'
|
||||||
| 'sdk-banner';
|
| 'sdk-banner';
|
||||||
|
|
||||||
export const usePlausibleTracker = () => {
|
export const usePlausibleTracker = () => {
|
||||||
|
@ -84,6 +84,7 @@ export type UiFlags = {
|
|||||||
createProjectWithEnvironmentConfig?: boolean;
|
createProjectWithEnvironmentConfig?: boolean;
|
||||||
projectsListNewCards?: boolean;
|
projectsListNewCards?: boolean;
|
||||||
newCreateProjectUI?: boolean;
|
newCreateProjectUI?: boolean;
|
||||||
|
manyStrategiesPagination?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -122,6 +122,7 @@ exports[`should create default config 1`] = `
|
|||||||
"googleAuthEnabled": false,
|
"googleAuthEnabled": false,
|
||||||
"killScheduledChangeRequestCache": false,
|
"killScheduledChangeRequestCache": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
|
"manyStrategiesPagination": false,
|
||||||
"messageBanner": {
|
"messageBanner": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"name": "message-banner",
|
"name": "message-banner",
|
||||||
|
@ -59,6 +59,7 @@ export type IFlagKey =
|
|||||||
| 'projectsListNewCards'
|
| 'projectsListNewCards'
|
||||||
| 'parseProjectFromSession'
|
| 'parseProjectFromSession'
|
||||||
| 'createProjectWithEnvironmentConfig'
|
| 'createProjectWithEnvironmentConfig'
|
||||||
|
| 'manyStrategiesPagination'
|
||||||
| 'newCreateProjectUI';
|
| 'newCreateProjectUI';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
@ -284,6 +285,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_NEW_CREATE_PROJECT_UI,
|
process.env.UNLEASH_EXPERIMENTAL_NEW_CREATE_PROJECT_UI,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
manyStrategiesPagination: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_MANY_STRATEGIES_PAGINATION,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -54,6 +54,7 @@ process.nextTick(async () => {
|
|||||||
projectsListNewCards: true,
|
projectsListNewCards: true,
|
||||||
parseProjectFromSession: true,
|
parseProjectFromSession: true,
|
||||||
createProjectWithEnvironmentConfig: true,
|
createProjectWithEnvironmentConfig: true,
|
||||||
|
manyStrategiesPagination: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user