1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-28 17:55:15 +02:00
unleash.unleash/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm.tsx
Thomas Heartman 90d6c7c0ba
chore: remove usage of feature naming pattern flag (#5364)
In preparation for this feature going GA
2023-11-20 12:42:24 +01:00

359 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Select from 'component/common/select';
import { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
import { Box, InputAdornment, styled, TextField } from '@mui/material';
import { CollaborationModeTooltip } from './CollaborationModeTooltip';
import Input from 'component/common/Input/Input';
import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag';
interface IProjectEnterpriseSettingsForm {
projectId: string;
projectMode?: string;
featureNamingPattern?: string;
featureNamingExample?: string;
featureNamingDescription?: string;
setFeatureNamingPattern?: React.Dispatch<React.SetStateAction<string>>;
setFeatureNamingExample?: React.Dispatch<React.SetStateAction<string>>;
setFeatureNamingDescription?: React.Dispatch<React.SetStateAction<string>>;
setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>;
handleSubmit: (e: any) => void;
errors: { [key: string]: string };
clearErrors: () => void;
}
const StyledForm = styled('form')(({ theme }) => ({
height: '100%',
paddingBottom: theme.spacing(4),
}));
const StyledSubtitle = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallerBody,
lineHeight: 1.25,
paddingBottom: theme.spacing(1),
}));
const StyledInput = styled(Input)(({ theme }) => ({
width: '100%',
marginBottom: theme.spacing(2),
paddingRight: theme.spacing(1),
}));
const StyledTextField = styled(TextField)(({ theme }) => ({
width: '100%',
marginBottom: theme.spacing(2),
}));
const StyledFieldset = styled('fieldset')(() => ({
padding: 0,
border: 'none',
}));
const StyledSelect = styled(Select)(({ theme }) => ({
marginBottom: theme.spacing(2),
minWidth: '200px',
}));
const StyledButtonContainer = styled('div')(() => ({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
}));
const StyledFlagNamingContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: theme.spacing(1),
'& > *': { width: '100%' },
}));
const StyledPatternNamingExplanation = styled('div')(({ theme }) => ({
'p + p': { marginTop: theme.spacing(1) },
}));
export const validateFeatureNamingExample = ({
pattern,
example,
featureNamingPatternError,
}: {
pattern: string;
example: string;
featureNamingPatternError: string | undefined;
}): { state: 'valid' } | { state: 'invalid'; reason: string } => {
if (featureNamingPatternError || !example || !pattern) {
return { state: 'valid' };
} else if (example && pattern) {
const regex = new RegExp(`^${pattern}$`);
const matches = regex.test(example);
if (!matches) {
return { state: 'invalid', reason: 'Example does not match regex' };
} else {
return { state: 'valid' };
}
}
return { state: 'valid' };
};
const useFeatureNamePatternTracking = () => {
const [previousPattern, setPreviousPattern] = React.useState<string>('');
const { trackEvent } = usePlausibleTracker();
const eventName = 'feature-naming-pattern' as const;
const trackPattern = (pattern: string = '') => {
if (pattern === previousPattern) {
// do nothing; they've probably updated something else in the
// project.
} else if (pattern === '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: 'removed' } });
} else if (pattern !== '' && previousPattern === '') {
trackEvent(eventName, { props: { action: 'added' } });
} else if (pattern !== '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: 'edited' } });
}
};
return { trackPattern, setPreviousPattern };
};
const ProjectEnterpriseSettingsForm: React.FC<IProjectEnterpriseSettingsForm> =
({
children,
handleSubmit,
projectId,
projectMode,
featureNamingExample,
featureNamingPattern,
featureNamingDescription,
setFeatureNamingExample,
setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectMode,
errors,
clearErrors,
}) => {
const privateProjects = useUiFlag('privateProjects');
const { setPreviousPattern, trackPattern } =
useFeatureNamePatternTracking();
const projectModeOptions = privateProjects
? [
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
{ key: 'private', label: 'private' },
]
: [
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
];
useEffect(() => {
setPreviousPattern(featureNamingPattern || '');
}, [projectId]);
const updateNamingExampleError = ({
example,
pattern,
}: {
example: string;
pattern: string;
}) => {
const validationResult = validateFeatureNamingExample({
pattern,
example,
featureNamingPatternError: errors.featureNamingPattern,
});
switch (validationResult.state) {
case 'invalid':
errors.namingExample = validationResult.reason;
break;
case 'valid':
delete errors.namingExample;
break;
}
};
const onSetFeatureNamingPattern = (regex: string) => {
const disallowedStrings = [
' ',
'\\t',
'\\s',
'\\n',
'\\r',
'\\f',
'\\v',
];
if (
disallowedStrings.some((blockedString) =>
regex.includes(blockedString),
)
) {
errors.featureNamingPattern =
'Whitespace is not allowed in the expression';
} else {
try {
new RegExp(regex);
delete errors.featureNamingPattern;
} catch (e) {
errors.featureNamingPattern = 'Invalid regular expression';
}
}
setFeatureNamingPattern?.(regex);
updateNamingExampleError({
pattern: regex,
example: featureNamingExample || '',
});
};
const onSetFeatureNamingExample = (example: string) => {
setFeatureNamingExample?.(example);
updateNamingExampleError({
pattern: featureNamingPattern || '',
example,
});
};
const onSetFeatureNamingDescription = (description: string) => {
setFeatureNamingDescription?.(description);
};
return (
<StyledForm
onSubmit={(submitEvent) => {
handleSubmit(submitEvent);
trackPattern(featureNamingPattern);
}}
>
<>
<Box
sx={{
display: 'flex',
alignItems: 'center',
marginBottom: 1,
gap: 1,
}}
>
<p>What is your project collaboration mode?</p>
<CollaborationModeTooltip />
</Box>
<StyledSelect
id='project-mode'
value={projectMode}
label='Project collaboration mode'
name='Project collaboration mode'
onChange={(e) => {
setProjectMode?.(e.target.value as ProjectMode);
}}
options={projectModeOptions}
/>
</>
<StyledFieldset>
<Box
sx={{
display: 'flex',
alignItems: 'center',
marginBottom: 1,
gap: 1,
}}
>
<legend>Feature flag naming pattern?</legend>
<FeatureFlagNamingTooltip />
</Box>
<StyledSubtitle>
<StyledPatternNamingExplanation id='pattern-naming-description'>
<p>
Define a{' '}
<a
href={`https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Cheatsheet`}
target='_blank'
rel='noreferrer'
>
JavaScript RegEx
</a>{' '}
used to enforce feature flag names within this
project. The regex will be surrounded by a
leading <code>^</code> and a trailing{' '}
<code>$</code>.
</p>
<p>
Leave it empty if you dont want to add a naming
pattern.
</p>
</StyledPatternNamingExplanation>
</StyledSubtitle>
<StyledFlagNamingContainer>
<StyledInput
label={'Naming Pattern'}
name='feature flag naming pattern'
aria-describedby='pattern-naming-description'
placeholder='[A-Za-z]+.[A-Za-z]+.[A-Za-z0-9-]+'
InputProps={{
startAdornment: (
<InputAdornment position='start'>
^
</InputAdornment>
),
endAdornment: (
<InputAdornment position='end'>
$
</InputAdornment>
),
}}
type={'text'}
value={featureNamingPattern || ''}
error={Boolean(errors.featureNamingPattern)}
errorText={errors.featureNamingPattern}
onChange={(e) =>
onSetFeatureNamingPattern(e.target.value)
}
/>
<StyledSubtitle>
<p id='pattern-additional-description'>
The example and description will be shown to
users when they create a new feature flag in
this project.
</p>
</StyledSubtitle>
<StyledInput
label={'Naming Example'}
name='feature flag naming example'
type={'text'}
aria-describedby='pattern-additional-description'
value={featureNamingExample || ''}
placeholder='dx.feature1.1-135'
error={Boolean(errors.namingExample)}
errorText={errors.namingExample}
onChange={(e) =>
onSetFeatureNamingExample(e.target.value)
}
/>
<StyledTextField
label={'Naming pattern description'}
name='feature flag naming description'
type={'text'}
aria-describedby='pattern-additional-description'
placeholder={`<project>.<featureName>.<ticket>
The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`}
multiline
minRows={5}
value={featureNamingDescription || ''}
onChange={(e) =>
onSetFeatureNamingDescription(e.target.value)
}
/>
</StyledFlagNamingContainer>
</StyledFieldset>
<StyledButtonContainer>{children}</StyledButtonContainer>
</StyledForm>
);
};
export default ProjectEnterpriseSettingsForm;