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

1-1319: add feature naming pattern descriptions (#4612)

This PR adds a feature naming pattern description to the project form.
It's rendered as a multi-line input field. The description is also
stored in the db.

This adapts most of @andreas-unleash's PR #4599 with some minor changes
(using description instead of prompt). Actually displaying this data to
the users will come in a later PR.


![image](https://github.com/Unleash/unleash/assets/17786332/b96d2dbb-2b90-4adf-bc83-cdc534c507ea)
This commit is contained in:
Thomas Heartman 2023-09-06 10:13:28 +02:00 committed by GitHub
parent 31df85a3f5
commit 73b7cc0b5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 116 additions and 28 deletions

View File

@ -35,8 +35,10 @@ const CreateProject = () => {
featureLimit, featureLimit,
featureNamingPattern, featureNamingPattern,
featureNamingExample, featureNamingExample,
featureNamingDescription,
setFeatureNamingExample, setFeatureNamingExample,
setFeatureNamingPattern, setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
@ -114,7 +116,9 @@ const CreateProject = () => {
featureLimit={featureLimit} featureLimit={featureLimit}
featureNamingExample={featureNamingExample} featureNamingExample={featureNamingExample}
featureNamingPattern={featureNamingPattern} featureNamingPattern={featureNamingPattern}
setProjectNamingPattern={setFeatureNamingPattern} setFeatureNamingPattern={setFeatureNamingPattern}
featureNamingDescription={featureNamingDescription}
setFeatureNamingDescription={setFeatureNamingDescription}
setFeatureNamingExample={setFeatureNamingExample} setFeatureNamingExample={setFeatureNamingExample}
setProjectStickiness={setProjectStickiness} setProjectStickiness={setProjectStickiness}
setFeatureLimit={setFeatureLimit} setFeatureLimit={setFeatureLimit}

View File

@ -43,6 +43,7 @@ const EditProject = () => {
projectMode, projectMode,
featureNamingPattern, featureNamingPattern,
featureNamingExample, featureNamingExample,
featureNamingDescription,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
@ -50,6 +51,7 @@ const EditProject = () => {
setProjectMode, setProjectMode,
setFeatureNamingExample, setFeatureNamingExample,
setFeatureNamingPattern, setFeatureNamingPattern,
setFeatureNamingDescription,
getProjectPayload, getProjectPayload,
clearErrors, clearErrors,
validateProjectId, validateProjectId,
@ -63,7 +65,8 @@ const EditProject = () => {
project.mode, project.mode,
String(project.featureLimit), String(project.featureLimit),
project?.featureNaming?.pattern || '', project?.featureNaming?.pattern || '',
project?.featureNaming?.example || '' project?.featureNaming?.example || '',
project?.featureNaming?.description || ''
); );
const formatApiCode = () => { const formatApiCode = () => {
@ -131,13 +134,15 @@ const EditProject = () => {
projectMode={projectMode} projectMode={projectMode}
featureNamingPattern={featureNamingPattern} featureNamingPattern={featureNamingPattern}
featureNamingExample={featureNamingExample} featureNamingExample={featureNamingExample}
featureNamingDescription={featureNamingDescription}
setProjectName={setProjectName} setProjectName={setProjectName}
projectStickiness={projectStickiness} projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness} setProjectStickiness={setProjectStickiness}
setProjectMode={setProjectMode} setProjectMode={setProjectMode}
setFeatureLimit={() => {}} setFeatureLimit={() => {}}
setFeatureNamingExample={setFeatureNamingExample} setFeatureNamingExample={setFeatureNamingExample}
setProjectNamingPattern={setFeatureNamingPattern} setFeatureNamingPattern={setFeatureNamingPattern}
setFeatureNamingDescription={setFeatureNamingDescription}
featureLimit={''} featureLimit={''}
projectDesc={projectDesc} projectDesc={projectDesc}
setProjectDesc={setProjectDesc} setProjectDesc={setProjectDesc}

View File

@ -21,8 +21,10 @@ interface IProjectForm {
featureCount?: number; featureCount?: number;
featureNamingPattern?: string; featureNamingPattern?: string;
featureNamingExample?: string; featureNamingExample?: string;
setProjectNamingPattern?: React.Dispatch<React.SetStateAction<string>>; featureNamingDescription?: string;
setFeatureNamingPattern?: React.Dispatch<React.SetStateAction<string>>;
setFeatureNamingExample?: React.Dispatch<React.SetStateAction<string>>; setFeatureNamingExample?: React.Dispatch<React.SetStateAction<string>>;
setFeatureNamingDescription?: React.Dispatch<React.SetStateAction<string>>;
setProjectStickiness?: React.Dispatch<React.SetStateAction<string>>; setProjectStickiness?: React.Dispatch<React.SetStateAction<string>>;
setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>; setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>;
setProjectId: React.Dispatch<React.SetStateAction<string>>; setProjectId: React.Dispatch<React.SetStateAction<string>>;
@ -100,7 +102,7 @@ const StyledFlagNamingContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
mt: theme.spacing(1), gap: theme.spacing(1),
'& > *': { width: '100%' }, '& > *': { width: '100%' },
})); }));
@ -116,8 +118,10 @@ const ProjectForm: React.FC<IProjectForm> = ({
featureCount, featureCount,
featureNamingExample, featureNamingExample,
featureNamingPattern, featureNamingPattern,
featureNamingDescription,
setFeatureNamingExample, setFeatureNamingExample,
setProjectNamingPattern, setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
@ -134,11 +138,11 @@ const ProjectForm: React.FC<IProjectForm> = ({
const onSetFeatureNamingPattern = (regex: string) => { const onSetFeatureNamingPattern = (regex: string) => {
try { try {
new RegExp(regex); new RegExp(regex);
setProjectNamingPattern && setProjectNamingPattern(regex); setFeatureNamingPattern && setFeatureNamingPattern(regex);
clearErrors(); clearErrors();
} catch (e) { } catch (e) {
errors.featureNamingPattern = 'Invalid regular expression'; errors.featureNamingPattern = 'Invalid regular expression';
setProjectNamingPattern && setProjectNamingPattern(regex); setFeatureNamingPattern && setFeatureNamingPattern(regex);
} }
}; };
@ -151,10 +155,14 @@ const ProjectForm: React.FC<IProjectForm> = ({
} else { } else {
delete errors.namingExample; delete errors.namingExample;
} }
setFeatureNamingExample && setFeatureNamingExample(trim(example)); setFeatureNamingExample && setFeatureNamingExample(example);
} }
}; };
const onSetFeatureNamingDescription = (description: string) => {
setFeatureNamingDescription && setFeatureNamingDescription(description);
};
return ( return (
<StyledForm onSubmit={handleSubmit}> <StyledForm onSubmit={handleSubmit}>
<StyledContainer> <StyledContainer>
@ -283,11 +291,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
</StyledInputContainer> </StyledInputContainer>
</> </>
<ConditionallyRender <ConditionallyRender
condition={ condition={Boolean(shouldShowFlagNaming)}
Boolean(shouldShowFlagNaming) &&
setProjectNamingPattern != null &&
setFeatureNamingExample != null
}
show={ show={
<StyledFieldset> <StyledFieldset>
<Box <Box
@ -322,9 +326,9 @@ const ProjectForm: React.FC<IProjectForm> = ({
<StyledFlagNamingContainer> <StyledFlagNamingContainer>
<StyledInput <StyledInput
label={'Naming Pattern'} label={'Naming Pattern'}
name="pattern" name="feature flag naming pattern"
aria-describedby="pattern-naming-description" aria-describedby="pattern-naming-description"
placeholder="^[A-Za-z]+-[A-Za-z0-9]+$" placeholder="^[A-Za-z]+\.[A-Za-z]+\.[A-Za-z0-9-]+$"
type={'text'} type={'text'}
value={featureNamingPattern || ''} value={featureNamingPattern || ''}
error={Boolean(errors.featureNamingPattern)} error={Boolean(errors.featureNamingPattern)}
@ -337,20 +341,20 @@ const ProjectForm: React.FC<IProjectForm> = ({
} }
/> />
<StyledSubtitle> <StyledSubtitle>
<p id="pattern-example-description"> <p id="pattern-additional-description">
The example will be shown to users when The example and description will be
they create a new feature flag in this shown to users when they create a new
project. feature flag in this project.
</p> </p>
</StyledSubtitle> </StyledSubtitle>
<StyledInput <StyledInput
label={'Naming Example'} label={'Naming Example'}
name="example" name="feature flag naming example"
type={'text'} type={'text'}
aria-describedBy="pattern-example-description" aria-describedBy="pattern-additional-description"
value={featureNamingExample || ''} value={featureNamingExample || ''}
placeholder="dx-feature1" placeholder="dx.feature1.1-135"
error={Boolean(errors.namingExample)} error={Boolean(errors.namingExample)}
errorText={errors.namingExample} errorText={errors.namingExample}
onChange={e => onChange={e =>
@ -359,6 +363,23 @@ const ProjectForm: React.FC<IProjectForm> = ({
) )
} }
/> />
<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> </StyledFlagNamingContainer>
</StyledFieldset> </StyledFieldset>
} }

View File

@ -38,6 +38,7 @@ const EditProject = () => {
featureLimit, featureLimit,
featureNamingPattern, featureNamingPattern,
featureNamingExample, featureNamingExample,
featureNamingDescription,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
@ -46,6 +47,7 @@ const EditProject = () => {
setFeatureLimit, setFeatureLimit,
setFeatureNamingPattern, setFeatureNamingPattern,
setFeatureNamingExample, setFeatureNamingExample,
setFeatureNamingDescription,
getProjectPayload, getProjectPayload,
clearErrors, clearErrors,
validateProjectId, validateProjectId,
@ -59,7 +61,8 @@ const EditProject = () => {
project.mode, project.mode,
project.featureLimit ? String(project.featureLimit) : '', project.featureLimit ? String(project.featureLimit) : '',
project.featureNaming?.pattern || '', project.featureNaming?.pattern || '',
project.featureNaming?.example || '' project.featureNaming?.example || '',
project.featureNaming?.description || ''
); );
const formatApiCode = () => { const formatApiCode = () => {
@ -123,12 +126,14 @@ const EditProject = () => {
featureCount={project.features.length} featureCount={project.features.length}
featureNamingPattern={featureNamingPattern} featureNamingPattern={featureNamingPattern}
featureNamingExample={featureNamingExample} featureNamingExample={featureNamingExample}
featureNamingDescription={featureNamingDescription}
setProjectName={setProjectName} setProjectName={setProjectName}
projectStickiness={projectStickiness} projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness} setProjectStickiness={setProjectStickiness}
setProjectMode={setProjectMode} setProjectMode={setProjectMode}
setProjectNamingPattern={setFeatureNamingPattern} setFeatureNamingPattern={setFeatureNamingPattern}
setFeatureNamingExample={setFeatureNamingExample} setFeatureNamingExample={setFeatureNamingExample}
setFeatureNamingDescription={setFeatureNamingDescription}
projectDesc={projectDesc} projectDesc={projectDesc}
mode="Edit" mode="Edit"
setProjectDesc={setProjectDesc} setProjectDesc={setProjectDesc}

View File

@ -12,7 +12,8 @@ const useProjectForm = (
initialProjectMode: ProjectMode = 'open', initialProjectMode: ProjectMode = 'open',
initialFeatureLimit = '', initialFeatureLimit = '',
initialFeatureNamingPattern = '', initialFeatureNamingPattern = '',
initialFeatureNamingExample = '' initialFeatureNamingExample = '',
initialFeatureNamingDescription = ''
) => { ) => {
const [projectId, setProjectId] = useState(initialProjectId); const [projectId, setProjectId] = useState(initialProjectId);
@ -31,6 +32,11 @@ const useProjectForm = (
const [featureNamingExample, setFeatureNamingExample] = useState( const [featureNamingExample, setFeatureNamingExample] = useState(
initialFeatureNamingExample initialFeatureNamingExample
); );
const [featureNamingDescription, setFeatureNamingDescription] = useState(
initialFeatureNamingDescription
);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const { validateId } = useProjectApi(); const { validateId } = useProjectApi();
@ -63,6 +69,10 @@ const useProjectForm = (
setFeatureNamingExample(initialFeatureNamingExample); setFeatureNamingExample(initialFeatureNamingExample);
}, [initialFeatureNamingExample]); }, [initialFeatureNamingExample]);
useEffect(() => {
setFeatureNamingDescription(initialFeatureNamingDescription);
}, [initialFeatureNamingDescription]);
useEffect(() => { useEffect(() => {
setProjectStickiness(initialProjectStickiness); setProjectStickiness(initialProjectStickiness);
}, [initialProjectStickiness]); }, [initialProjectStickiness]);
@ -78,6 +88,7 @@ const useProjectForm = (
featureNaming: { featureNaming: {
pattern: featureNamingPattern, pattern: featureNamingPattern,
example: featureNamingExample, example: featureNamingExample,
description: featureNamingDescription,
}, },
}; };
}; };
@ -125,8 +136,10 @@ const useProjectForm = (
featureLimit, featureLimit,
featureNamingPattern, featureNamingPattern,
featureNamingExample, featureNamingExample,
featureNamingDescription,
setFeatureNamingPattern, setFeatureNamingPattern,
setFeatureNamingExample, setFeatureNamingExample,
setFeatureNamingDescription,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,

View File

@ -16,6 +16,7 @@ export interface IProjectCard {
export type FeatureNamingType = { export type FeatureNamingType = {
pattern: string; pattern: string;
example: string; example: string;
description: string;
}; };
export interface IProject { export interface IProject {

View File

@ -39,6 +39,7 @@ const SETTINGS_COLUMNS = [
'feature_limit', 'feature_limit',
'feature_naming_pattern', 'feature_naming_pattern',
'feature_naming_example', 'feature_naming_example',
'feature_naming_description',
]; ];
const SETTINGS_TABLE = 'project_settings'; const SETTINGS_TABLE = 'project_settings';
const PROJECT_ENVIRONMENTS = 'project_environments'; const PROJECT_ENVIRONMENTS = 'project_environments';
@ -240,6 +241,7 @@ class ProjectStore implements IProjectStore {
feature_limit: project.featureLimit, feature_limit: project.featureLimit,
feature_naming_pattern: project.featureNamingPattern, feature_naming_pattern: project.featureNamingPattern,
feature_naming_example: project.featureNamingExample, feature_naming_example: project.featureNamingExample,
feature_naming_description: project.featureNamingDescription,
}) })
.returning('*'); .returning('*');
return this.mapRow({ ...row[0], ...settingsRow[0] }); return this.mapRow({ ...row[0], ...settingsRow[0] });
@ -269,6 +271,8 @@ class ProjectStore implements IProjectStore {
feature_limit: data.featureLimit, feature_limit: data.featureLimit,
feature_naming_pattern: data.featureNaming?.pattern, feature_naming_pattern: data.featureNaming?.pattern,
feature_naming_example: data.featureNaming?.example, feature_naming_example: data.featureNaming?.example,
feature_naming_description:
data.featureNaming?.description,
}); });
} else { } else {
await this.db(SETTINGS_TABLE).insert({ await this.db(SETTINGS_TABLE).insert({
@ -278,6 +282,7 @@ class ProjectStore implements IProjectStore {
feature_limit: data.featureLimit, feature_limit: data.featureLimit,
feature_naming_pattern: data.featureNaming?.pattern, feature_naming_pattern: data.featureNaming?.pattern,
feature_naming_example: data.featureNaming?.example, feature_naming_example: data.featureNaming?.example,
feature_naming_description: data.featureNaming?.description,
}); });
} }
} catch (err) { } catch (err) {
@ -573,6 +578,7 @@ class ProjectStore implements IProjectStore {
featureNaming: { featureNaming: {
pattern: row.feature_naming_pattern, pattern: row.feature_naming_pattern,
example: row.feature_naming_example, example: row.feature_naming_example,
description: row.feature_naming_description,
}, },
}; };
} }

View File

@ -11,14 +11,23 @@ export const createFeatureNamingPatternSchema = {
nullable: true, nullable: true,
description: description:
'A JavaScript regular expression pattern, without the start and end delimiters. Optional flags are not allowed.', 'A JavaScript regular expression pattern, without the start and end delimiters. Optional flags are not allowed.',
example: '[a-z]{2,5}.team-[a-z]+.[a-z-]+', example: '^[A-Za-z]+\\.[A-Za-z]+\\.[A-Za-z0-9-]+$',
}, },
example: { example: {
type: 'string', type: 'string',
nullable: true, nullable: true,
description: description:
'An example of a feature name that matches the pattern. Must itself match the pattern supplied.', 'An example of a feature name that matches the pattern. Must itself match the pattern supplied.',
example: 'new-project.team-red.feature-1', example: 'dx.feature1.1-135',
},
description: {
type: 'string',
nullable: true,
description:
'A description of the pattern in a human-readable format. Will be shown to users when they create a new feature flag.',
example: `<project>.<featureName>.<ticket>
The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`,
}, },
}, },
components: {}, components: {},

View File

@ -13,6 +13,7 @@ export const projectSchema = joi
featureNaming: joi.object().keys({ featureNaming: joi.object().keys({
pattern: joi.string().allow(null).allow('').optional(), pattern: joi.string().allow(null).allow('').optional(),
example: joi.string().allow(null).allow('').optional(), example: joi.string().allow(null).allow('').optional(),
description: joi.string().allow(null).allow('').optional(),
}), }),
}) })
.options({ allowUnknown: false, stripUnknown: true }); .options({ allowUnknown: false, stripUnknown: true });

View File

@ -192,6 +192,7 @@ export type ProjectMode = 'open' | 'protected';
export interface IFeatureNaming { export interface IFeatureNaming {
pattern: string | null; pattern: string | null;
example: string | null; example: string | null;
description: string | null;
} }
export interface IProjectOverview { export interface IProjectOverview {

View File

@ -21,6 +21,7 @@ export interface IProjectInsert {
featureLimit?: number; featureLimit?: number;
featureNamingPattern?: string; featureNamingPattern?: string;
featureNamingExample?: string; featureNamingExample?: string;
featureNamingDescription?: string;
} }
export interface IProjectSettings { export interface IProjectSettings {
@ -29,6 +30,7 @@ export interface IProjectSettings {
featureLimit?: number; featureLimit?: number;
featureNamingPattern?: string; featureNamingPattern?: string;
featureNamingExample?: string; featureNamingExample?: string;
featureNamingDescription?: string;
} }
export interface IProjectSettingsRow { export interface IProjectSettingsRow {

View File

@ -0,0 +1,20 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE project_settings
ADD COLUMN IF NOT EXISTS "feature_naming_description" text;
`,
cb(),
);
};
exports.down = function (db, cb) {
db.runSql(
`
ALTER TABLE project_settings DROP COLUMN IF EXISTS "feature_naming_description";
`,
cb,
);
};