Skip to content

Commit

Permalink
1-1319: add feature naming pattern descriptions (#4612)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
thomasheartman authored Sep 6, 2023
1 parent 31df85a commit 73b7cc0
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ const CreateProject = () => {
featureLimit,
featureNamingPattern,
featureNamingExample,
featureNamingDescription,
setFeatureNamingExample,
setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectId,
setProjectName,
setProjectDesc,
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ const EditProject = () => {
projectMode,
featureNamingPattern,
featureNamingExample,
featureNamingDescription,
setProjectId,
setProjectName,
setProjectDesc,
setProjectStickiness,
setProjectMode,
setFeatureNamingExample,
setFeatureNamingPattern,
setFeatureNamingDescription,
getProjectPayload,
clearErrors,
validateProjectId,
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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}
Expand Down
61 changes: 41 additions & 20 deletions frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>>;
Expand Down Expand Up @@ -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%' },
}));

Expand All @@ -116,8 +118,10 @@ const ProjectForm: React.FC<IProjectForm> = ({
featureCount,
featureNamingExample,
featureNamingPattern,
featureNamingDescription,
setFeatureNamingExample,
setProjectNamingPattern,
setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectId,
setProjectName,
setProjectDesc,
Expand All @@ -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);
}
};

Expand All @@ -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>
Expand Down Expand Up @@ -283,11 +291,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
</StyledInputContainer>
</>
<ConditionallyRender
condition={
Boolean(shouldShowFlagNaming) &&
setProjectNamingPattern != null &&
setFeatureNamingExample != null
}
condition={Boolean(shouldShowFlagNaming)}
show={
<StyledFieldset>
<Box
Expand Down Expand Up @@ -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)}
Expand All @@ -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 =>
Expand All @@ -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>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const EditProject = () => {
featureLimit,
featureNamingPattern,
featureNamingExample,
featureNamingDescription,
setProjectId,
setProjectName,
setProjectDesc,
Expand All @@ -46,6 +47,7 @@ const EditProject = () => {
setFeatureLimit,
setFeatureNamingPattern,
setFeatureNamingExample,
setFeatureNamingDescription,
getProjectPayload,
clearErrors,
validateProjectId,
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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}
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/component/project/Project/hooks/useProjectForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const useProjectForm = (
initialProjectMode: ProjectMode = 'open',
initialFeatureLimit = '',
initialFeatureNamingPattern = '',
initialFeatureNamingExample = ''
initialFeatureNamingExample = '',
initialFeatureNamingDescription = ''
) => {
const [projectId, setProjectId] = useState(initialProjectId);

Expand All @@ -31,6 +32,11 @@ const useProjectForm = (
const [featureNamingExample, setFeatureNamingExample] = useState(
initialFeatureNamingExample
);

const [featureNamingDescription, setFeatureNamingDescription] = useState(
initialFeatureNamingDescription
);

const [errors, setErrors] = useState({});

const { validateId } = useProjectApi();
Expand Down Expand Up @@ -63,6 +69,10 @@ const useProjectForm = (
setFeatureNamingExample(initialFeatureNamingExample);
}, [initialFeatureNamingExample]);

useEffect(() => {
setFeatureNamingDescription(initialFeatureNamingDescription);
}, [initialFeatureNamingDescription]);

useEffect(() => {
setProjectStickiness(initialProjectStickiness);
}, [initialProjectStickiness]);
Expand All @@ -78,6 +88,7 @@ const useProjectForm = (
featureNaming: {
pattern: featureNamingPattern,
example: featureNamingExample,
description: featureNamingDescription,
},
};
};
Expand Down Expand Up @@ -125,8 +136,10 @@ const useProjectForm = (
featureLimit,
featureNamingPattern,
featureNamingExample,
featureNamingDescription,
setFeatureNamingPattern,
setFeatureNamingExample,
setFeatureNamingDescription,
setProjectId,
setProjectName,
setProjectDesc,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/interfaces/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface IProjectCard {
export type FeatureNamingType = {
pattern: string;
example: string;
description: string;
};

export interface IProject {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/db/project-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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] });
Expand Down Expand Up @@ -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({
Expand All @@ -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) {
Expand Down Expand Up @@ -573,6 +578,7 @@ class ProjectStore implements IProjectStore {
featureNaming: {
pattern: row.feature_naming_pattern,
example: row.feature_naming_example,
description: row.feature_naming_description,
},
};
}
Expand Down
13 changes: 11 additions & 2 deletions src/lib/openapi/spec/create-feature-naming-pattern-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand Down
1 change: 1 addition & 0 deletions src/lib/services/project-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
1 change: 1 addition & 0 deletions src/lib/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export type ProjectMode = 'open' | 'protected';
export interface IFeatureNaming {
pattern: string | null;
example: string | null;
description: string | null;
}

export interface IProjectOverview {
Expand Down
Loading

0 comments on commit 73b7cc0

Please sign in to comment.