1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-10 01:19:53 +01:00
unleash.unleash/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx
Thomas Heartman b2c58102dd
chore(unl-204): remove uses of toast text and confetti (#8941)
As of PR #8935, we no longer support both text and title, and confetti
has been removed.

This PR:
- removes `confetti` from the toast interface
- merges `text` and `title` into `text` and updates its uses across the
codebase.
- readjusts the text where necessary.
2024-12-10 13:38:04 +00:00

374 lines
16 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, 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 useProjects from 'hooks/api/getters/useProjects/useProjects';
import { Limit } from 'component/common/Limit/Limit';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
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 { 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';
import { useFlagLimits } from './useFlagLimits';
import { useFeatureCreatedFeedback } from './hooks/useFeatureCreatedFeedback';
interface ICreateFeatureDialogProps {
open: boolean;
onClose: () => void;
skipNavigationOnComplete?: boolean;
onSuccess?: () => 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,
onSuccess,
skipNavigationOnComplete,
}: 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}
skipNavigationOnComplete={skipNavigationOnComplete}
onSuccess={onSuccess}
/>
);
}
};
const CreateFeatureDialogContent = ({
open,
onClose,
skipNavigationOnComplete,
onSuccess,
}: ICreateFeatureDialogProps) => {
const { setToastData, setToastApiError } = useToast();
const { uiConfig, isOss } = useUiConfig();
const navigate = useNavigate();
const openFeatureCreatedFeedback = useFeatureCreatedFeedback();
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);
if (!skipNavigationOnComplete) {
navigate(`/projects/${project}/features/${name}`);
}
setToastData({
text: 'Flag created successfully',
type: 'success',
});
onClose();
onSuccess?.();
openFeatureCreatedFeedback();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}
};
const { total: totalFlags, loading: loadingTotalFlagCount } =
useGlobalFeatureSearch(1);
const { project: projectInfo } = useProjectOverview(project);
const { tags: allTags } = useAllTags();
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}
namingPattern={projectInfo.featureNaming}
errors={errors}
handleSubmit={handleSubmit}
Icon={<FlagIcon />}
validateName={validateToggleName}
Limit={
<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>
);
};