From 9c2589489202f594888e6c6caa26c821b92f6bf6 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 28 Dec 2021 21:36:28 -0500 Subject: [PATCH] feat(EntityTagsPicker): implement scaffolder field extension for selecting tags Signed-off-by: Phil Kuang --- .changeset/eleven-otters-unite.md | 5 + plugins/scaffolder/api-report.md | 10 ++ .../EntityTagsPicker/EntityTagsPicker.tsx | 107 ++++++++++++++++++ .../fields/EntityTagsPicker/index.ts | 16 +++ .../scaffolder/src/components/fields/index.ts | 1 + plugins/scaffolder/src/extensions/default.ts | 5 + plugins/scaffolder/src/index.ts | 2 + plugins/scaffolder/src/plugin.ts | 12 ++ 8 files changed, 158 insertions(+) create mode 100644 .changeset/eleven-otters-unite.md create mode 100644 plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts diff --git a/.changeset/eleven-otters-unite.md b/.changeset/eleven-otters-unite.md new file mode 100644 index 0000000000..f7836a7ade --- /dev/null +++ b/.changeset/eleven-otters-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Implement a `EntityTagsPicker` field extension diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 4f6928e72d..5252e08299 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -80,6 +80,16 @@ export const EntityPicker: ({ // @public (undocumented) export const EntityPickerFieldExtension: () => null; +// @public +export const EntityTagsPicker: ({ + formData, + onChange, + uiSchema, +}: FieldProps) => JSX.Element; + +// @public +export const EntityTagsPickerFieldExtension: () => null; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "FavouriteTemplate" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx new file mode 100644 index 0000000000..b21398a3f4 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import { useAsync, useEffectOnce } from 'react-use'; +import { CatalogEntitiesRequest } from '@backstage/catalog-client'; +import { Entity, makeValidator } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { FormControl, TextField } from '@material-ui/core'; +import { Autocomplete } from '@material-ui/lab'; +import { FieldProps } from '@rjsf/core'; + +/** + * EntityTagsPicker + * @public + */ +export const EntityTagsPicker = ({ + formData, + onChange, + uiSchema, +}: FieldProps) => { + const catalogApi = useApi(catalogApiRef); + const [inputValue, setInputValue] = useState(''); + const [inputError, setInputError] = useState(false); + const tagValidator = makeValidator().isValidTag; + const kinds = uiSchema['ui:options']?.kinds as string[]; + + const { loading, value: existingTags } = useAsync(async () => { + const tagsRequest: CatalogEntitiesRequest = { fields: ['metadata.tags'] }; + if (kinds) { + tagsRequest.filter = { kind: kinds }; + } + + const entities = await catalogApi.getEntities(tagsRequest); + + return [ + ...new Set( + entities.items + .flatMap((e: Entity) => e.metadata?.tags) + .filter(Boolean) as string[], + ), + ].sort(); + }); + + const setTags = (_: React.ChangeEvent<{}>, values: string[] | null) => { + // Reset error state in case all tags were removed + let hasError = false; + let addDuplicate = false; + const currentTags = formData || []; + + // If adding a new tag + if (values?.length && currentTags.length < values.length) { + const newTag = (values[values.length - 1] = values[values.length - 1] + .toLocaleLowerCase('en-US') + .trim()); + hasError = !tagValidator(newTag); + addDuplicate = currentTags.indexOf(newTag) !== -1; + } + + setInputError(hasError); + setInputValue(!hasError ? '' : inputValue); + if (!hasError && !addDuplicate) { + onChange(values || []); + } + }; + + // Initialize field to always return an array + useEffectOnce(() => onChange(formData || [])); + + return ( + + ( + setInputValue(e.target.value)} + error={inputError} + helperText="Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters" + /> + )} + /> + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts new file mode 100644 index 0000000000..09f4d5c519 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityTagsPicker } from './EntityTagsPicker'; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 299470e220..ecde218029 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -19,3 +19,4 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './TextValuePicker'; export * from './OwnedEntityPicker'; +export * from './EntityTagsPicker'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 12a29809e0..820cc0e6d3 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -18,6 +18,7 @@ import { EntityNamePicker, entityNamePickerValidation, } from '../components/fields/EntityNamePicker'; +import { EntityTagsPicker } from '../components/fields/EntityTagsPicker'; import { OwnerPicker } from '../components/fields/OwnerPicker'; import { repoPickerValidation, @@ -36,6 +37,10 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [ name: 'EntityNamePicker', validation: entityNamePickerValidation, }, + { + component: EntityTagsPicker, + name: 'EntityTagsPicker', + }, { component: RepoUrlPicker, name: 'RepoUrlPicker', diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index dfbd2af296..3851b2151b 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -30,6 +30,7 @@ export type { CustomFieldValidator, FieldExtensionOptions } from './extensions'; export { EntityPickerFieldExtension, EntityNamePickerFieldExtension, + EntityTagsPickerFieldExtension, OwnerPickerFieldExtension, OwnedEntityPickerFieldExtension, RepoUrlPickerFieldExtension, @@ -40,6 +41,7 @@ export { export { EntityNamePicker, EntityPicker, + EntityTagsPicker, OwnerPicker, RepoUrlPicker, TextValuePicker, diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 95cad50eba..02b36c4bb6 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -36,6 +36,7 @@ import { identityApiRef, } from '@backstage/core-plugin-api'; import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker'; +import { EntityTagsPicker } from './components/fields/EntityTagsPicker'; export const scaffolderPlugin = createPlugin({ id: 'scaffolder', @@ -103,3 +104,14 @@ export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide( name: 'OwnedEntityPicker', }), ); + +/** + * EntityTagsPickerFieldExtension + * @public + */ +export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: EntityTagsPicker, + name: 'EntityTagsPicker', + }), +);