mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +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,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Alert, styled } from '@mui/material';
|
||||
import { Alert, Pagination, styled } from '@mui/material';
|
||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
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 { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
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 {
|
||||
isDisabled: boolean;
|
||||
@ -50,9 +55,12 @@ const EnvironmentAccordionBody = ({
|
||||
usePendingChangeRequests(projectId);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchFeature } = useFeature(projectId, featureId);
|
||||
const manyStrategiesPagination = useUiFlag('manyStrategiesPagination');
|
||||
const [strategies, setStrategies] = useState(
|
||||
featureEnvironment?.strategies || [],
|
||||
);
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
|
||||
const [dragItem, setDragItem] = useState<{
|
||||
id: string;
|
||||
index: number;
|
||||
@ -63,10 +71,20 @@ const EnvironmentAccordionBody = ({
|
||||
setStrategies(featureEnvironment?.strategies || []);
|
||||
}, [featureEnvironment?.strategies]);
|
||||
|
||||
useEffect(() => {
|
||||
if (strategies.length > 50) {
|
||||
trackEvent('many-strategies');
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!featureEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pageSize = 20;
|
||||
const { page, pages, setPageIndex, pageIndex } =
|
||||
usePagination<IFeatureStrategy>(strategies, pageSize);
|
||||
|
||||
const onReorder = async (payload: { id: string; sortOrder: number }[]) => {
|
||||
try {
|
||||
await setStrategiesSortOrder(
|
||||
@ -195,21 +213,68 @@ const EnvironmentAccordionBody = ({
|
||||
<ConditionallyRender
|
||||
condition={strategies.length > 0}
|
||||
show={
|
||||
<>
|
||||
{strategies.map((strategy, index) => (
|
||||
<StrategyDraggableItem
|
||||
key={strategy.id}
|
||||
strategy={strategy}
|
||||
index={index}
|
||||
environmentName={featureEnvironment.name}
|
||||
otherEnvironments={otherEnvironments}
|
||||
isDragging={dragItem?.id === strategy.id}
|
||||
onDragStartRef={onDragStartRef}
|
||||
onDragOver={onDragOver(strategy.id)}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
strategies.length < 50 ||
|
||||
!manyStrategiesPagination
|
||||
}
|
||||
show={
|
||||
<>
|
||||
{strategies.map((strategy, index) => (
|
||||
<StrategyDraggableItem
|
||||
key={strategy.id}
|
||||
strategy={strategy}
|
||||
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={
|
||||
<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);
|
||||
setPageIndex(0);
|
||||
// setPageIndex(0);
|
||||
setPaginatedData(result);
|
||||
/* eslint-disable-next-line */
|
||||
}, [JSON.stringify(data), limit]);
|
||||
|
@ -59,6 +59,7 @@ export type CustomEvents =
|
||||
| 'search-bar'
|
||||
| 'sdk-reporting'
|
||||
| 'insights-share'
|
||||
| 'many-strategies'
|
||||
| 'sdk-banner';
|
||||
|
||||
export const usePlausibleTracker = () => {
|
||||
|
@ -84,6 +84,7 @@ export type UiFlags = {
|
||||
createProjectWithEnvironmentConfig?: boolean;
|
||||
projectsListNewCards?: boolean;
|
||||
newCreateProjectUI?: boolean;
|
||||
manyStrategiesPagination?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -122,6 +122,7 @@ exports[`should create default config 1`] = `
|
||||
"googleAuthEnabled": false,
|
||||
"killScheduledChangeRequestCache": false,
|
||||
"maintenanceMode": false,
|
||||
"manyStrategiesPagination": false,
|
||||
"messageBanner": {
|
||||
"enabled": false,
|
||||
"name": "message-banner",
|
||||
|
@ -59,6 +59,7 @@ export type IFlagKey =
|
||||
| 'projectsListNewCards'
|
||||
| 'parseProjectFromSession'
|
||||
| 'createProjectWithEnvironmentConfig'
|
||||
| 'manyStrategiesPagination'
|
||||
| 'newCreateProjectUI';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
@ -284,6 +285,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_NEW_CREATE_PROJECT_UI,
|
||||
false,
|
||||
),
|
||||
manyStrategiesPagination: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_MANY_STRATEGIES_PAGINATION,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -54,6 +54,7 @@ process.nextTick(async () => {
|
||||
projectsListNewCards: true,
|
||||
parseProjectFromSession: true,
|
||||
createProjectWithEnvironmentConfig: true,
|
||||
manyStrategiesPagination: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user