diff --git a/.changeset/twenty-trees-travel.md b/.changeset/twenty-trees-travel.md new file mode 100644 index 0000000000..7f65084320 --- /dev/null +++ b/.changeset/twenty-trees-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Use the OWNED_BY relation and compare it to the users MEMBER_OF relation. The user entity is searched by name, based on the userId of the identity. diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 83bf4a6f6e..28fb1b08c7 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -15,7 +15,11 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { Entity } from '@backstage/catalog-model'; +import { + Entity, + RELATION_MEMBER_OF, + RELATION_OWNED_BY, +} from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, @@ -46,6 +50,12 @@ describe('CatalogPage', () => { owner: 'tools@example.com', type: 'service', }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { kind: 'Group', name: 'tools', namespace: 'default' }, + }, + ], }, { apiVersion: 'backstage.io/v1alpha1', @@ -57,11 +67,34 @@ describe('CatalogPage', () => { owner: 'not-tools@example.com', type: 'service', }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'not-tools', + namespace: 'default', + }, + }, + ], }, ] as Entity[], }), getLocationByEntity: () => Promise.resolve({ id: 'id', type: 'github', target: 'url' }), + getEntityByName: async entityName => { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: entityName.name }, + relations: [ + { + type: RELATION_MEMBER_OF, + target: { namespace: 'default', kind: 'Group', name: 'tools' }, + }, + ], + }; + }, }; const testProfile: Partial = { displayName: 'Display Name', diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index e73a789380..5b728895b8 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -19,7 +19,6 @@ import { Content, ContentHeader, errorApiRef, - identityApiRef, SupportButton, useApi, } from '@backstage/core'; @@ -38,6 +37,8 @@ import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import CatalogLayout from './CatalogLayout'; import { CatalogTabs, LabeledComponentType } from './CatalogTabs'; import { WelcomeBanner } from './WelcomeBanner'; +import { useUserGroups } from '../useUserGroups'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -65,7 +66,6 @@ const CatalogPageContents = () => { const catalogApi = useApi(catalogApiRef); const errorApi = useApi(errorApiRef); const { isStarredEntity } = useStarredEntities(); - const userId = useApi(identityApiRef).getUserId(); const [selectedTab, setSelectedTab] = useState(); const [selectedSidebarItem, setSelectedSidebarItem] = useState(); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; @@ -112,6 +112,8 @@ const CatalogPageContents = () => { [], ); + const { groups } = useUserGroups(); + const filterGroups = useMemo( () => [ { @@ -121,7 +123,16 @@ const CatalogPageContents = () => { id: 'owned', label: 'Owned', icon: SettingsIcon, - filterFn: entity => entity.spec?.owner === userId, + filterFn: entity => { + const ownerRelation = entity.relations?.find( + r => + r.type === RELATION_OWNED_BY && + r.target.kind.toLowerCase() === 'group', + ); + return ( + !!ownerRelation && groups.includes(ownerRelation.target.name) + ); + }, }, { id: 'starred', @@ -142,7 +153,7 @@ const CatalogPageContents = () => { ], }, ], - [isStarredEntity, userId, orgName], + [isStarredEntity, orgName, groups], ); const showAddExampleEntities = diff --git a/plugins/catalog/src/components/useUserGroups.ts b/plugins/catalog/src/components/useUserGroups.ts new file mode 100644 index 0000000000..c91a449837 --- /dev/null +++ b/plugins/catalog/src/components/useUserGroups.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { useAsync } from 'react-use'; +import { useMemo } from 'react'; +import { RELATION_MEMBER_OF } from '@backstage/catalog-model'; +import { identityApiRef, useApi } from '@backstage/core'; +import { catalogApiRef } from '../plugin'; + +/** + * Get the group memberships of the logged-in user. + */ +export const useUserGroups: () => { + groups: string[]; + loading: boolean; + error?: Error; +} = () => { + const catalogApi = useApi(catalogApiRef); + const userId = useApi(identityApiRef).getUserId(); + + // TODO: should the identityApiRef already include the entity? or at least a full EntityName? + const { value: user, loading, error } = useAsync(async () => { + return await catalogApi.getEntityByName({ + kind: 'User', + namespace: 'default', + name: userId, + }); + }, [catalogApi, userId]); + + // calculate the group memberships + const groups = useMemo(() => { + if (user && user.relations) { + return user.relations + .filter( + r => + r.type === RELATION_MEMBER_OF && + r.target.kind.toLowerCase() === 'group', + ) + .map(r => r.target.name); + } + + return []; + }, [user]); + + return { groups, loading, error }; +};