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)

This commit is contained in:
parent
eb7208025f
commit
10489c7534
@ -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>
|
||||
);
|
||||
};
|
@ -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 />
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user