Skip to content

Commit 64e18c0

Browse files
first commit
Signed-off-by: Abhay-soni-developer <[email protected]>
1 parent a0d43ce commit 64e18c0

33 files changed

+13542
-1
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
11
# GitHub Homepage Cards for Backstage Enterprise
22

3-
A plugin to showcase Recent Pull Requests and Actions pipeline relevant to the user at the homepage in Backstage Enterprise.
3+
This plugin is contributed by Statusneo.
4+
<img src="https://raw.githubusercontent.com/StatusNeo/backstage-plugin-github/main/src/assets/statusneo.png"/>
5+
To read more about this plugin please read this [blog](https://statusneo.com/23468-2).
6+
7+
# Setup
8+
Before you can start using the GitHub cards, you must set up a GitHub provider.
9+
You can follow this page which will guide you through the process of setting up the GitHub provider for your backstage instance.
10+
[Setup GitHub Provider](https://backstage.io/docs/auth/github/provider/)
11+
12+
Now in the below steps, I will assume you are already done with the provider.
13+
## Integration Steps
14+
1. First Install the Github cards plugin by running this command from the root of the package.
15+
```shell
16+
yarn add –cwd packages/app @statusneo/backstage-github-plugin
17+
```
18+
19+
2. Import GithubPullRequestsCard, and GithubActionsCard from the installed package
20+
3. You can then use these components at the backstage frontend wherever you need.
21+
22+
```javascript
23+
import { GithubPullRequestsCard, GithubActionsCard } from '@statusneo/backstage-plugin-github';
24+
25+
// ...
26+
<Grid item xs={12} md={6}>
27+
<GithubActionsCard />
28+
</Grid>
29+
<Grid item xs={12} md={6}>
30+
<GithubPullRequestsCard />
31+
</Grid>
32+
// ...
33+
```
34+
35+
Now you are ready to use this backstage GitHub plugin to make your software management and development cycle a little more hassle-free.

package.json

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"name": "@statusneo/backstage-plugin-github",
3+
"version": "0.1.3",
4+
"main": "src/index.ts",
5+
"types": "src/index.ts",
6+
"license": "ISC",
7+
"publishConfig": {
8+
"access": "public",
9+
"main": "dist/index.esm.js",
10+
"types": "dist/index.d.ts"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": ""
15+
},
16+
"backstage": {
17+
"role": "frontend-plugin"
18+
},
19+
"author": {
20+
"email": "[email protected]",
21+
"url": "https://statusneo.com/",
22+
"name": "statusneo"
23+
},
24+
"scripts": {
25+
"start": "backstage-cli package start",
26+
"build": "backstage-cli package build",
27+
"lint": "backstage-cli package lint",
28+
"test": "backstage-cli package test",
29+
"clean": "backstage-cli package clean",
30+
"prepack": "backstage-cli package prepack",
31+
"postpack": "backstage-cli package postpack"
32+
},
33+
"dependencies": {
34+
"@backstage/core-components": "^0.12.3",
35+
"@backstage/core-plugin-api": "^1.3.0",
36+
"@backstage/errors": "^1.1.4",
37+
"@backstage/theme": "^0.2.16",
38+
"@material-table/core": "v3.1.0",
39+
"@material-ui/core": "^4.9.13",
40+
"@material-ui/icons": "^4.9.1",
41+
"@material-ui/lab": "^4.0.0-alpha.57",
42+
"@tanstack/react-query": "^4.22.0",
43+
"react-use": "^17.2.4"
44+
},
45+
"peerDependencies": {
46+
"react": "^16.13.1 || ^17.0.0"
47+
},
48+
"devDependencies": {
49+
"@backstage/cli": "^0.22.1",
50+
"@backstage/core-app-api": "^1.4.0",
51+
"@backstage/dev-utils": "^1.0.11",
52+
"@backstage/test-utils": "^1.2.4",
53+
"@testing-library/jest-dom": "^5.10.1",
54+
"@testing-library/react": "^12.1.3",
55+
"@testing-library/user-event": "^14.0.0",
56+
"@types/node": "*",
57+
"cross-fetch": "^3.1.5",
58+
"msw": "^0.49.0"
59+
},
60+
"files": [
61+
"dist"
62+
],
63+
"contributors": [{
64+
"email": "[email protected]",
65+
"url": "https://github.com/Abhay-soni-developer",
66+
"name": "abhaysoni"
67+
}],
68+
"maintainers": [{
69+
"email": "[email protected]",
70+
"url": "https://github.com/Abhay-soni-developer",
71+
"name": "abhaysoni"
72+
}],
73+
"keywords": ["backstage", "statusneo", "github"]
74+
}

src/api/GithubApi.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createApiRef } from '@backstage/core-plugin-api';
2+
import { SearchApiResponse, UserProfile, UserRepositoryApiRequest, UserRepositoryApiResponse, WorkflowRunApiRequest, WorkflowRunApiResponse, WorkflowRunJobApiRequest, WorkflowRunJobApiResponse } from '../utils/types';
3+
4+
export interface GithubApi {
5+
getUserPullRequest: (params: any) => Promise<SearchApiResponse>;
6+
getAuthenticatedUser: ()=>Promise<UserProfile>
7+
getAuthenticatedUserToken: ()=>Promise<string>
8+
getWorkflowRuns: (params: WorkflowRunApiRequest)=>Promise<WorkflowRunApiResponse>
9+
getWorkflowJobsByWorkflowId: (params: WorkflowRunJobApiRequest)=>Promise<WorkflowRunJobApiResponse>
10+
getAuthenticatedUserReposByType: (params: UserRepositoryApiRequest) => Promise<UserRepositoryApiResponse>
11+
}
12+
13+
export const githubApiRef = createApiRef<GithubApi>({
14+
id: 'plugin.github.service',
15+
});

src/api/GithubClient.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { GithubApi } from './GithubApi';
2+
import { OAuthApi, FetchApi } from '@backstage/core-plugin-api';
3+
import { ResponseError } from '@backstage/errors';
4+
import {
5+
SearchApiFetchRequest,
6+
SearchApiResponse,
7+
UserProfile,
8+
UserRepositoryApiRequest,
9+
UserRepositoryApiResponse,
10+
WorkflowRunApiRequest,
11+
WorkflowRunApiResponse,
12+
WorkflowRunJobApiRequest,
13+
WorkflowRunJobApiResponse,
14+
} from '../utils/types';
15+
16+
/** @public */
17+
export class GithubClient implements GithubApi {
18+
private readonly authApi: OAuthApi;
19+
private readonly fetchApi: FetchApi;
20+
21+
constructor(options: { authApi: OAuthApi; fetchApi: FetchApi }) {
22+
this.authApi = options.authApi;
23+
this.fetchApi = options.fetchApi;
24+
}
25+
26+
private async get<T>(
27+
path: string,
28+
params: { [key in string]: any } = {},
29+
): Promise<T> {
30+
const query = new URLSearchParams(params);
31+
const url = new URL(
32+
`${path}?${query.toString().replaceAll('%2B', '+')}`,
33+
'https://api.github.com/',
34+
);
35+
36+
const token = await this.authApi.getAccessToken();
37+
38+
const response = await this.fetchApi.fetch(url.toString(), {
39+
headers: {
40+
Authorization: `Bearer ${token}`,
41+
},
42+
});
43+
44+
if (!response.ok) {
45+
throw await ResponseError.fromResponse(response);
46+
}
47+
48+
return response.json() as Promise<T>;
49+
}
50+
51+
public getUserPullRequest(
52+
params: SearchApiFetchRequest,
53+
): Promise<SearchApiResponse> {
54+
return this.get<SearchApiResponse>(`search/issues`, params);
55+
}
56+
57+
public getAuthenticatedUser(): Promise<UserProfile> {
58+
return this.get<UserProfile>('user', {});
59+
}
60+
61+
public getAuthenticatedUserToken(): Promise<string> {
62+
return this.authApi.getAccessToken();
63+
}
64+
65+
public getWorkflowRuns(
66+
params: WorkflowRunApiRequest,
67+
): Promise<WorkflowRunApiResponse> {
68+
const { repoPath, ...queryParams } = params;
69+
return this.get<WorkflowRunApiResponse>(
70+
`repos/${repoPath}/actions/runs`,
71+
queryParams,
72+
);
73+
}
74+
75+
public getWorkflowJobsByWorkflowId(
76+
params: WorkflowRunJobApiRequest,
77+
): Promise<WorkflowRunJobApiResponse> {
78+
const { repoPath, run_id } = params;
79+
return this.get<WorkflowRunJobApiResponse>(
80+
`repos/${repoPath}/actions/runs/${run_id}/jobs`,
81+
{},
82+
);
83+
}
84+
85+
public getAuthenticatedUserReposByType (params: UserRepositoryApiRequest): Promise<UserRepositoryApiResponse> {
86+
return this.get<UserRepositoryApiResponse>(
87+
`/user/repos`,
88+
params,
89+
);
90+
}
91+
92+
}

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./GithubApi";
2+
export * from "./GithubClient";

src/assets/github.svg

Lines changed: 1 addition & 0 deletions
Loading

src/assets/statusneo.png

4.55 KB
Loading
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import {
2+
Table,
3+
TableColumn
4+
} from '@backstage/core-components';
5+
import { BackstageTheme } from '@backstage/theme';
6+
import { MTablePagination, MTableHeader } from '@material-table/core';
7+
import { withStyles } from '@material-ui/core/styles';
8+
import React from 'react';
9+
import { UserProfile } from '../../utils/types';
10+
import { SignInContent } from '../SignInContent';
11+
12+
export type CustomTableProps<T extends object> = {
13+
title: string | JSX.Element;
14+
subtitle: string;
15+
columns: TableColumn<T>[];
16+
data: T[];
17+
isLoading: boolean;
18+
signIn: (instantPopup: boolean) => void;
19+
isSignedIn: boolean;
20+
isInitialized: boolean;
21+
isError: boolean;
22+
error?: any;
23+
user: UserProfile | null; // github user profile
24+
detailPanel?: ({ rowData }: { rowData: T }) => React.ReactNode;
25+
columnsButton?: boolean;
26+
} & PaginationProps &
27+
SearchProps;
28+
29+
type PaginationProps =
30+
| {
31+
paging?: never;
32+
totalCount?: never;
33+
page?: never;
34+
setPage?: never;
35+
pageSize?: never;
36+
setPageSize?: never;
37+
}
38+
| {
39+
paging: boolean;
40+
totalCount: number;
41+
page: number;
42+
setPage: (newPage: number) => void;
43+
pageSize: number;
44+
setPageSize: (newPageSize: number) => void;
45+
};
46+
47+
type SearchProps =
48+
| {
49+
isSearchAvailable?: never;
50+
setSearchText?: never;
51+
searchPlaceholder?: never;
52+
searchTooltip?: never;
53+
}
54+
| {
55+
isSearchAvailable: boolean;
56+
setSearchText: (searchText: string) => void;
57+
searchPlaceholder?: string;
58+
searchTooltip?: string;
59+
};
60+
61+
CustomTable.defaultProps = {
62+
title: '',
63+
subtitle: '',
64+
columns: [],
65+
data: [],
66+
isLoading: false,
67+
paging: false,
68+
isSearchAvailable: false,
69+
totalCount: 0,
70+
searchPlaceholder: 'search',
71+
searchTooltip: 'search',
72+
pageSize: 0,
73+
page: 1,
74+
select: false,
75+
columnsButton: false
76+
};
77+
78+
const StyledMTableHeader = withStyles(
79+
theme => ({
80+
header: {
81+
padding: theme.spacing(1, 2, 1, 2.5),
82+
borderTop: `1px solid ${theme.palette.grey.A100}`,
83+
borderBottom: `1px solid ${theme.palette.grey.A100}`,
84+
// withStyles hasn't a generic overload for theme
85+
color: (theme as BackstageTheme).palette.textSubtle,
86+
fontWeight: 'bold',
87+
position: 'static',
88+
wordBreak: 'normal',
89+
},
90+
}),
91+
{ name: 'BackstageTableHeader' },
92+
)(MTableHeader);
93+
94+
export function CustomTable<T extends object = {}>(props: CustomTableProps<T>) {
95+
return (
96+
<Table<T>
97+
title={props.title}
98+
subtitle={props.subtitle}
99+
columns={props.columns}
100+
isLoading={props.isLoading}
101+
options={{
102+
search: props.isSearchAvailable,
103+
paging: props.paging,
104+
padding: 'dense',
105+
toolbar: true,
106+
filtering: false,
107+
showFirstLastPageButtons: true,
108+
paginationType: 'stepped',
109+
loadingType: 'linear',
110+
emptyRowsWhenPaging: true,
111+
tableLayout: 'auto',
112+
debounceInterval: 350,
113+
sorting: false,
114+
columnsButton: props.columnsButton,
115+
}}
116+
localization={{
117+
toolbar: {
118+
searchPlaceholder: props.searchPlaceholder,
119+
searchTooltip: props.searchTooltip,
120+
},
121+
}}
122+
data={props.data}
123+
onSearchChange={(searchText: string) => {
124+
if (props.isSearchAvailable) {
125+
props.setSearchText(searchText);
126+
}
127+
}}
128+
emptyContent={
129+
<>
130+
{!props.isSignedIn && (
131+
<SignInContent handleAuthClick={() => props.signIn(false)} />
132+
)}
133+
</>
134+
}
135+
components={{
136+
Pagination: paginationProps => {
137+
const { classes, ...remainingProps } = paginationProps;
138+
return (
139+
<MTablePagination
140+
{...remainingProps}
141+
page={props.page}
142+
count={props.totalCount}
143+
// @ts-ignore
144+
onPageChange={(e: any, newPage: number) => {
145+
if (props.paging) {
146+
props.setPage(newPage);
147+
}
148+
}}
149+
/>
150+
);
151+
},
152+
Header: headerProps => <StyledMTableHeader {...headerProps} />,
153+
}}
154+
detailPanel={props.detailPanel}
155+
/>
156+
);
157+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./CustomTable"

0 commit comments

Comments
 (0)