From c99e60eee1cdf862631be50f884745d95e989bb0 Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Fri, 14 Apr 2023 12:36:50 +0200 Subject: [PATCH 01/17] OwnedGroups Dropdown Signed-off-by: Peter Macdonald --- .../sample-templates/remote-templates.yaml | 1 + .../OwnedGroupsPicker.test.tsx | 15 ++++ .../OwnedGroupsPicker/OwnedGroupsPicker.tsx | 81 +++++++++++++++++++ .../fields/OwnedGroupsPicker/index.ts | 20 +++++ .../fields/OwnedGroupsPicker/schema.ts | 47 +++++++++++ .../scaffolder/src/components/fields/index.ts | 1 + plugins/scaffolder/src/extensions/default.ts | 9 +++ plugins/scaffolder/src/index.ts | 1 + plugins/scaffolder/src/plugin.tsx | 17 ++++ 9 files changed, 192 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/OwnedGroupsPicker/index.ts create mode 100644 plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts diff --git a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml index a812c9ce52..8959676d60 100644 --- a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml @@ -11,3 +11,4 @@ spec: - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/pull-request/template.yaml - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/springboot-grpc-template/template.yaml + - https://github.com/Parsifal-M/backstage-testing-grounds/blob/main/templates/template.yaml diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.test.tsx new file mode 100644 index 0000000000..eb2f165350 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.test.tsx @@ -0,0 +1,15 @@ +/* + * Copyright 2023 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. + */ diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx new file mode 100644 index 0000000000..5123adff84 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2023 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 from 'react'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { EntityPicker } from '../EntityPicker/EntityPicker'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { OwnedGroupsPickerProps } from './schema'; +import { TextField } from '@material-ui/core'; + +export { OwnedGroupsPickerSchema } from './schema'; + +export const OwnedGroupsPicker = (props: OwnedGroupsPickerProps) => { + const { + schema: { title = 'Group', description = 'A group from the catalog' }, + uiSchema, + required, + } = props; + + const identityApi = useApi(identityApiRef); + const { loading, value: identityRefs } = useAsync(async () => { + const identity = await identityApi.getBackstageIdentity(); + return identity.userEntityRef; + }); + + const catalogFilter = { + kind: 'group', + ['relations.hasMember']: identityRefs || [], + }; + + const groupUiSchema = { + ...uiSchema, + 'ui:options': { + catalogFilter, + defaultKind: 'group', + }, + }; + + if (loading) { + return ( + ( + + )} + options={[]} + /> + ); + } + + return ( + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/index.ts new file mode 100644 index 0000000000..673213ae65 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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 { + OwnedGroupsPickerFieldSchema, + type OwnedGroupsPickerUiOptions, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts new file mode 100644 index 0000000000..6f771db5e6 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2023 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 { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; +import { entityQueryFilterExpressionSchema } from '../EntityPicker/schema'; + +export const OwnedGroupsPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), + catalogFilter: z + .array(entityQueryFilterExpressionSchema) + .or(entityQueryFilterExpressionSchema) + .optional() + .describe('List of key-value filter expression for entities'), + }), +); + +export type OwnedGroupsPickerUiOptions = + typeof OwnedGroupsPickerFieldSchema.uiOptionsType; +export type OwnedGroupsPickerProps = typeof OwnedGroupsPickerFieldSchema.type; +export const OwnedGroupsPickerSchema = OwnedGroupsPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 8d86f601ce..aeab13bd12 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,4 +18,5 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; +export * from './OwnedGroupsPicker'; export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index c6ab5276d0..a6984e5a49 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -39,6 +39,10 @@ import { OwnedEntityPicker, OwnedEntityPickerSchema, } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { + OwnedGroupsPicker, + OwnedGroupsPickerSchema, +} from '../components/fields/OwnedGroupsPicker/OwnedGroupsPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -73,4 +77,9 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ name: 'OwnedEntityPicker', schema: OwnedEntityPickerSchema, }, + { + component: OwnedGroupsPicker, + name: 'OwnedGroupsPicker', + schema: OwnedGroupsPickerSchema, + }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index bc56629a2c..ff1b0c42f5 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -27,6 +27,7 @@ export { EntityTagsPickerFieldExtension, OwnerPickerFieldExtension, OwnedEntityPickerFieldExtension, + OwnedGroupsFieldExtension, RepoUrlPickerFieldExtension, ScaffolderPage, scaffolderPlugin, diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index dfe87a3780..3b921f91f0 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -64,6 +64,10 @@ import { actionsRouteRef, editRouteRef, } from './routes'; +import { + OwnedGroupsPicker, + OwnedGroupsPickerSchema, +} from './components/fields/OwnedGroupsPicker/OwnedGroupsPicker'; /** * The main plugin export for the scaffolder. @@ -158,6 +162,19 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( }), ); +/** + * A field extension for picking users and groups out of the Catalog. + * + * @public + */ +export const OwnedGroupsFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: OwnedGroupsPicker, + name: 'OwnedGroupsPicker', + schema: OwnedGroupsPickerSchema, + }), +); + /** * The Router and main entrypoint to the Scaffolder plugin. * From 0a40875f14f74dec2bdb6160c9778dad8a25cd3c Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Tue, 2 May 2023 10:46:02 +0200 Subject: [PATCH 02/17] Owned Groups Picker Signed-off-by: Peter Macdonald --- .../scaffolder-backend/sample-templates/remote-templates.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml index 8959676d60..a812c9ce52 100644 --- a/plugins/scaffolder-backend/sample-templates/remote-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/remote-templates.yaml @@ -11,4 +11,3 @@ spec: - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/pull-request/template.yaml - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml - https://github.com/backstage/software-templates/blob/main/scaffolder-templates/springboot-grpc-template/template.yaml - - https://github.com/Parsifal-M/backstage-testing-grounds/blob/main/templates/template.yaml From de75235bec3556c91954b48e2e06d7433f1e070c Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Thu, 11 May 2023 09:20:52 +0200 Subject: [PATCH 03/17] Small refactor Signed-off-by: Peter Macdonald --- .../GroupsImPartOfPicker.tsx | 78 ++++++++++++++++++ .../index.ts | 4 +- .../schema.ts} | 18 +++++ .../OwnedGroupsPicker/OwnedGroupsPicker.tsx | 81 ------------------- .../fields/OwnedGroupsPicker/schema.ts | 47 ----------- .../scaffolder/src/components/fields/index.ts | 2 +- plugins/scaffolder/src/extensions/default.ts | 12 +-- plugins/scaffolder/src/plugin.tsx | 12 +-- 8 files changed, 111 insertions(+), 143 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx rename plugins/scaffolder/src/components/fields/{OwnedGroupsPicker => GroupsImPartOfPicker}/index.ts (90%) rename plugins/scaffolder/src/components/fields/{OwnedGroupsPicker/OwnedGroupsPicker.test.tsx => GroupsImPartOfPicker/schema.ts} (53%) delete mode 100644 plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx delete mode 100644 plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts diff --git a/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx b/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx new file mode 100644 index 0000000000..eb3d2e423e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2023 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 from 'react'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { + TextField, + MenuItem, + Select, + FormControl, + InputLabel, +} from '@material-ui/core'; +import { + GroupsImPartOfPickerProps, + GroupsImPartOfPickerSchema, +} from './schema'; + +export { GroupsImPartOfPickerSchema }; + +export const GroupsImPartOfPicker = (props: GroupsImPartOfPickerProps) => { + const { + schema: { title = 'Group', description = 'A group you are part of' }, + required, + } = props; + + const identityApi = useApi(identityApiRef); + const { loading, value: identityRefs } = useAsync(async () => { + const identity = await identityApi.getBackstageIdentity(); + return identity.ownershipEntityRefs; + }); + + const [selectedGroup, setSelectedGroup] = React.useState(''); + + const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { + setSelectedGroup(event.target.value as string); + }; + + if (loading) { + return ( + + ); + } + + return ( + + {title} + + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/index.ts b/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/index.ts similarity index 90% rename from plugins/scaffolder/src/components/fields/OwnedGroupsPicker/index.ts rename to plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/index.ts index 673213ae65..960f5de5ef 100644 --- a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/index.ts @@ -15,6 +15,6 @@ */ export { - OwnedGroupsPickerFieldSchema, - type OwnedGroupsPickerUiOptions, + GroupsImPartOfPickerSchema, + type GroupsImPartOfPickerUiOptions, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.test.tsx b/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/schema.ts similarity index 53% rename from plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.test.tsx rename to plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/schema.ts index eb2f165350..839aa349a4 100644 --- a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/schema.ts @@ -13,3 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +export const GroupsImPartOfPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + title: z.string().optional(), + description: z.string().optional(), + }), +); + +export type GroupsImPartOfPickerUiOptions = + typeof GroupsImPartOfPickerFieldSchema.uiOptionsType; +export type GroupsImPartOfPickerProps = + typeof GroupsImPartOfPickerFieldSchema.type; +export const GroupsImPartOfPickerSchema = + GroupsImPartOfPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx deleted file mode 100644 index 5123adff84..0000000000 --- a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/OwnedGroupsPicker.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2023 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 from 'react'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -import useAsync from 'react-use/lib/useAsync'; -import { EntityPicker } from '../EntityPicker/EntityPicker'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import { OwnedGroupsPickerProps } from './schema'; -import { TextField } from '@material-ui/core'; - -export { OwnedGroupsPickerSchema } from './schema'; - -export const OwnedGroupsPicker = (props: OwnedGroupsPickerProps) => { - const { - schema: { title = 'Group', description = 'A group from the catalog' }, - uiSchema, - required, - } = props; - - const identityApi = useApi(identityApiRef); - const { loading, value: identityRefs } = useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - return identity.userEntityRef; - }); - - const catalogFilter = { - kind: 'group', - ['relations.hasMember']: identityRefs || [], - }; - - const groupUiSchema = { - ...uiSchema, - 'ui:options': { - catalogFilter, - defaultKind: 'group', - }, - }; - - if (loading) { - return ( - ( - - )} - options={[]} - /> - ); - } - - return ( - - ); -}; diff --git a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts deleted file mode 100644 index 6f771db5e6..0000000000 --- a/plugins/scaffolder/src/components/fields/OwnedGroupsPicker/schema.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2023 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 { z } from 'zod'; -import { makeFieldSchemaFromZod } from '../utils'; -import { entityQueryFilterExpressionSchema } from '../EntityPicker/schema'; - -export const OwnedGroupsPickerFieldSchema = makeFieldSchemaFromZod( - z.string(), - z.object({ - defaultKind: z - .string() - .optional() - .describe( - 'The default entity kind. Options of this kind will not be prefixed.', - ), - defaultNamespace: z - .union([z.string(), z.literal(false)]) - .optional() - .describe( - 'The default namespace. Options with this namespace will not be prefixed.', - ), - catalogFilter: z - .array(entityQueryFilterExpressionSchema) - .or(entityQueryFilterExpressionSchema) - .optional() - .describe('List of key-value filter expression for entities'), - }), -); - -export type OwnedGroupsPickerUiOptions = - typeof OwnedGroupsPickerFieldSchema.uiOptionsType; -export type OwnedGroupsPickerProps = typeof OwnedGroupsPickerFieldSchema.type; -export const OwnedGroupsPickerSchema = OwnedGroupsPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index aeab13bd12..c9f23a00b7 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,5 +18,5 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; -export * from './OwnedGroupsPicker'; +export * from './GroupsImPartOfPicker'; export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index a6984e5a49..9c027610a5 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -40,9 +40,9 @@ import { OwnedEntityPickerSchema, } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; import { - OwnedGroupsPicker, - OwnedGroupsPickerSchema, -} from '../components/fields/OwnedGroupsPicker/OwnedGroupsPicker'; + GroupsImPartOfPicker, + GroupsImPartOfPickerSchema, +} from '../components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -78,8 +78,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ schema: OwnedEntityPickerSchema, }, { - component: OwnedGroupsPicker, - name: 'OwnedGroupsPicker', - schema: OwnedGroupsPickerSchema, + component: GroupsImPartOfPicker, + name: 'GroupsImPartOfPicker', + schema: GroupsImPartOfPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 3b921f91f0..2561e6d63c 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -65,9 +65,9 @@ import { editRouteRef, } from './routes'; import { - OwnedGroupsPicker, - OwnedGroupsPickerSchema, -} from './components/fields/OwnedGroupsPicker/OwnedGroupsPicker'; + GroupsImPartOfPicker, + GroupsImPartOfPickerSchema, +} from './components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker'; /** * The main plugin export for the scaffolder. @@ -169,9 +169,9 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( */ export const OwnedGroupsFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ - component: OwnedGroupsPicker, - name: 'OwnedGroupsPicker', - schema: OwnedGroupsPickerSchema, + component: GroupsImPartOfPicker, + name: 'GroupsImPartOfPicker', + schema: GroupsImPartOfPickerSchema, }), ); From d308271c9715d00a109cdf7149b03522d486b6fd Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Thu, 11 May 2023 09:24:53 +0200 Subject: [PATCH 04/17] Small refactor Signed-off-by: Peter Macdonald --- plugins/scaffolder/src/index.ts | 2 +- plugins/scaffolder/src/plugin.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index ff1b0c42f5..5817c4046f 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -27,7 +27,7 @@ export { EntityTagsPickerFieldExtension, OwnerPickerFieldExtension, OwnedEntityPickerFieldExtension, - OwnedGroupsFieldExtension, + GroupsImPartOfPickerFieldExtension, RepoUrlPickerFieldExtension, ScaffolderPage, scaffolderPlugin, diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 2561e6d63c..6e43959ea0 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -167,7 +167,7 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( * * @public */ -export const OwnedGroupsFieldExtension = scaffolderPlugin.provide( +export const GroupsImPartOfPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: GroupsImPartOfPicker, name: 'GroupsImPartOfPicker', From 93dafd1d7b4ae781afca7f395e1a57a275f86c16 Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Tue, 23 May 2023 12:51:20 +0200 Subject: [PATCH 05/17] OwnershipEntityRefPicker Work Signed-off-by: Peter Macdonald --- .../GroupsImPartOfPicker.tsx | 78 ---------------- .../OwnershipEntityRefPicker.tsx | 93 +++++++++++++++++++ .../index.ts | 4 +- .../schema.ts | 14 +-- .../scaffolder/src/components/fields/index.ts | 2 +- plugins/scaffolder/src/extensions/default.ts | 12 +-- plugins/scaffolder/src/index.ts | 2 +- plugins/scaffolder/src/plugin.tsx | 14 +-- 8 files changed, 117 insertions(+), 102 deletions(-) delete mode 100644 plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx rename plugins/scaffolder/src/components/fields/{GroupsImPartOfPicker => OwnershipEntityRefPicker}/index.ts (89%) rename plugins/scaffolder/src/components/fields/{GroupsImPartOfPicker => OwnershipEntityRefPicker}/schema.ts (68%) diff --git a/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx b/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx deleted file mode 100644 index eb3d2e423e..0000000000 --- a/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2023 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 from 'react'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -import useAsync from 'react-use/lib/useAsync'; -import { - TextField, - MenuItem, - Select, - FormControl, - InputLabel, -} from '@material-ui/core'; -import { - GroupsImPartOfPickerProps, - GroupsImPartOfPickerSchema, -} from './schema'; - -export { GroupsImPartOfPickerSchema }; - -export const GroupsImPartOfPicker = (props: GroupsImPartOfPickerProps) => { - const { - schema: { title = 'Group', description = 'A group you are part of' }, - required, - } = props; - - const identityApi = useApi(identityApiRef); - const { loading, value: identityRefs } = useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - return identity.ownershipEntityRefs; - }); - - const [selectedGroup, setSelectedGroup] = React.useState(''); - - const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { - setSelectedGroup(event.target.value as string); - }; - - if (loading) { - return ( - - ); - } - - return ( - - {title} - - - ); -}; diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx new file mode 100644 index 0000000000..80a870e1ed --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2023 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, { useEffect, useState } from 'react'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { TextField, FormControl } from '@material-ui/core'; +import { + OwnershipEntityRefPickerProps, + OwnershipEntityRefPickerSchema, +} from './schema'; +import { Autocomplete } from '@material-ui/lab'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { NotFoundError } from '@backstage/errors'; + +export { OwnershipEntityRefPickerSchema }; + +export const OwnershipEntityRefPicker = ( + props: OwnershipEntityRefPickerProps, +) => { + const { + schema: { title = 'Group', description = 'A group you are part of' }, + required, + rawErrors, + formData, + } = props; + + const identityApi = useApi(identityApiRef); + const catalogApi = useApi(catalogApiRef); + const [groups, setGroups] = useState([]); + const [selectedGroup, setSelectedGroup] = useState(''); + + useEffect(() => { + const fetchUserGroups = async () => { + const identity = await identityApi.getBackstageIdentity(); + const userIdentity = identity.ownershipEntityRefs; + + if (!userIdentity) { + throw new NotFoundError('No ownership entity refs found'); + } + + const userOwnedGroups = await catalogApi.getEntities({ + filter: { + kind: ['Group'], + 'relations.hasMember': userIdentity, + }, + }); + const groupValues = userOwnedGroups.items.map(item => item.metadata.name); + setGroups(groupValues); + }; + + fetchUserGroups(); + }, [identityApi, catalogApi]); + + return ( + 0 && !formData} + > + setSelectedGroup(value || '')} + getOptionLabel={group => group} + renderInput={params => ( + + )} + /> + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts similarity index 89% rename from plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/index.ts rename to plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts index 960f5de5ef..2e744ef6e9 100644 --- a/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts @@ -15,6 +15,6 @@ */ export { - GroupsImPartOfPickerSchema, - type GroupsImPartOfPickerUiOptions, + OwnershipEntityRefPickerSchema, + type OwnershipEntityRefPickerUiOptions, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts similarity index 68% rename from plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/schema.ts rename to plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts index 839aa349a4..9236113356 100644 --- a/plugins/scaffolder/src/components/fields/GroupsImPartOfPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts @@ -17,7 +17,7 @@ import { z } from 'zod'; import { makeFieldSchemaFromZod } from '../utils'; -export const GroupsImPartOfPickerFieldSchema = makeFieldSchemaFromZod( +export const OwnershipEntityRefPickerFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ title: z.string().optional(), @@ -25,9 +25,9 @@ export const GroupsImPartOfPickerFieldSchema = makeFieldSchemaFromZod( }), ); -export type GroupsImPartOfPickerUiOptions = - typeof GroupsImPartOfPickerFieldSchema.uiOptionsType; -export type GroupsImPartOfPickerProps = - typeof GroupsImPartOfPickerFieldSchema.type; -export const GroupsImPartOfPickerSchema = - GroupsImPartOfPickerFieldSchema.schema; +export type OwnershipEntityRefPickerUiOptions = + typeof OwnershipEntityRefPickerFieldSchema.uiOptionsType; +export type OwnershipEntityRefPickerProps = + typeof OwnershipEntityRefPickerFieldSchema.type; +export const OwnershipEntityRefPickerSchema = + OwnershipEntityRefPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index c9f23a00b7..67bb126ede 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,5 +18,5 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; -export * from './GroupsImPartOfPicker'; +export * from './OwnershipEntityRefPicker'; export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 9c027610a5..dae89266d0 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -40,9 +40,9 @@ import { OwnedEntityPickerSchema, } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; import { - GroupsImPartOfPicker, - GroupsImPartOfPickerSchema, -} from '../components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker'; + OwnershipEntityRefPicker, + OwnershipEntityRefPickerSchema, +} from '../components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -78,8 +78,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ schema: OwnedEntityPickerSchema, }, { - component: GroupsImPartOfPicker, - name: 'GroupsImPartOfPicker', - schema: GroupsImPartOfPickerSchema, + component: OwnershipEntityRefPicker, + name: 'OwnershipEntityRefPicker', + schema: OwnershipEntityRefPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 5817c4046f..7ec9717bdc 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -27,7 +27,7 @@ export { EntityTagsPickerFieldExtension, OwnerPickerFieldExtension, OwnedEntityPickerFieldExtension, - GroupsImPartOfPickerFieldExtension, + OwnershipEntityRefPickerFieldExtension, RepoUrlPickerFieldExtension, ScaffolderPage, scaffolderPlugin, diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 6e43959ea0..b190cf982a 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -65,9 +65,9 @@ import { editRouteRef, } from './routes'; import { - GroupsImPartOfPicker, - GroupsImPartOfPickerSchema, -} from './components/fields/GroupsImPartOfPicker/GroupsImPartOfPicker'; + OwnershipEntityRefPicker, + OwnershipEntityRefPickerSchema, +} from './components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker'; /** * The main plugin export for the scaffolder. @@ -167,11 +167,11 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( * * @public */ -export const GroupsImPartOfPickerFieldExtension = scaffolderPlugin.provide( +export const OwnershipEntityRefPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ - component: GroupsImPartOfPicker, - name: 'GroupsImPartOfPicker', - schema: GroupsImPartOfPickerSchema, + component: OwnershipEntityRefPicker, + name: 'OwnershipEntityRefPicker', + schema: OwnershipEntityRefPickerSchema, }), ); From a9cec8ce0835245aa78948743df5700d1cd63d5d Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Mon, 29 May 2023 15:08:33 +0200 Subject: [PATCH 06/17] small modifications, TODO: Testing Signed-off-by: Peter Macdonald --- .../OwnershipEntityRefPicker.test.tsx | 15 +++++++++++++++ .../OwnershipEntityRefPicker.tsx | 11 +++-------- .../fields/OwnershipEntityRefPicker/schema.ts | 7 +++++-- 3 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx new file mode 100644 index 0000000000..eb2f165350 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -0,0 +1,15 @@ +/* + * Copyright 2023 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. + */ diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx index 80a870e1ed..5fee57eb86 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx @@ -30,12 +30,7 @@ export { OwnershipEntityRefPickerSchema }; export const OwnershipEntityRefPicker = ( props: OwnershipEntityRefPickerProps, ) => { - const { - schema: { title = 'Group', description = 'A group you are part of' }, - required, - rawErrors, - formData, - } = props; + const { uiSchema, required, rawErrors, formData } = props; const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); @@ -79,9 +74,9 @@ export const OwnershipEntityRefPicker = ( renderInput={params => ( Date: Tue, 30 May 2023 09:38:16 +0200 Subject: [PATCH 07/17] writing tests Signed-off-by: Peter Macdonald --- .../OwnershipEntityRefPicker.test.tsx | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx index eb2f165350..1c1027634a 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -13,3 +13,119 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { CatalogApi } from '@backstage/catalog-client'; +import { FieldProps } from '@rjsf/core'; +import { EntityPickerProps } from '../EntityPicker/schema'; +import { Entity } from '@backstage/catalog-model'; +import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker'; + +const makeUserEntity = ( + kind: string, + memberOf: string[], + name: string, +): Entity => ({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind, + metadata: { name }, + spec: { + memberOf: memberOf, + }, +}); + +const makeGroupEntity = ( + kind: string, + members: string[], + name: string, +): Entity => ({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind, + metadata: { name }, + spec: { + members: members, + }, +}); + +describe('', () => { + let entities: Entity[]; + const onChange = jest.fn(); + const schema = {}; + const required = false; + let uiSchema: EntityPickerProps['uiSchema']; + const rawErrors: string[] = []; + const formData = undefined; + + let props: FieldProps; + + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + addLocation: jest.fn(), + getLocationByRef: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + + beforeEach(() => { + entities = [ + makeUserEntity('User', ['group1', 'group2'], 'Bob'), + makeGroupEntity('Group', ['Alice', 'Dave'], 'group3'), + ]; + }); + + afterEach(() => jest.resetAllMocks()); + + it('should only return the groups a user is part of', async () => { + catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]); + + uiSchema = { 'ui:options': { catalogApi } }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + render(); + + // Assuming the component calls `onChange` with the groups a user is a part of + expect(onChange).toHaveBeenCalledWith(['group1', 'group2']); + }); + + it('should not return groups a user is not part of', async () => { + catalogApi.getEntityByRef.mockResolvedValueOnce(entities[1]); + + uiSchema = { 'ui:options': { catalogApi } }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + render(); + + // Assuming the component calls `onChange` with the groups a user is a part of + expect(onChange).not.toHaveBeenCalledWith(['group1', 'group2']); + }); + + it('should render without imploding', () => { + uiSchema = { 'ui:options': { catalogApi } }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + const { container } = render(); + expect(container).not.toBeNull(); + }); +}); From e7ce0cd7d6cd227749e1fd63dcf7abb9cba75d97 Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Tue, 30 May 2023 09:51:14 +0200 Subject: [PATCH 08/17] writing tests Signed-off-by: Peter Macdonald --- .../OwnershipEntityRefPicker.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx index 1c1027634a..a290adbaee 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -91,12 +91,12 @@ describe('', () => { render(); - // Assuming the component calls `onChange` with the groups a user is a part of expect(onChange).toHaveBeenCalledWith(['group1', 'group2']); }); it('should not return groups a user is not part of', async () => { - catalogApi.getEntityByRef.mockResolvedValueOnce(entities[1]); + // Mock 'getEntityByRef' to return the entity that represents Bob + catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]); uiSchema = { 'ui:options': { catalogApi } }; props = { @@ -110,8 +110,8 @@ describe('', () => { render(); - // Assuming the component calls `onChange` with the groups a user is a part of - expect(onChange).not.toHaveBeenCalledWith(['group1', 'group2']); + // The onChange should not have been called with 'group3', since Bob is not part of 'group3' + expect(onChange).not.toHaveBeenCalledWith(['group3']); }); it('should render without imploding', () => { From 2af2bdfb9017d0e3fb83e474d103f922c0eaa3ea Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Tue, 30 May 2023 14:58:21 +0200 Subject: [PATCH 09/17] writing tests Signed-off-by: Peter Macdonald --- .../OwnershipEntityRefPicker.test.tsx | 222 ++++++++++++------ .../OwnershipEntityRefPicker.tsx | 16 +- 2 files changed, 158 insertions(+), 80 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx index a290adbaee..81bbb21438 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -15,117 +15,191 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { CatalogApi } from '@backstage/catalog-client'; import { FieldProps } from '@rjsf/core'; -import { EntityPickerProps } from '../EntityPicker/schema'; -import { Entity } from '@backstage/catalog-model'; import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker'; +import { TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; +import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import userEvent from '@testing-library/user-event'; -const makeUserEntity = ( - kind: string, - memberOf: string[], - name: string, -): Entity => ({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind, - metadata: { name }, - spec: { - memberOf: memberOf, - }, -}); - -const makeGroupEntity = ( - kind: string, - members: string[], - name: string, -): Entity => ({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind, - metadata: { name }, - spec: { - members: members, - }, -}); +// Create a mock IdentityApi +const mockIdentityApi: IdentityApi = { + getProfileInfo: () => + Promise.resolve({ + displayName: 'Bob', + email: 'bob@example.com', + picture: 'https://example.com/picture.jpg', + }), + getBackstageIdentity: () => + Promise.resolve({ + id: 'Bob', + idToken: 'token', + type: 'user', + userEntityRef: 'user:default/Bob', + ownershipEntityRefs: ['group:default/group1', 'group:default/group2'], + }), + getCredentials: () => Promise.resolve({ token: 'token' }), + signOut: () => Promise.resolve(), +}; describe('', () => { let entities: Entity[]; const onChange = jest.fn(); const schema = {}; const required = false; - let uiSchema: EntityPickerProps['uiSchema']; - const rawErrors: string[] = []; - const formData = undefined; - - let props: FieldProps; const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - addLocation: jest.fn(), - getLocationByRef: jest.fn(), - removeEntityByUid: jest.fn(), + getEntities: jest.fn(async () => ({ items: entities })), } as any; beforeEach(() => { entities = [ - makeUserEntity('User', ['group1', 'group2'], 'Bob'), - makeGroupEntity('Group', ['Alice', 'Dave'], 'group3'), + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group3' }, + spec: { members: ['Alice'] }, + }, ]; + + onChange.mockClear(); + catalogApi.getEntities.mockClear(); }); - afterEach(() => jest.resetAllMocks()); + afterEach(() => { + jest.resetAllMocks(); + }); - it('should only return the groups a user is part of', async () => { - catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]); + it('should only return the groups a user is part of and not the groups a user is not part of', async () => { + const userGroups = entities.filter( + entity => + entity.spec && + Array.isArray(entity.spec.members) && + entity.spec.members.includes('Bob'), + ); - uiSchema = { 'ui:options': { catalogApi } }; - props = { + catalogApi.getEntities.mockResolvedValue({ items: userGroups }); + + const props = { onChange, schema, required, - uiSchema, - rawErrors, - formData, } as unknown as FieldProps; - render(); + render( + + + , + ); - expect(onChange).toHaveBeenCalledWith(['group1', 'group2']); + await waitFor(() => + expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + ); + + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: { + kind: ['Group'], + 'relations.hasMember': ['group:default/group1', 'group:default/group2'], + }, + }); + + // Check that getEntities was set up to return the correct data + await expect(catalogApi.getEntities.mock.results[0].value).resolves.toEqual( + { + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2' }, + spec: { members: ['Bob'] }, + }, + ], + }, + ); + + await expect( + catalogApi.getEntities.mock.results[0].value, + ).resolves.not.toEqual( + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + metadata: { name: 'group3' }, + }), + ]), + }), + ); }); - it('should not return groups a user is not part of', async () => { - // Mock 'getEntityByRef' to return the entity that represents Bob - catalogApi.getEntityByRef.mockResolvedValueOnce(entities[0]); + it('should display the groups a user is part of and not display the groups a user is not part of', async () => { + const userGroups = entities.filter( + entity => + entity.spec && + Array.isArray(entity.spec.members) && + entity.spec.members.includes('Bob'), + ); + catalogApi.getEntities.mockResolvedValue({ items: userGroups }); - uiSchema = { 'ui:options': { catalogApi } }; - props = { + const props = { onChange, schema, required, - uiSchema, - rawErrors, - formData, } as unknown as FieldProps; - render(); + const { queryByText, getByRole } = render( + + + , + ); - // The onChange should not have been called with 'group3', since Bob is not part of 'group3' - expect(onChange).not.toHaveBeenCalledWith(['group3']); - }); + await waitFor(() => + expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + ); - it('should render without imploding', () => { - uiSchema = { 'ui:options': { catalogApi } }; - props = { - onChange, - schema, - required, - uiSchema, - rawErrors, - formData, - } as unknown as FieldProps; + // Simulate user input + const inputField = getByRole('combobox'); + userEvent.click(inputField); + userEvent.type(inputField, 'group'); - const { container } = render(); - expect(container).not.toBeNull(); + // Wait for the dropdown elements to appear + await waitFor(() => { + const group1Element = queryByText('group1'); + const group2Element = queryByText('group2'); + expect(group1Element).toBeInTheDocument(); + expect(group2Element).toBeInTheDocument(); + }); + + // Assert that 'group3' is not rendered in the component + expect(queryByText('group3')).not.toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx index 5fee57eb86..f89c6ff789 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx @@ -30,12 +30,16 @@ export { OwnershipEntityRefPickerSchema }; export const OwnershipEntityRefPicker = ( props: OwnershipEntityRefPickerProps, ) => { - const { uiSchema, required, rawErrors, formData } = props; + const { + schema: { title, description }, + required, + rawErrors, + } = props; const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); const [groups, setGroups] = useState([]); - const [selectedGroup, setSelectedGroup] = useState(''); + const [selectedGroup, setSelectedGroup] = useState(null); useEffect(() => { const fetchUserGroups = async () => { @@ -63,20 +67,20 @@ export const OwnershipEntityRefPicker = ( 0 && !formData} + error={rawErrors?.length > 0} > setSelectedGroup(value || '')} getOptionLabel={group => group} renderInput={params => ( Date: Tue, 30 May 2023 18:56:20 +0200 Subject: [PATCH 10/17] api-report and changeset Signed-off-by: Peter Macdonald --- .changeset/late-bags-cover.md | 5 +++++ plugins/scaffolder/api-report.md | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .changeset/late-bags-cover.md diff --git a/.changeset/late-bags-cover.md b/.changeset/late-bags-cover.md new file mode 100644 index 0000000000..521a16f2f2 --- /dev/null +++ b/.changeset/late-bags-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added `OwnershipEntityRefPicker` field extension that will display a dropdown of groups a user is part of. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index e50395be05..0785ff0982 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -292,6 +292,27 @@ export const OwnerPickerFieldSchema: FieldSchema< // @public export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; +// @public +export const OwnershipEntityRefPickerFieldExtension: FieldExtensionComponent_2< + string, + { + title?: string | undefined; + description?: string | undefined; + } +>; + +// Warning: (ae-missing-release-tag) "OwnershipEntityRefPickerSchema" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const OwnershipEntityRefPickerSchema: CustomFieldExtensionSchema_2; + +// Warning: (ae-forgotten-export) The symbol "OwnershipEntityRefPickerFieldSchema" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "OwnershipEntityRefPickerUiOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type OwnershipEntityRefPickerUiOptions = + typeof OwnershipEntityRefPickerFieldSchema.uiOptionsType; + // @public export const repoPickerValidation: ( value: string, From 0278da3d5cbaced675572a0c807dc42d0201b50d Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Wed, 31 May 2023 08:44:07 +0200 Subject: [PATCH 11/17] api-report fixed? Signed-off-by: Peter Macdonald --- plugins/scaffolder/api-report.md | 18 +++++++++------- .../fields/OwnershipEntityRefPicker/index.ts | 1 + .../fields/OwnershipEntityRefPicker/schema.ts | 21 +++++++++++++++++++ plugins/scaffolder/src/plugin.tsx | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 0785ff0982..0472f019cc 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -301,15 +301,19 @@ export const OwnershipEntityRefPickerFieldExtension: FieldExtensionComponent_2< } >; -// Warning: (ae-missing-release-tag) "OwnershipEntityRefPickerSchema" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export const OwnershipEntityRefPickerFieldSchema: FieldSchema< + string, + { + title?: string | undefined; + description?: string | undefined; + } +>; + +// @public export const OwnershipEntityRefPickerSchema: CustomFieldExtensionSchema_2; -// Warning: (ae-forgotten-export) The symbol "OwnershipEntityRefPickerFieldSchema" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "OwnershipEntityRefPickerUiOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type OwnershipEntityRefPickerUiOptions = typeof OwnershipEntityRefPickerFieldSchema.uiOptionsType; diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts index 2e744ef6e9..6237038511 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts @@ -17,4 +17,5 @@ export { OwnershipEntityRefPickerSchema, type OwnershipEntityRefPickerUiOptions, + OwnershipEntityRefPickerFieldSchema, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts index bc138bc8f9..6d1f85b62a 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts @@ -17,6 +17,11 @@ import { z } from 'zod'; import { makeFieldSchemaFromZod } from '../utils'; +/** + * Field schema for the OwnershipEntityRefPicker. + * @public + */ + export const OwnershipEntityRefPickerFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ @@ -28,9 +33,25 @@ export const OwnershipEntityRefPickerFieldSchema = makeFieldSchemaFromZod( }), ); +/** + * UI options for the OwnershipEntityRefPicker. + * @public + */ + export type OwnershipEntityRefPickerUiOptions = typeof OwnershipEntityRefPickerFieldSchema.uiOptionsType; +/** + * Props for the OwnershipEntityRefPicker. + * @public + */ + export type OwnershipEntityRefPickerProps = typeof OwnershipEntityRefPickerFieldSchema.type; + +/** + * Schema for the OwnershipEntityRefPicker. + * @public + */ + export const OwnershipEntityRefPickerSchema = OwnershipEntityRefPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index b190cf982a..0a261a7ff0 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -163,7 +163,7 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( ); /** - * A field extension for picking users and groups out of the Catalog. + * A field extension for picking groups a user belongs to out of the catalog. * * @public */ From 67d7412d4fe213dacbadb5468cf2fc8daa4d2e46 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Jun 2023 13:15:47 +0200 Subject: [PATCH 12/17] feat: reworking the picker slightly to just use groups and decorate the meta for now Signed-off-by: blam --- .../OwnershipEntityRefPicker.test.tsx | 129 +++++++++++++----- .../OwnershipEntityRefPicker.tsx | 72 ++++++---- 2 files changed, 146 insertions(+), 55 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx index 81bbb21438..40e7b09d8f 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -22,7 +22,12 @@ import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker'; import { TestApiProvider } from '@backstage/test-utils'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; -import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import { + ErrorApi, + IdentityApi, + errorApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; import userEvent from '@testing-library/user-event'; // Create a mock IdentityApi @@ -52,9 +57,14 @@ describe('', () => { const required = false; const catalogApi: jest.Mocked = { - getEntities: jest.fn(async () => ({ items: entities })), + getEntitiesByRefs: jest.fn(async () => ({ items: entities })), } as any; + const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + beforeEach(() => { entities = [ { @@ -78,7 +88,7 @@ describe('', () => { ]; onChange.mockClear(); - catalogApi.getEntities.mockClear(); + catalogApi.getEntitiesByRefs.mockClear(); }); afterEach(() => { @@ -93,7 +103,7 @@ describe('', () => { entity.spec.members.includes('Bob'), ); - catalogApi.getEntities.mockResolvedValue({ items: userGroups }); + catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); const props = { onChange, @@ -106,6 +116,7 @@ describe('', () => { apis={[ [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], + [errorApiRef, mockErrorApi], ]} > @@ -113,38 +124,35 @@ describe('', () => { ); await waitFor(() => - expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), ); - expect(catalogApi.getEntities).toHaveBeenCalledWith({ - filter: { - kind: ['Group'], - 'relations.hasMember': ['group:default/group1', 'group:default/group2'], - }, + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith({ + entityRefs: ['group:default/group1', 'group:default/group2'], }); // Check that getEntities was set up to return the correct data - await expect(catalogApi.getEntities.mock.results[0].value).resolves.toEqual( - { - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { name: 'group1' }, - spec: { members: ['Bob'] }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { name: 'group2' }, - spec: { members: ['Bob'] }, - }, - ], - }, - ); + await expect( + catalogApi.getEntitiesByRefs.mock.results[0].value, + ).resolves.toEqual({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2' }, + spec: { members: ['Bob'] }, + }, + ], + }); await expect( - catalogApi.getEntities.mock.results[0].value, + catalogApi.getEntitiesByRefs.mock.results[0].value, ).resolves.not.toEqual( expect.objectContaining({ items: expect.arrayContaining([ @@ -163,7 +171,7 @@ describe('', () => { Array.isArray(entity.spec.members) && entity.spec.members.includes('Bob'), ); - catalogApi.getEntities.mockResolvedValue({ items: userGroups }); + catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); const props = { onChange, @@ -176,6 +184,7 @@ describe('', () => { apis={[ [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], + [errorApiRef, mockErrorApi], ]} > @@ -183,7 +192,7 @@ describe('', () => { ); await waitFor(() => - expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), ); // Simulate user input @@ -202,4 +211,62 @@ describe('', () => { // Assert that 'group3' is not rendered in the component expect(queryByText('group3')).not.toBeInTheDocument(); }); + + it('should call the onChange handler with the correct entityRef and and use a nice display name', async () => { + const userGroups = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1', title: 'My First Group' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2', title: 'My Second Group' }, + spec: { members: ['Bob'] }, + }, + ]; + + catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); + + const props = { + onChange, + schema, + required, + } as unknown as FieldProps; + + const { getByRole } = render( + + + , + ); + + await waitFor(() => + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), + ); + + const inputField = getByRole('combobox'); + userEvent.click(inputField); + userEvent.type(inputField, 'group'); + + await waitFor(() => { + const option = getByRole('option', { name: 'My First Group' }); + expect(option).toBeInTheDocument(); + }); + + const option = getByRole('option', { name: 'My First Group' }); + userEvent.click(option); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith('group:default/group1'); + }); + }); }); diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx index f89c6ff789..299e8e66d7 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx @@ -14,8 +14,12 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import React, { useState } from 'react'; +import { + errorApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; import { TextField, FormControl } from '@material-ui/core'; import { OwnershipEntityRefPickerProps, @@ -24,6 +28,8 @@ import { import { Autocomplete } from '@material-ui/lab'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { NotFoundError } from '@backstage/errors'; +import useAsync from 'react-use/lib/useAsync'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; export { OwnershipEntityRefPickerSchema }; @@ -34,34 +40,52 @@ export const OwnershipEntityRefPicker = ( schema: { title, description }, required, rawErrors, + onChange, } = props; const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); - const [groups, setGroups] = useState([]); - const [selectedGroup, setSelectedGroup] = useState(null); + const errorApi = useApi(errorApiRef); + const [groups, setGroups] = useState< + { + label: string; + ref: string; + }[] + >([]); + const [selectedGroup, setSelectedGroup] = useState(null); - useEffect(() => { - const fetchUserGroups = async () => { - const identity = await identityApi.getBackstageIdentity(); - const userIdentity = identity.ownershipEntityRefs; + useAsync(async () => { + const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); - if (!userIdentity) { - throw new NotFoundError('No ownership entity refs found'); - } + if (!ownershipEntityRefs || !ownershipEntityRefs.length) { + errorApi.post(new NotFoundError('No ownership entity refs found')); + return; + } - const userOwnedGroups = await catalogApi.getEntities({ - filter: { - kind: ['Group'], - 'relations.hasMember': userIdentity, - }, - }); - const groupValues = userOwnedGroups.items.map(item => item.metadata.name); - setGroups(groupValues); - }; + const { items } = await catalogApi.getEntitiesByRefs({ + entityRefs: ownershipEntityRefs, + }); - fetchUserGroups(); - }, [identityApi, catalogApi]); + const groupValues = items + .filter((e): e is Entity => Boolean(e)) + .map(item => ({ + label: item.metadata.title ?? item.metadata.name, + ref: stringifyEntityRef(item), + })); + + setGroups(groupValues); + }); + + const updateChange = ( + _: React.ChangeEvent<{}>, + value: { label: string; ref: string } | null, + ) => { + setSelectedGroup(value); + onChange(value?.ref ?? ''); + }; return ( setSelectedGroup(value || '')} - getOptionLabel={group => group} + onChange={updateChange} + getOptionLabel={group => group.label} renderInput={params => ( Date: Tue, 13 Jun 2023 15:01:43 +0200 Subject: [PATCH 13/17] feat: change the lookup to get all groups where the logged in user is a member Signed-off-by: blam --- .../OwnershipEntityRefPicker.test.tsx | 69 ++++++++++--------- .../OwnershipEntityRefPicker.tsx | 13 ++-- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx index 40e7b09d8f..3c01ffda94 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -43,7 +43,7 @@ const mockIdentityApi: IdentityApi = { id: 'Bob', idToken: 'token', type: 'user', - userEntityRef: 'user:default/Bob', + userEntityRef: 'user:default/bob', ownershipEntityRefs: ['group:default/group1', 'group:default/group2'], }), getCredentials: () => Promise.resolve({ token: 'token' }), @@ -57,7 +57,7 @@ describe('', () => { const required = false; const catalogApi: jest.Mocked = { - getEntitiesByRefs: jest.fn(async () => ({ items: entities })), + getEntities: jest.fn(async () => ({ items: entities })), } as any; const mockErrorApi: jest.Mocked = { @@ -88,7 +88,7 @@ describe('', () => { ]; onChange.mockClear(); - catalogApi.getEntitiesByRefs.mockClear(); + catalogApi.getEntities.mockClear(); }); afterEach(() => { @@ -103,7 +103,7 @@ describe('', () => { entity.spec.members.includes('Bob'), ); - catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); + catalogApi.getEntities.mockResolvedValue({ items: userGroups }); const props = { onChange, @@ -124,35 +124,38 @@ describe('', () => { ); await waitFor(() => - expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), + expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), ); - expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith({ - entityRefs: ['group:default/group1', 'group:default/group2'], + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: { + type: 'Group', + 'relations.hasMember': ['user:default/bob'], + }, }); // Check that getEntities was set up to return the correct data - await expect( - catalogApi.getEntitiesByRefs.mock.results[0].value, - ).resolves.toEqual({ - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { name: 'group1' }, - spec: { members: ['Bob'] }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { name: 'group2' }, - spec: { members: ['Bob'] }, - }, - ], - }); + await expect(catalogApi.getEntities.mock.results[0].value).resolves.toEqual( + { + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2' }, + spec: { members: ['Bob'] }, + }, + ], + }, + ); await expect( - catalogApi.getEntitiesByRefs.mock.results[0].value, + catalogApi.getEntities.mock.results[0].value, ).resolves.not.toEqual( expect.objectContaining({ items: expect.arrayContaining([ @@ -171,7 +174,8 @@ describe('', () => { Array.isArray(entity.spec.members) && entity.spec.members.includes('Bob'), ); - catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); + + catalogApi.getEntities.mockResolvedValue({ items: userGroups }); const props = { onChange, @@ -192,7 +196,7 @@ describe('', () => { ); await waitFor(() => - expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), + expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), ); // Simulate user input @@ -228,7 +232,7 @@ describe('', () => { }, ]; - catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); + catalogApi.getEntities.mockResolvedValue({ items: userGroups }); const props = { onChange, @@ -249,7 +253,7 @@ describe('', () => { ); await waitFor(() => - expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), + expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), ); const inputField = getByRole('combobox'); @@ -257,8 +261,9 @@ describe('', () => { userEvent.type(inputField, 'group'); await waitFor(() => { - const option = getByRole('option', { name: 'My First Group' }); - expect(option).toBeInTheDocument(); + expect( + getByRole('option', { name: 'My First Group' }), + ).toBeInTheDocument(); }); const option = getByRole('option', { name: 'My First Group' }); diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx index 299e8e66d7..fe3cc96c0a 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx @@ -58,15 +58,18 @@ export const OwnershipEntityRefPicker = ( }>(null); useAsync(async () => { - const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); + const { userEntityRef } = await identityApi.getBackstageIdentity(); - if (!ownershipEntityRefs || !ownershipEntityRefs.length) { - errorApi.post(new NotFoundError('No ownership entity refs found')); + if (!userEntityRef) { + errorApi.post(new NotFoundError('No user entity ref found')); return; } - const { items } = await catalogApi.getEntitiesByRefs({ - entityRefs: ownershipEntityRefs, + const { items } = await catalogApi.getEntities({ + filter: { + type: 'Group', + ['relations.hasMember']: [userEntityRef], + }, }); const groupValues = items From 7c506d87b0e2b18661ba3ca63652fa484f552214 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Jun 2023 15:08:01 +0200 Subject: [PATCH 14/17] feat: reworking the calls, and changing the name Signed-off-by: blam --- .../MyGroupsPicker.test.tsx} | 10 +++++----- .../MyGroupsPicker.tsx} | 11 +++------- .../index.ts | 6 +++--- .../schema.ts | 20 +++++++++---------- .../scaffolder/src/components/fields/index.ts | 2 +- plugins/scaffolder/src/extensions/default.ts | 13 ++++++------ plugins/scaffolder/src/plugin.tsx | 2 +- 7 files changed, 29 insertions(+), 35 deletions(-) rename plugins/scaffolder/src/components/fields/{OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx => MyGroupsPicker/MyGroupsPicker.test.tsx} (96%) rename plugins/scaffolder/src/components/fields/{OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx => MyGroupsPicker/MyGroupsPicker.tsx} (93%) rename plugins/scaffolder/src/components/fields/{OwnershipEntityRefPicker => MyGroupsPicker}/index.ts (84%) rename plugins/scaffolder/src/components/fields/{OwnershipEntityRefPicker => MyGroupsPicker}/schema.ts (64%) diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx similarity index 96% rename from plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx rename to plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx index 3c01ffda94..0bdfe5f153 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { CatalogApi } from '@backstage/catalog-client'; import { FieldProps } from '@rjsf/core'; -import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker'; +import { MyGroupsPicker } from './MyGroupsPicker'; import { TestApiProvider } from '@backstage/test-utils'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; @@ -50,7 +50,7 @@ const mockIdentityApi: IdentityApi = { signOut: () => Promise.resolve(), }; -describe('', () => { +describe('', () => { let entities: Entity[]; const onChange = jest.fn(); const schema = {}; @@ -119,7 +119,7 @@ describe('', () => { [errorApiRef, mockErrorApi], ]} > - + , ); @@ -191,7 +191,7 @@ describe('', () => { [errorApiRef, mockErrorApi], ]} > - + , ); @@ -248,7 +248,7 @@ describe('', () => { [errorApiRef, mockErrorApi], ]} > - + , ); diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx similarity index 93% rename from plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx rename to plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx index fe3cc96c0a..28bc2b93fa 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx @@ -21,21 +21,16 @@ import { useApi, } from '@backstage/core-plugin-api'; import { TextField, FormControl } from '@material-ui/core'; -import { - OwnershipEntityRefPickerProps, - OwnershipEntityRefPickerSchema, -} from './schema'; +import { MyGroupsPickerProps, MyGroupsPickerSchema } from './schema'; import { Autocomplete } from '@material-ui/lab'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { NotFoundError } from '@backstage/errors'; import useAsync from 'react-use/lib/useAsync'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -export { OwnershipEntityRefPickerSchema }; +export { MyGroupsPickerSchema }; -export const OwnershipEntityRefPicker = ( - props: OwnershipEntityRefPickerProps, -) => { +export const MyGroupsPicker = (props: MyGroupsPickerProps) => { const { schema: { title, description }, required, diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts b/plugins/scaffolder/src/components/fields/MyGroupsPicker/index.ts similarity index 84% rename from plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts rename to plugins/scaffolder/src/components/fields/MyGroupsPicker/index.ts index 6237038511..00be19cf96 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/index.ts @@ -15,7 +15,7 @@ */ export { - OwnershipEntityRefPickerSchema, - type OwnershipEntityRefPickerUiOptions, - OwnershipEntityRefPickerFieldSchema, + MyGroupsPickerSchema, + type MyGroupsPickerUiOptions, + MyGroupsPickerFieldSchema, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts b/plugins/scaffolder/src/components/fields/MyGroupsPicker/schema.ts similarity index 64% rename from plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts rename to plugins/scaffolder/src/components/fields/MyGroupsPicker/schema.ts index 6d1f85b62a..d5f8af7c56 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/schema.ts @@ -18,11 +18,11 @@ import { z } from 'zod'; import { makeFieldSchemaFromZod } from '../utils'; /** - * Field schema for the OwnershipEntityRefPicker. + * Field schema for the MyGroupsPicker. * @public */ -export const OwnershipEntityRefPickerFieldSchema = makeFieldSchemaFromZod( +export const MyGroupsPickerFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ title: z.string().default('Group').describe('Group'), @@ -34,24 +34,22 @@ export const OwnershipEntityRefPickerFieldSchema = makeFieldSchemaFromZod( ); /** - * UI options for the OwnershipEntityRefPicker. + * UI options for the MyGroupsPicker. * @public */ -export type OwnershipEntityRefPickerUiOptions = - typeof OwnershipEntityRefPickerFieldSchema.uiOptionsType; +export type MyGroupsPickerUiOptions = + typeof MyGroupsPickerFieldSchema.uiOptionsType; /** - * Props for the OwnershipEntityRefPicker. + * Props for the MyGroupsPicker. * @public */ -export type OwnershipEntityRefPickerProps = - typeof OwnershipEntityRefPickerFieldSchema.type; +export type MyGroupsPickerProps = typeof MyGroupsPickerFieldSchema.type; /** - * Schema for the OwnershipEntityRefPicker. + * Schema for the MyGroupsPicker. * @public */ -export const OwnershipEntityRefPickerSchema = - OwnershipEntityRefPickerFieldSchema.schema; +export const MyGroupsPickerSchema = MyGroupsPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 67bb126ede..7ee4448f6b 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,5 +18,5 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; -export * from './OwnershipEntityRefPicker'; +export * from './MyGroupsPicker'; export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index dae89266d0..ef827cfaca 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -40,9 +40,10 @@ import { OwnedEntityPickerSchema, } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; import { - OwnershipEntityRefPicker, - OwnershipEntityRefPickerSchema, -} from '../components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker'; + MyGroupsPicker, + MyGroupsPickerSchema, +} from '../components/fields/MyGroupsPicker/MyGroupsPicker'; +import { MyGroupsPickerFieldSchema } from '../components'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -78,8 +79,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ schema: OwnedEntityPickerSchema, }, { - component: OwnershipEntityRefPicker, - name: 'OwnershipEntityRefPicker', - schema: OwnershipEntityRefPickerSchema, + component: MyGroupsPicker, + name: 'MyGroupsPicker', + schema: MyGroupsPickerFieldSchema, }, ]; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 0a261a7ff0..674941eaa0 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -67,7 +67,7 @@ import { import { OwnershipEntityRefPicker, OwnershipEntityRefPickerSchema, -} from './components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker'; +} from './components/fields/MyGroupsPicker/MyGroupsPicker'; /** * The main plugin export for the scaffolder. From 15a681acc36bb3fedeb32dda48f6b499a92f58f2 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Jun 2023 17:03:26 +0200 Subject: [PATCH 15/17] chore: fixing typescript Signed-off-by: blam --- plugins/scaffolder/src/plugin.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 674941eaa0..ad9e70d843 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -65,8 +65,8 @@ import { editRouteRef, } from './routes'; import { - OwnershipEntityRefPicker, - OwnershipEntityRefPickerSchema, + MyGroupsPicker, + MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; /** @@ -167,11 +167,11 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( * * @public */ -export const OwnershipEntityRefPickerFieldExtension = scaffolderPlugin.provide( +export const MyGroupsPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ - component: OwnershipEntityRefPicker, - name: 'OwnershipEntityRefPicker', - schema: OwnershipEntityRefPickerSchema, + component: MyGroupsPicker, + name: 'MyGroupsPicker', + schema: MyGroupsPickerSchema, }), ); From 5d695c341832baf2236ed0329d31b798e1978083 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Jun 2023 17:04:03 +0200 Subject: [PATCH 16/17] chore: fixing changeset Signed-off-by: blam --- .changeset/late-bags-cover.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/late-bags-cover.md b/.changeset/late-bags-cover.md index 521a16f2f2..8fd5bd35ca 100644 --- a/.changeset/late-bags-cover.md +++ b/.changeset/late-bags-cover.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': minor --- -Added `OwnershipEntityRefPicker` field extension that will display a dropdown of groups a user is part of. +Added `MyGroupsPicker` field extension that will display a dropdown of groups a user is part of. From 65bff8df26862986da76ee29936710fdeea8a72e Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Jun 2023 17:08:00 +0200 Subject: [PATCH 17/17] chore: fixing api-reprots Signed-off-by: blam --- plugins/scaffolder/api-report.md | 50 ++++++++++---------- plugins/scaffolder/src/extensions/default.ts | 3 +- plugins/scaffolder/src/index.ts | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 0472f019cc..48f24868de 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -207,6 +207,31 @@ export function makeFieldSchemaFromZod< : never >; +// @public +export const MyGroupsPickerFieldExtension: FieldExtensionComponent_2< + string, + { + title?: string | undefined; + description?: string | undefined; + } +>; + +// @public +export const MyGroupsPickerFieldSchema: FieldSchema< + string, + { + title?: string | undefined; + description?: string | undefined; + } +>; + +// @public +export const MyGroupsPickerSchema: CustomFieldExtensionSchema_2; + +// @public +export type MyGroupsPickerUiOptions = + typeof MyGroupsPickerFieldSchema.uiOptionsType; + // @public export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< string, @@ -292,31 +317,6 @@ export const OwnerPickerFieldSchema: FieldSchema< // @public export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; -// @public -export const OwnershipEntityRefPickerFieldExtension: FieldExtensionComponent_2< - string, - { - title?: string | undefined; - description?: string | undefined; - } ->; - -// @public -export const OwnershipEntityRefPickerFieldSchema: FieldSchema< - string, - { - title?: string | undefined; - description?: string | undefined; - } ->; - -// @public -export const OwnershipEntityRefPickerSchema: CustomFieldExtensionSchema_2; - -// @public -export type OwnershipEntityRefPickerUiOptions = - typeof OwnershipEntityRefPickerFieldSchema.uiOptionsType; - // @public export const repoPickerValidation: ( value: string, diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index ef827cfaca..013f242a12 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -43,7 +43,6 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from '../components/fields/MyGroupsPicker/MyGroupsPicker'; -import { MyGroupsPickerFieldSchema } from '../components'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -81,6 +80,6 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { component: MyGroupsPicker, name: 'MyGroupsPicker', - schema: MyGroupsPickerFieldSchema, + schema: MyGroupsPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 7ec9717bdc..ee91d2988c 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -27,7 +27,7 @@ export { EntityTagsPickerFieldExtension, OwnerPickerFieldExtension, OwnedEntityPickerFieldExtension, - OwnershipEntityRefPickerFieldExtension, + MyGroupsPickerFieldExtension, RepoUrlPickerFieldExtension, ScaffolderPage, scaffolderPlugin,