Skip to content

Commit

Permalink
Merge pull request #1503 from RoadieHQ/shortcut-entity-page
Browse files Browse the repository at this point in the history
Add a component card for the shortcut plugin
  • Loading branch information
punkle authored Jul 26, 2024
2 parents c5af1c5 + fd847ee commit c487e2b
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-hounds-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roadiehq/backstage-plugin-shortcut': minor
---

Add an entity card for the shortcut plugin.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,15 +42,29 @@ export class ShortcutClient {
return proxyUrl + this.proxyPath;
}

async fetchStories({ owner }: { owner?: string }): Promise<Story[]> {
const response = await fetch(
`${await this.getApiUrl()}/search/stories?page_size=25&query=owner:${owner}`,
);
async fetch<T>({ path }: { path: string }): Promise<T> {
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<StoryResponse> {
const encodedQuery = encodeURIComponent(query);
return await this.fetch<StoryResponse>({
path: `/search/stories?page_size=25&query=${encodedQuery}`,
});
}

async fetchOwnedStories({ owner }: { owner?: string }): Promise<Story[]> {
const query = `owner:${owner}`;
return (await this.fetchStories({ query })).data;
}

async getUsers(): Promise<User[]> {
Expand Down
6 changes: 6 additions & 0 deletions plugins/frontend/backstage-plugin-shortcut/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export type Story = {
app_url: string;
};

export type StoryResponse = {
next?: string;
total: number;
data: Array<Story>;
};

export type User = {
id: string;
profile: Profile;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Story>[] = (
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 <ErrorPanel error={error} />;
}
return (
<Table
title={props.title ? props.title : 'Shortcut Stories'}
options={{
paging: true,
search: false,
sorting: true,
draggable: false,
padding: 'dense',
}}
isLoading={loading}
data={data || []}
columns={columnsBuilder(users)}
actions={[
{
icon: () => <SyncIcon />,
tooltip: 'Refresh',
isFreeAction: true,
onClick: () => retry(),
},
]}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
16 changes: 16 additions & 0 deletions plugins/frontend/backstage-plugin-shortcut/src/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
8 changes: 7 additions & 1 deletion plugins/frontend/backstage-plugin-shortcut/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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]);
13 changes: 13 additions & 0 deletions plugins/frontend/backstage-plugin-shortcut/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
),
},
}),
);

0 comments on commit c487e2b

Please sign in to comment.