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,
featureNamingPattern,
featureNamingExample,
featureNamingDescription,
setFeatureNamingExample,
setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectId,
setProjectName,
setProjectDesc,
@ -114,7 +116,9 @@ const CreateProject = () => {
featureLimit={featureLimit}
featureNamingExample={featureNamingExample}
featureNamingPattern={featureNamingPattern}
setProjectNamingPattern={setFeatureNamingPattern}
setFeatureNamingPattern={setFeatureNamingPattern}
featureNamingDescription={featureNamingDescription}
setFeatureNamingDescription={setFeatureNamingDescription}
setFeatureNamingExample={setFeatureNamingExample}
setProjectStickiness={setProjectStickiness}
setFeatureLimit={setFeatureLimit}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,14 +11,23 @@ export const createFeatureNamingPatternSchema = {
nullable: true,
description:
'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: {
type: 'string',
nullable: true,
description:
'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: {},

View File

@ -13,6 +13,7 @@ export const projectSchema = joi
featureNaming: joi.object().keys({
pattern: 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 });

View File

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

View File

@ -21,6 +21,7 @@ export interface IProjectInsert {
featureLimit?: number;
featureNamingPattern?: string;
featureNamingExample?: string;
featureNamingDescription?: string;
}
export interface IProjectSettings {
@ -29,6 +30,7 @@ export interface IProjectSettings {
featureLimit?: number;
featureNamingPattern?: string;
featureNamingExample?: string;
featureNamingDescription?: string;
}
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,
);
};