From d7b0a117686ba30d67b7d6e36a57d97ea0ae075b Mon Sep 17 00:00:00 2001 From: Joe Patterson Date: Mon, 17 Oct 2022 11:02:15 +1000 Subject: [PATCH] updating the pr on the feedback to add docs, remove some items not needed including the preview html Signed-off-by: Joe Patterson --- .../software-catalog/external-integrations.md | 126 ++++++++++++++++++ packages/catalog-model/api-report.md | 19 ++- .../src/kinds/GroupEntityV1alpha1.ts | 12 +- .../src/kinds/UserEntityV1alpha1.ts | 11 +- packages/catalog-model/src/kinds/index.ts | 2 + .../GroupProfile/GroupProfileCard.stories.tsx | 2 +- .../Group/GroupProfile/GroupProfileCard.tsx | 49 +------ .../src/components/Cards/Meta/LinksGroup.tsx | 69 ++++++++++ .../Cards/Meta/ProfileInfoGroup.tsx | 56 ++++++++ .../org/src/components/Cards/Meta/index.ts | 17 +++ .../UserProfileCard.stories.tsx | 2 +- .../User/UserProfileCard/UserProfileCard.tsx | 50 +------ storybook/.storybook/preview-head.html | 4 - 13 files changed, 302 insertions(+), 117 deletions(-) create mode 100644 plugins/org/src/components/Cards/Meta/LinksGroup.tsx create mode 100644 plugins/org/src/components/Cards/Meta/ProfileInfoGroup.tsx create mode 100644 plugins/org/src/components/Cards/Meta/index.ts delete mode 100644 storybook/.storybook/preview-head.html diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index 40f917e2f3..f074f3a641 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -261,6 +261,132 @@ the `connect` call has been made to the provider. Start up the backend - it should now start reading from the previously registered location and you'll see your entities start to appear in Backstage. +### Example User Entity Provider + +If you have a 3rd party entity provider such as an internal HR system that you wish to use you are not limited to using our entity providers, (or simply wish to add to existing entity providers with your own data). + +We can create an entity provider to read entities that are based off that provider. + +We create a basic entity provider as shown above. In the example below we might want to extract our users from an HR system, I am assuming the HR system already has the slackUserId to get that information please see the [Slack Api](https://api.slack.com/methods). + +```typescript +import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, +} from '@backstage/catalog-model' +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend' +import { WebClient } from '@slack/web-api' +import {kebabCase} from 'lodash' + +interface Staff { + displayName: string + slackUserId: string + jobTitle: string + photoUrl: string + address: string + email:string +} + +export class UserEntityProvider implements EntityProvider { + private readonly getStaffUrl: string + protected readonly slackTeam: string + protected readonly slackToken: string + protected connection?: EntityProviderConnection + + static fromConfig(config: Config, options: { logger: Logger }) { + const getStaffUrl = config.getString('staff.url') + const slackToken = config.getString('slack.token') + const slackTeam = config.getString('slack.team') + return new UserEntityProvider({ + ...options, + getStaffUrl, + slackToken, + slackTeam, + }) + } + + private constructor(options: { + getStaffUrl: string + slackToken: string + slackTeam: string + }) { + this.getStaffUrl = options.getStaffUrl + this.slackToken = options.slackToken + this.slackTeam = options.slackTeam + } + + async getAllStaff(): Promise{ + await return axios.get(this.getStaffUrl) + } + + public async connect(connection: EntityProviderConnection): Promise { + this.connection = connection + } + + async run(): Promise { + if (!this.connection) { + throw new Error('USer Connection Not initialized') + } + + const userResources: UserEntity[] = [] + const staff = await this.getAllStaff() + + for (const user of staff) { + // we can add any links here in this case it would be adding a slack link to the users so you can directly slack them. + const links = + user.slackUserId != null && user.slackUserId.length > 0 + ? [ + { + url: `slack://user?team=${this.slackTeam}&id=${user.slackUserId}`, + title: 'Slack', + icon: 'message', + }, + ] + : undefined + const userEntity: UserEntity = { + kind: 'User', + apiVersion: 'backstage.io/v1alpha1', + metadata: { + annotations: { + [ANNOTATION_LOCATION]: 'hr-user-https://www.hrurl.com/', + [ANNOTATION_ORIGIN_LOCATION]: 'hr-user-https://www.hrurl.com/', + }, + links, + // name of the entity + name: kebabCase(user.displayName as string), + // name for display purposes could be anything including email + title: user.displayName as string, + }, + spec: { + profile: { + displayName: user.displayName as string, + email: user.email, + picture: user.photoUrl ?? 'fake', + // we can add any string/string here and it will be displayed on a user profile card, eg Job Title, Address, or any other information you want displayed + 'Job Title': user.jobTitle as string, + 'Address': user.address, + }, + memberOf: [], + }, + } + + userResources.push(userEntity) + } + + await this.connection.applyMutation({ + type: 'full', + entities: userResources.map((entity) => ({ + entity, + locationKey: 'hr-user-https://www.hrurl.com/', + })), + }) +} + +``` + ## Custom Processors The other possible way of ingesting data into the catalog is through the use of diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 1a58dcb5ca..da4c335051 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -269,6 +269,13 @@ export { GroupEntityV1alpha1 }; // @public export const groupEntityV1alpha1Validator: KindValidator; +// @public +export type GroupProfile = Record & { + displayName?: string; + email?: string; + picture?: string; +}; + // @public (undocumented) export function isApiEntity(entity: Entity): entity is ApiEntityV1alpha1; @@ -497,6 +504,13 @@ export { UserEntityV1alpha1 }; // @public export const userEntityV1alpha1Validator: KindValidator; +// @public +export type UserProfile = Record & { + displayName?: string; + email?: string; + picture?: string; +}; + // @public export type Validators = { isValidApiVersion(value: unknown): boolean; @@ -509,9 +523,4 @@ export type Validators = { isValidAnnotationValue(value: unknown): boolean; isValidTag(value: unknown): boolean; }; - -// Warnings were encountered during analysis: -// -// src/kinds/GroupEntityV1alpha1.d.ts:23:9 - (ae-forgotten-export) The symbol "GroupProfile" needs to be exported by the entry point index.d.ts -// src/kinds/UserEntityV1alpha1.d.ts:22:9 - (ae-forgotten-export) The symbol "UserProfile" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 51f5f30f04..50304e5220 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -18,18 +18,16 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Group.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -export interface GroupProfileStatic { - displayName?: string; - email?: string; - picture?: string; -} - /** * Backstage Group Profile. * * @public */ -export type GroupProfile = Record & GroupProfileStatic; +export type GroupProfile = Record & { + displayName?: string; + email?: string; + picture?: string; +}; /** * Backstage catalog Group kind Entity. diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index 39d3861702..9e034ad6b3 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -18,17 +18,16 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/User.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -export interface UserProfileStatic { - displayName?: string; - email?: string; - picture?: string; -} /** * Backstage User Profile. * * @public */ -export type UserProfile = Record & UserProfileStatic; +export type UserProfile = Record & { + displayName?: string; + email?: string; + picture?: string; +}; /** * Backstage catalog User kind Entity. diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 7211c25a8d..dd9d0545c6 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -33,6 +33,7 @@ export { groupEntityV1alpha1Validator } from './GroupEntityV1alpha1'; export type { GroupEntityV1alpha1 as GroupEntity, GroupEntityV1alpha1, + GroupProfile, } from './GroupEntityV1alpha1'; export { locationEntityV1alpha1Validator } from './LocationEntityV1alpha1'; export type { @@ -55,4 +56,5 @@ export { userEntityV1alpha1Validator } from './UserEntityV1alpha1'; export type { UserEntityV1alpha1 as UserEntity, UserEntityV1alpha1, + UserProfile, } from './UserEntityV1alpha1'; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx index 7d28b5a2fa..264c4081d7 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -98,7 +98,7 @@ const extraDetailsEntity: GroupEntity = { { url: 'slack://user?team=T00000000&id=U00000000', title: 'Slack', - icon: 'message', + icon: 'chat', }, { url: 'https://www.google.com', diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index c12b4b8906..e6cd183e8f 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -37,13 +37,10 @@ import { ListItemText, Tooltip, IconButton, - Divider, } from '@material-ui/core'; -import Icon from '@material-ui/core/Icon'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; -import LinkIcon from '@material-ui/icons/Link'; import EditIcon from '@material-ui/icons/Edit'; import CachedIcon from '@material-ui/icons/Cached'; import Alert from '@material-ui/lab/Alert'; @@ -55,8 +52,7 @@ import { Link, } from '@backstage/core-components'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; - -const staticProfileKeys = ['displayName', 'email', 'picture']; +import { LinksGroup, ProfileInfoGroup } from '../../Meta'; const CardTitle = (props: { title: string }) => ( @@ -99,11 +95,6 @@ export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { const entityMetadataEditUrl = group.metadata.annotations?.[ANNOTATION_EDIT_URL]; - const profileKeys = - profile !== undefined - ? Object.keys(profile).filter(key => !staticProfileKeys.includes(key)) - : []; - const displayName = profile?.displayName ?? name; const emailHref = profile?.email ? `mailto:${profile.email}` : '#'; const infoCardAction = entityMetadataEditUrl ? ( @@ -200,42 +191,8 @@ export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { secondary="Child Groups" /> - {links !== undefined && } - {links !== undefined && - links.map(link => { - return ( - - {link.icon ? ( - - - {link.icon} - - - ) : ( - - - - )} - {link.title} - - ); - })} - {profile !== undefined && profileKeys.length > 0 && } - {profile !== undefined && - profileKeys.length > 0 && - profileKeys.map(key => { - const value = profile[key]; - - return ( - - - {key} - - - {value} - - ); - })} + + diff --git a/plugins/org/src/components/Cards/Meta/LinksGroup.tsx b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx new file mode 100644 index 0000000000..e826c1cf9c --- /dev/null +++ b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2022 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 { EntityLink } from '@backstage/catalog-model'; +import { IconComponent, useApp } from '@backstage/core-plugin-api'; +import LanguageIcon from '@material-ui/icons/Language'; +import { + ListItem, + ListItemIcon, + ListItemText, + Divider, +} from '@material-ui/core'; +import React, { useCallback } from 'react'; + +const WebLink = ({ + href, + Icon, + text, +}: { + href: string; + text?: string; + Icon?: IconComponent; +}) => ( + + {Icon ? : } + {text} + +); + +export const LinksGroup = ({ links }: { links?: EntityLink[] }) => { + const app = useApp(); + const iconResolver = useCallback( + (key?: string): IconComponent => + key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon, + [app], + ); + + if (links === undefined) { + return null; + } + + return ( + <> + + {links.map(link => { + return ( + + ); + })} + + ); +}; diff --git a/plugins/org/src/components/Cards/Meta/ProfileInfoGroup.tsx b/plugins/org/src/components/Cards/Meta/ProfileInfoGroup.tsx new file mode 100644 index 0000000000..177983d4a5 --- /dev/null +++ b/plugins/org/src/components/Cards/Meta/ProfileInfoGroup.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2022 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 { ListItem, ListItemText, Divider } from '@material-ui/core'; +import React, { useMemo } from 'react'; + +const staticProfileKeys = ['displayName', 'email', 'picture']; + +export const ProfileInfoGroup = ({ + profile, +}: { + profile?: Record; +}) => { + const profileKeys = useMemo( + () => + profile !== undefined + ? Object.keys(profile).filter(key => !staticProfileKeys.includes(key)) + : [], + [profile], + ); + + if (profile === undefined || profileKeys.length === 0) { + return null; + } + + return ( + <> + + {profileKeys.map(key => { + const value = profile[key]; + + return ( + + + {key} + + + {value} + + ); + })} + + ); +}; diff --git a/plugins/org/src/components/Cards/Meta/index.ts b/plugins/org/src/components/Cards/Meta/index.ts new file mode 100644 index 0000000000..5f4e5b4690 --- /dev/null +++ b/plugins/org/src/components/Cards/Meta/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 * from './LinksGroup'; +export * from './ProfileInfoGroup'; diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx index 28fc3d3831..7abfbee389 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -113,7 +113,7 @@ const extraDetailsEntity: UserEntity = { { url: 'slack://user?team=T00000000&id=U00000000', title: 'Slack', - icon: 'message', + icon: 'chat', }, { url: 'https://www.google.com', diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 68a27764d8..8a56dc58c2 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -33,15 +33,12 @@ import { ListItemIcon, ListItemText, Tooltip, - Divider, } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import PersonIcon from '@material-ui/icons/Person'; -import LinkIcon from '@material-ui/icons/Link'; import Alert from '@material-ui/lab/Alert'; -import Icon from '@material-ui/core/Icon'; import React from 'react'; import { Avatar, @@ -49,8 +46,7 @@ import { InfoCardVariants, Link, } from '@backstage/core-components'; - -const staticProfileKeys = ['displayName', 'email', 'picture']; +import { LinksGroup, ProfileInfoGroup } from '../../Meta'; const CardTitle = (props: { title?: string }) => props.title ? ( @@ -80,11 +76,6 @@ export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { kind: 'Group', }); - const profileKeys = - profile !== undefined - ? Object.keys(profile).filter(key => !staticProfileKeys.includes(key)) - : []; - return ( } @@ -139,43 +130,8 @@ export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { - {links !== undefined && } - {links !== undefined && - links.map(link => { - return ( - - {link.icon ? ( - - - {link.icon} - - - ) : ( - - - - )} - {link.title} - - ); - })} - - {profile !== undefined && profileKeys.length > 0 && } - {profile !== undefined && - profileKeys.length > 0 && - profileKeys.map(key => { - const value = profile[key]; - - return ( - - - {key} - - - {value} - - ); - })} + + diff --git a/storybook/.storybook/preview-head.html b/storybook/.storybook/preview-head.html deleted file mode 100644 index a21b9971e8..0000000000 --- a/storybook/.storybook/preview-head.html +++ /dev/null @@ -1,4 +0,0 @@ -