mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
There's a bug where the UI will fetch all features every time you load a project screen (including every time you filter the project results). The reason is that the create flag dialog was rendered (just not open) every time. To solve it, we instead wrap it in an extra component that prevents all the fetching and setup from running when the dialog isn't open. Additionally, we'll lower the page size for the global fetch limit to 1, so that we send less data.
372 lines
15 KiB
TypeScript
372 lines
15 KiB
TypeScript
import { formatUnknownError } from 'utils/formatUnknownError';
|
|
import useToast from 'hooks/useToast';
|
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
|
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
|
import {
|
|
type ReactNode,
|
|
useState,
|
|
useContext,
|
|
type FormEvent,
|
|
useMemo,
|
|
} from 'react';
|
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Dialog, styled } from '@mui/material';
|
|
import { useUiFlag } from 'hooks/useUiFlag';
|
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
|
import { Limit } from 'component/common/Limit/Limit';
|
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
import UIContext from 'contexts/UIContext';
|
|
import useFeatureForm from 'component/feature/hooks/useFeatureForm';
|
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
|
import FlagIcon from '@mui/icons-material/Flag';
|
|
import ImpressionDataIcon from '@mui/icons-material/AltRoute';
|
|
import { useFlagLimits } from 'component/feature/CreateFeature/CreateFeature';
|
|
import { useGlobalFeatureSearch } from 'component/feature/FeatureToggleList/useGlobalFeatureSearch';
|
|
import useProjectOverview, {
|
|
featuresCount,
|
|
} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
|
import type { FeatureTypeSchema } from 'openapi';
|
|
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
|
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
|
import { DialogFormTemplate } from 'component/common/DialogFormTemplate/DialogFormTemplate';
|
|
import { SingleSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton';
|
|
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
|
|
import Label from '@mui/icons-material/Label';
|
|
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
|
import { MultiSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton';
|
|
import type { ITag } from 'interfaces/tags';
|
|
import { ToggleConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/ToggleConfigButton';
|
|
|
|
interface ICreateFeatureDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const StyledDialog = styled(Dialog)(({ theme }) => ({
|
|
'& .MuiDialog-paper': {
|
|
borderRadius: theme.shape.borderRadiusLarge,
|
|
maxWidth: theme.spacing(170),
|
|
width: '100%',
|
|
backgroundColor: 'transparent',
|
|
},
|
|
padding: 0,
|
|
'& .MuiPaper-root > section': {
|
|
overflowX: 'hidden',
|
|
},
|
|
}));
|
|
|
|
const configButtonData = {
|
|
project: {
|
|
icon: <ProjectIcon />,
|
|
text: 'Projects allow you to group feature flags together in the Unleash admin UI and in SDK payloads.',
|
|
},
|
|
tags: {
|
|
icon: <Label />,
|
|
text: 'Tags are used to label flags in Unleash. They can be used when filtering flags in the UI. Additionally, they are used by some integrations.',
|
|
},
|
|
type: {
|
|
icon: <FlagIcon />,
|
|
text: "A flag's type conveys its purpose. All types have the same capabilities, but choosing the right type signals what kind of flag it is. You can change this at any time.",
|
|
},
|
|
|
|
impressionData: {
|
|
icon: <ImpressionDataIcon />,
|
|
text: `Impression data is used to track how your flag is performing. When enabled, you can subscribe to 'impression events' in the SDK and process them according to your needs.`,
|
|
},
|
|
};
|
|
|
|
export const CreateFeatureDialog = ({
|
|
open,
|
|
onClose,
|
|
}: ICreateFeatureDialogProps) => {
|
|
if (open) {
|
|
// wrap the inner component so that we only fetch data etc
|
|
// when the dialog is actually open.
|
|
return <CreateFeatureDialogContent open={open} onClose={onClose} />;
|
|
}
|
|
};
|
|
|
|
const CreateFeatureDialogContent = ({
|
|
open,
|
|
onClose,
|
|
}: ICreateFeatureDialogProps) => {
|
|
const { setToastData, setToastApiError } = useToast();
|
|
const { setShowFeedback } = useContext(UIContext);
|
|
const { uiConfig, isOss } = useUiConfig();
|
|
const navigate = useNavigate();
|
|
|
|
const {
|
|
type,
|
|
setType,
|
|
tags,
|
|
setTags,
|
|
name,
|
|
setName,
|
|
project,
|
|
setProject,
|
|
description,
|
|
setDescription,
|
|
validateToggleName,
|
|
impressionData,
|
|
setImpressionData,
|
|
getTogglePayload,
|
|
clearErrors,
|
|
errors,
|
|
} = useFeatureForm();
|
|
const { createFeatureToggle, loading } = useFeatureApi();
|
|
|
|
const generalDocumentation: {
|
|
icon: ReactNode;
|
|
text: string;
|
|
link?: { url: string; label: string };
|
|
} = {
|
|
icon: <FlagIcon />,
|
|
text: 'Feature flags are at the core of Unleash. Use them to control your feature rollouts.',
|
|
link: {
|
|
url: 'https://docs.getunleash.io/reference/feature-toggles',
|
|
label: 'Feature flags documentation',
|
|
},
|
|
};
|
|
|
|
const [documentation, setDocumentation] = useState(generalDocumentation);
|
|
|
|
const clearDocumentationOverride = () =>
|
|
setDocumentation(generalDocumentation);
|
|
|
|
const flagPayload = getTogglePayload();
|
|
|
|
const formatApiCode = () => {
|
|
return `curl --location --request POST '${
|
|
uiConfig.unleashUrl
|
|
}/api/admin/projects/${project}/features' \\
|
|
--header 'Authorization: INSERT_API_KEY' \\
|
|
--header 'Content-Type: application/json' \\
|
|
--data-raw '${JSON.stringify(flagPayload, undefined, 2)}'`;
|
|
};
|
|
|
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
clearErrors();
|
|
const validToggleName = await validateToggleName();
|
|
|
|
if (validToggleName) {
|
|
const payload = getTogglePayload();
|
|
try {
|
|
await createFeatureToggle(project, payload);
|
|
navigate(`/projects/${project}/features/${name}`);
|
|
setToastData({
|
|
title: 'Flag created successfully',
|
|
text: 'Now you can start using your flag.',
|
|
confetti: true,
|
|
type: 'success',
|
|
});
|
|
setShowFeedback(true);
|
|
} catch (error: unknown) {
|
|
setToastApiError(formatUnknownError(error));
|
|
}
|
|
}
|
|
};
|
|
|
|
const { total: totalFlags, loading: loadingTotalFlagCount } =
|
|
useGlobalFeatureSearch(1);
|
|
|
|
const { project: projectInfo } = useProjectOverview(project);
|
|
const { tags: allTags } = useAllTags();
|
|
|
|
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
|
|
|
const { globalFlagLimitReached, projectFlagLimitReached, limitMessage } =
|
|
useFlagLimits({
|
|
global: {
|
|
limit: uiConfig.resourceLimits.featureFlags,
|
|
count: totalFlags ?? 0,
|
|
},
|
|
project: {
|
|
limit: projectInfo.featureLimit,
|
|
count: featuresCount(projectInfo),
|
|
},
|
|
});
|
|
|
|
const { projects } = useProjects();
|
|
const { featureTypes } = useFeatureTypes();
|
|
const FeatureTypeIcon = getFeatureTypeIcons(type);
|
|
|
|
const longestFeatureTypeName = featureTypes.reduce(
|
|
(prev: number, type: { name: string }) =>
|
|
prev >= type.name.length ? prev : type.name.length,
|
|
0,
|
|
);
|
|
|
|
const currentProjectName = useMemo(() => {
|
|
const projectObject = projects.find((p) => p.id === project);
|
|
return projectObject?.name;
|
|
}, [project, projects]);
|
|
|
|
return (
|
|
<StyledDialog open={open} onClose={onClose}>
|
|
<FormTemplate
|
|
compact
|
|
disablePadding
|
|
description={documentation.text}
|
|
documentationIcon={documentation.icon}
|
|
documentationLink={documentation.link?.url}
|
|
documentationLinkLabel={documentation.link?.label}
|
|
formatApiCode={formatApiCode}
|
|
useFixedSidebar
|
|
>
|
|
<DialogFormTemplate
|
|
createButtonProps={{
|
|
projectId: project,
|
|
disabled:
|
|
loading ||
|
|
loadingTotalFlagCount ||
|
|
globalFlagLimitReached ||
|
|
projectFlagLimitReached,
|
|
permission: CREATE_FEATURE,
|
|
tooltipProps: { title: limitMessage, arrow: true },
|
|
}}
|
|
description={description}
|
|
errors={errors}
|
|
handleSubmit={handleSubmit}
|
|
Icon={<FlagIcon />}
|
|
validateName={validateToggleName}
|
|
Limit={
|
|
<ConditionallyRender
|
|
condition={resourceLimitsEnabled}
|
|
show={
|
|
<Limit
|
|
name='feature flags'
|
|
limit={uiConfig.resourceLimits.featureFlags}
|
|
currentValue={totalFlags ?? 0}
|
|
/>
|
|
}
|
|
/>
|
|
}
|
|
name={name}
|
|
onClose={onClose}
|
|
resource={'feature flag'}
|
|
setDescription={setDescription}
|
|
setName={setName}
|
|
configButtons={
|
|
<>
|
|
<ConditionallyRender
|
|
condition={!isOss()}
|
|
show={
|
|
<SingleSelectConfigButton
|
|
tooltip={{
|
|
header: 'Select a project for the flag',
|
|
}}
|
|
description={
|
|
configButtonData.project.text
|
|
}
|
|
options={projects.map((project) => ({
|
|
label: project.name,
|
|
value: project.id,
|
|
}))}
|
|
onChange={(value: any) => {
|
|
setProject(value);
|
|
}}
|
|
button={{
|
|
label:
|
|
currentProjectName ?? project,
|
|
icon: configButtonData.project.icon,
|
|
labelWidth: '20ch',
|
|
}}
|
|
search={{
|
|
label: 'Filter projects',
|
|
placeholder: 'Select project',
|
|
}}
|
|
onOpen={() =>
|
|
setDocumentation(
|
|
configButtonData.project,
|
|
)
|
|
}
|
|
onClose={clearDocumentationOverride}
|
|
/>
|
|
}
|
|
/>
|
|
<MultiSelectConfigButton<ITag>
|
|
tooltip={{
|
|
header: 'Select tags',
|
|
}}
|
|
description={configButtonData.tags.text}
|
|
selectedOptions={tags}
|
|
options={allTags.map((tag) => ({
|
|
label: `${tag.type}:${tag.value}`,
|
|
value: tag,
|
|
}))}
|
|
onChange={setTags}
|
|
button={{
|
|
label:
|
|
tags.size > 0
|
|
? `${tags.size} selected`
|
|
: 'Tags',
|
|
labelWidth: `${'nn selected'.length}ch`,
|
|
icon: <Label />,
|
|
}}
|
|
search={{
|
|
label: 'Filter tags',
|
|
placeholder: 'Select tags',
|
|
}}
|
|
onOpen={() =>
|
|
setDocumentation(configButtonData.tags)
|
|
}
|
|
onClose={clearDocumentationOverride}
|
|
/>
|
|
<SingleSelectConfigButton
|
|
tooltip={{
|
|
header: 'Select a flag type',
|
|
}}
|
|
description={configButtonData.type.text}
|
|
options={featureTypes.map(
|
|
(type: FeatureTypeSchema) => ({
|
|
label: type.name,
|
|
value: type.id,
|
|
}),
|
|
)}
|
|
onChange={(value: any) => {
|
|
setType(value);
|
|
}}
|
|
button={{
|
|
label:
|
|
featureTypes.find((t) => t.id === type)
|
|
?.name || 'Select flag type',
|
|
icon: <FeatureTypeIcon />,
|
|
labelWidth: `${longestFeatureTypeName}ch`,
|
|
}}
|
|
search={{
|
|
label: 'Filter flag types',
|
|
placeholder: 'Select flag type',
|
|
}}
|
|
onOpen={() =>
|
|
setDocumentation({
|
|
text: configButtonData.type.text,
|
|
icon: <FeatureTypeIcon />,
|
|
})
|
|
}
|
|
onClose={clearDocumentationOverride}
|
|
/>
|
|
|
|
<ToggleConfigButton
|
|
tooltip={{
|
|
header: 'Enable or disable impression data',
|
|
description:
|
|
configButtonData.impressionData.text,
|
|
}}
|
|
currentValue={impressionData}
|
|
onClick={() =>
|
|
setImpressionData(!impressionData)
|
|
}
|
|
label={`Impression data ${impressionData ? 'on' : 'off'}`}
|
|
icon={<ImpressionDataIcon />}
|
|
labelWidth={`${'impression data off'.length}ch`}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
</FormTemplate>
|
|
</StyledDialog>
|
|
);
|
|
};
|