feat(EntityTagsPicker): implement scaffolder field extension for selecting tags

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2021-12-28 21:36:28 -05:00
parent 9120f5351d
commit 9c25894892
8 changed files with 158 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Implement a `EntityTagsPicker` field extension
+10
View File
@@ -80,6 +80,16 @@ export const EntityPicker: ({
// @public (undocumented)
export const EntityPickerFieldExtension: () => null;
// @public
export const EntityTagsPicker: ({
formData,
onChange,
uiSchema,
}: FieldProps<string[]>) => 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)
@@ -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<string[]>) => {
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 (
<FormControl margin="normal">
<Autocomplete
multiple
freeSolo
filterSelectedOptions
onChange={setTags}
value={formData || []}
inputValue={inputValue}
loading={loading}
options={existingTags || []}
ChipProps={{ size: 'small' }}
renderInput={params => (
<TextField
{...params}
label="Tags"
onChange={e => 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"
/>
)}
/>
</FormControl>
);
};
@@ -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';
@@ -19,3 +19,4 @@ export * from './OwnerPicker';
export * from './RepoUrlPicker';
export * from './TextValuePicker';
export * from './OwnedEntityPicker';
export * from './EntityTagsPicker';
@@ -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',
+2
View File
@@ -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,
+12
View File
@@ -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',
}),
);