diff --git a/.changeset/moody-hounds-rest.md b/.changeset/moody-hounds-rest.md new file mode 100644 index 000000000..56c07eb04 --- /dev/null +++ b/.changeset/moody-hounds-rest.md @@ -0,0 +1,5 @@ +--- +'@roadiehq/backstage-plugin-shortcut': minor +--- + +Add an entity card for the shortcut plugin. diff --git a/plugins/frontend/backstage-plugin-shortcut/src/api/ShortcutClient.ts b/plugins/frontend/backstage-plugin-shortcut/src/api/ShortcutClient.ts index 13e2e9372..6c2e650a0 100644 --- a/plugins/frontend/backstage-plugin-shortcut/src/api/ShortcutClient.ts +++ b/plugins/frontend/backstage-plugin-shortcut/src/api/ShortcutClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Story, User } from './types'; +import { Story, StoryResponse, User } from './types'; import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api'; const DEFAULT_PROXY_PATH = '/shortcut/api'; @@ -42,15 +42,29 @@ export class ShortcutClient { return proxyUrl + this.proxyPath; } - async fetchStories({ owner }: { owner?: string }): Promise { - const response = await fetch( - `${await this.getApiUrl()}/search/stories?page_size=25&query=owner:${owner}`, - ); + async fetch({ path }: { path: string }): Promise { + const response = await fetch(`${await this.getApiUrl()}${path}`); const payload = await response.json(); if (!response.ok) { + if (payload.message) { + throw new Error(payload.message); + } throw new Error(payload.errors[0]); } - return payload.data; + + return payload; + } + + async fetchStories({ query }: { query: string }): Promise { + const encodedQuery = encodeURIComponent(query); + return await this.fetch({ + path: `/search/stories?page_size=25&query=${encodedQuery}`, + }); + } + + async fetchOwnedStories({ owner }: { owner?: string }): Promise { + const query = `owner:${owner}`; + return (await this.fetchStories({ query })).data; } async getUsers(): Promise { diff --git a/plugins/frontend/backstage-plugin-shortcut/src/api/types.ts b/plugins/frontend/backstage-plugin-shortcut/src/api/types.ts index fa9329a41..0e04c9299 100644 --- a/plugins/frontend/backstage-plugin-shortcut/src/api/types.ts +++ b/plugins/frontend/backstage-plugin-shortcut/src/api/types.ts @@ -26,6 +26,12 @@ export type Story = { app_url: string; }; +export type StoryResponse = { + next?: string; + total: number; + data: Array; +}; + export type User = { id: string; profile: Profile; diff --git a/plugins/frontend/backstage-plugin-shortcut/src/components/ComponentExtensions/EntityStoriesCard.tsx b/plugins/frontend/backstage-plugin-shortcut/src/components/ComponentExtensions/EntityStoriesCard.tsx new file mode 100644 index 000000000..0619c0e42 --- /dev/null +++ b/plugins/frontend/backstage-plugin-shortcut/src/components/ComponentExtensions/EntityStoriesCard.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2024 Larder Software Limited + * + * 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 { ErrorPanel, Table, TableColumn } from '@backstage/core-components'; +import SyncIcon from '@material-ui/icons/Sync'; +import { shortcutApiRef } from '../../api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useAsyncRetry } from 'react-use'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { SHORTCUT_QUERY_ANNOTATION } from '../../constants'; +import { Story, User } from '../../api/types'; + +const columnsBuilder: (users?: User[]) => TableColumn[] = ( + users?: User[], +) => [ + { + title: 'Name', + field: 'name', + }, + { + title: 'Status', + render: story => (story.started ? <>In progress : <>'Not started'), + }, + { + title: 'Owners', + render: story => { + return users + ?.filter(user => story.owner_ids.includes(user.id)) + .map(user => user.profile.name) + .join(', '); + }, + }, +]; + +export const EntityStoriesCard = (props: { + title?: string; + additionalQuery?: string; +}) => { + const shortcutApi = useApi(shortcutApiRef); + const { entity } = useEntity(); + + const { + value: data, + retry, + error, + loading, + } = useAsyncRetry(async () => { + let query = entity.metadata.annotations?.[SHORTCUT_QUERY_ANNOTATION]; + if (props.additionalQuery) { + query = `${query} ${props.additionalQuery}`; + } + if (query) { + return (await shortcutApi.fetchStories({ query })).data; + } + return []; + }); + + const { value: users } = useAsyncRetry(async () => { + return shortcutApi.getUsers(); + }); + + if (error) { + return ; + } + return ( + , + tooltip: 'Refresh', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + /> + ); +}; diff --git a/plugins/frontend/backstage-plugin-shortcut/src/components/Home/StoriesCardHomepage.tsx b/plugins/frontend/backstage-plugin-shortcut/src/components/Home/StoriesCardHomepage.tsx index 2025483f5..ed4c794b5 100644 --- a/plugins/frontend/backstage-plugin-shortcut/src/components/Home/StoriesCardHomepage.tsx +++ b/plugins/frontend/backstage-plugin-shortcut/src/components/Home/StoriesCardHomepage.tsx @@ -224,7 +224,7 @@ const StoriesCardContent = () => { user => user.profile.email_address === profile.email, )?.profile.mention_name; - const stories = await api.fetchStories({ + const stories = await api.fetchOwnedStories({ owner: loggedUser ? loggedUser : undefined, }); diff --git a/plugins/frontend/backstage-plugin-shortcut/src/constants.ts b/plugins/frontend/backstage-plugin-shortcut/src/constants.ts new file mode 100644 index 000000000..2980bd3dc --- /dev/null +++ b/plugins/frontend/backstage-plugin-shortcut/src/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Larder Software Limited + * + * 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 const SHORTCUT_QUERY_ANNOTATION = 'shortcut.com/story-query'; diff --git a/plugins/frontend/backstage-plugin-shortcut/src/index.ts b/plugins/frontend/backstage-plugin-shortcut/src/index.ts index d588613c9..f6a88fb43 100644 --- a/plugins/frontend/backstage-plugin-shortcut/src/index.ts +++ b/plugins/frontend/backstage-plugin-shortcut/src/index.ts @@ -14,6 +14,12 @@ * limitations under the License. */ -export { backstagePluginShortcutPlugin, HomepageStoriesCard } from './plugin'; +export { + backstagePluginShortcutPlugin, + HomepageStoriesCard, + EntityShortcutStoriesCard, +} from './plugin'; export * from './api'; export * from './components/Home'; +export * from './constants'; +export * from './isShortcutAvailable'; diff --git a/plugins/frontend/backstage-plugin-shortcut/src/isShortcutAvailable.ts b/plugins/frontend/backstage-plugin-shortcut/src/isShortcutAvailable.ts new file mode 100644 index 000000000..03f8367b9 --- /dev/null +++ b/plugins/frontend/backstage-plugin-shortcut/src/isShortcutAvailable.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Larder Software Limited + * + * 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 { Entity } from '@backstage/catalog-model'; +import { SHORTCUT_QUERY_ANNOTATION } from './constants'; + +export const isShortcutAvailable = (entity: Entity) => + Boolean(entity?.metadata.annotations?.[SHORTCUT_QUERY_ANNOTATION]); diff --git a/plugins/frontend/backstage-plugin-shortcut/src/plugin.ts b/plugins/frontend/backstage-plugin-shortcut/src/plugin.ts index 65abe41b8..8186c0876 100644 --- a/plugins/frontend/backstage-plugin-shortcut/src/plugin.ts +++ b/plugins/frontend/backstage-plugin-shortcut/src/plugin.ts @@ -17,6 +17,7 @@ import { createApiFactory, createPlugin, discoveryApiRef, + createComponentExtension, } from '@backstage/core-plugin-api'; import { createCardExtension } from '@backstage/plugin-home'; import { shortcutApiRef, ShortcutClient } from './api'; @@ -44,3 +45,15 @@ export const HomepageStoriesCard = backstagePluginShortcutPlugin.provide( components: () => import('./components/Home/StoriesCardHomepage'), }), ); + +export const EntityShortcutStoriesCard = backstagePluginShortcutPlugin.provide( + createComponentExtension({ + name: 'EntityStoriesCard ', + component: { + lazy: () => + import('./components/ComponentExtensions/EntityStoriesCard').then( + m => m.EntityStoriesCard, + ), + }, + }), +);