1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

feat: added PoC for the new feature creation dialog (#7666)

![image](https://github.com/user-attachments/assets/82412746-34b3-48f2-95b1-1cdfdeb1ea72)
This commit is contained in:
Jaanus Sellin 2024-07-25 15:12:48 +03:00 committed by GitHub
parent eb7208025f
commit 10489c7534
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 366 additions and 15 deletions

View File

@ -0,0 +1,315 @@
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 } from 'react';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useNavigate } from 'react-router-dom';
import { Dialog, styled } from '@mui/material';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
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';
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 allows you to group feature flags together in the management UI.',
},
type: {
icon: <FlagIcon />,
text: "A flag's type conveys its purpose.",
},
impressionData: {
icon: <ImpressionDataIcon />,
text: 'Impression data is used to track how your flag is performing.',
},
};
export const CreateFeatureDialog = ({
open,
onClose,
}: ICreateFeatureDialogProps) => {
const { setToastData, setToastApiError } = useToast();
const { setShowFeedback } = useContext(UIContext);
const { uiConfig } = useUiConfig();
const navigate = useNavigate();
const {
type,
setType,
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 aria-hidden='true' />,
text: 'Feature flags are the core of Unleash.',
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();
const { project: projectInfo } = useProjectOverview(project);
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,
);
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={{
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={
<>
<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: project,
icon: configButtonData.project.icon,
labelWidth: '12ch',
}}
search={{
label: 'Filter projects',
placeholder: 'Select project',
}}
onOpen={() =>
setDocumentation(configButtonData.project)
}
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}
/>
<SingleSelectConfigButton
tooltip={{
header: 'Enable or disable impression data',
}}
description={
configButtonData.impressionData.text
}
options={[
{ label: 'On', value: 'true' },
{ label: 'Off', value: 'false' },
]}
onChange={(value: string) => {
setImpressionData(value === 'true');
}}
button={{
label: `Impression data ${impressionData ? 'on' : 'off'}`,
icon: <ImpressionDataIcon />,
labelWidth: `${'impression data off'.length}ch`,
}}
search={{
label: 'Filter impression data states',
placeholder: 'Select impression data state',
}}
onOpen={() =>
setDocumentation(
configButtonData.impressionData,
)
}
onClose={clearDocumentationOverride}
/>
</>
}
/>
</FormTemplate>
</StyledDialog>
);
};

View File

@ -16,7 +16,7 @@ import Add from '@mui/icons-material/Add';
import FileDownload from '@mui/icons-material/FileDownload';
import { styled } from '@mui/material';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { getCreateTogglePath } from 'utils/routePathHelpers';
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
@ -24,7 +24,9 @@ import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import type { FeatureSchema } from 'openapi';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import ReviewsOutlined from '@mui/icons-material/ReviewsOutlined';
import { useFeedback } from '../../../../feedbackNew/useFeedback';
import { useFeedback } from 'component/feedbackNew/useFeedback';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { CreateFeatureDialog } from './CreateFeatureDialog';
interface IProjectFeatureTogglesHeaderProps {
isLoading?: boolean;
@ -40,6 +42,52 @@ const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap',
}));
const FlagCreationButton: FC = () => {
const [searchParams] = useSearchParams();
const projectId = useRequiredPathParam('projectId');
const showCreateDialog = Boolean(searchParams.get('create'));
const [openCreateDialog, setOpenCreateDialog] = useState(showCreateDialog);
const { loading } = useUiConfig();
const navigate = useNavigate();
const improveCreateFlagFlow = useUiFlag('improveCreateFlagFlow');
return (
<ConditionallyRender
condition={improveCreateFlagFlow}
show={
<>
<StyledResponsiveButton
onClick={() => setOpenCreateDialog(true)}
maxWidth='960px'
Icon={Add}
disabled={loading}
permission={CREATE_FEATURE}
data-testid='NAVIGATE_TO_CREATE_FEATURE'
>
New feature flag
</StyledResponsiveButton>
<CreateFeatureDialog
open={openCreateDialog}
onClose={() => setOpenCreateDialog(false)}
/>
</>
}
elseShow={
<StyledResponsiveButton
onClick={() => navigate(getCreateTogglePath(projectId))}
maxWidth='960px'
Icon={Add}
projectId={projectId}
permission={CREATE_FEATURE}
data-testid='NAVIGATE_TO_CREATE_FEATURE'
>
New feature flag
</StyledResponsiveButton>
}
/>
);
};
export const ProjectFeatureTogglesHeader: FC<
IProjectFeatureTogglesHeaderProps
> = ({
@ -57,7 +105,6 @@ export const ProjectFeatureTogglesHeader: FC<
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const featuresExportImportFlag = useUiFlag('featuresExportImport');
const [showExportDialog, setShowExportDialog] = useState(false);
const navigate = useNavigate();
const { trackEvent } = usePlausibleTracker();
const projectOverviewRefactorFeedback = useUiFlag(
'projectOverviewRefactorFeedback',
@ -178,18 +225,7 @@ export const ProjectFeatureTogglesHeader: FC<
</Button>
}
/>
<StyledResponsiveButton
onClick={() =>
navigate(getCreateTogglePath(projectId))
}
maxWidth='960px'
Icon={Add}
projectId={projectId}
permission={CREATE_FEATURE}
data-testid='NAVIGATE_TO_CREATE_FEATURE'
>
New feature flag
</StyledResponsiveButton>
<FlagCreationButton />
</>
}
>