feat: github issues plugin mvp
Signed-off-by: Kamil Wolny <mrwolny@gmail.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { GitHubIssues } from './GitHubIssues';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import {
|
||||
setupRequestMockHandlers,
|
||||
renderInTestApp,
|
||||
} from '@backstage/test-utils';
|
||||
|
||||
// test for repo duplicates
|
||||
|
||||
describe('GitHubIssues', () => {
|
||||
const server = setupServer();
|
||||
// Enable sane handlers for network requests
|
||||
setupRequestMockHandlers(server);
|
||||
|
||||
// setup mock response
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<GitHubIssues mode="card" />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
expect(rendered.getByText('Welcome to github-issues!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { Box, IconButton, Typography } from '@material-ui/core';
|
||||
import { InfoCard, Progress } from '@backstage/core-components';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { useEntityGitHubRepositories } from '../../hooks/useEntityGitHubRepositories';
|
||||
import {
|
||||
RepoIssues,
|
||||
useGetIssuesByRepoFromGitHub,
|
||||
} from '../../hooks/useGetIssuesByRepoFromGitHub';
|
||||
|
||||
import { IssueList } from './IssuesList';
|
||||
import { NoRepositoriesInfo } from './NoRepositoriesInfo';
|
||||
|
||||
export type PluginMode = 'page' | 'card';
|
||||
|
||||
export type Props = {
|
||||
mode: PluginMode;
|
||||
itemsPerPage?: number;
|
||||
itemsPerRepo?: number;
|
||||
};
|
||||
|
||||
export const GitHubIssues = ({
|
||||
itemsPerPage = 10,
|
||||
itemsPerRepo = 40,
|
||||
}: Props) => {
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [activeFilter, setActiveFilter] = React.useState<Array<string>>([]);
|
||||
|
||||
const [issuesByRepository, setIssuesByRepository] =
|
||||
React.useState<Record<string, RepoIssues>>();
|
||||
|
||||
const { repositories } = useEntityGitHubRepositories();
|
||||
const getIssues = useGetIssuesByRepoFromGitHub();
|
||||
|
||||
const filters = React.useMemo(
|
||||
() =>
|
||||
issuesByRepository
|
||||
? Object.keys(issuesByRepository)
|
||||
.filter(repo => issuesByRepository[repo].issues.totalCount > 0)
|
||||
.map(repo => ({
|
||||
label: `${repo} (${issuesByRepository[repo].issues.totalCount})`,
|
||||
value: repo,
|
||||
}))
|
||||
: [],
|
||||
[issuesByRepository],
|
||||
);
|
||||
|
||||
const totalIssuesInGitHub = React.useMemo(
|
||||
() =>
|
||||
issuesByRepository
|
||||
? Object.values(issuesByRepository).reduce(
|
||||
(acc, { issues: { totalCount } }) => acc + totalCount,
|
||||
0,
|
||||
)
|
||||
: 0,
|
||||
[issuesByRepository],
|
||||
);
|
||||
|
||||
const filteredRepos = React.useMemo(
|
||||
() =>
|
||||
issuesByRepository && activeFilter.length
|
||||
? activeFilter.reduce(
|
||||
(acc, val) => ({
|
||||
[val]: issuesByRepository[val],
|
||||
...acc,
|
||||
}),
|
||||
{},
|
||||
)
|
||||
: issuesByRepository,
|
||||
[issuesByRepository, activeFilter],
|
||||
);
|
||||
|
||||
const issues = React.useMemo(
|
||||
() =>
|
||||
filteredRepos
|
||||
? Object.values(filteredRepos)
|
||||
.map(({ issues: { edges } }) => edges)
|
||||
.flat()
|
||||
.sort((a, b) => {
|
||||
if (a.node.updatedAt > b.node.updatedAt) {
|
||||
return -1;
|
||||
} else if (b.node.updatedAt > a.node.updatedAt) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
: [],
|
||||
[filteredRepos],
|
||||
);
|
||||
|
||||
const fetchGitHubIssues = React.useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const issuesByRepo = await getIssues(repositories, itemsPerRepo);
|
||||
|
||||
setIssuesByRepository(issuesByRepo);
|
||||
setIsLoading(false);
|
||||
}, [itemsPerRepo, getIssues, repositories]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (repositories.length) {
|
||||
fetchGitHubIssues();
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [repositories.length, fetchGitHubIssues]);
|
||||
|
||||
if (!repositories.length) {
|
||||
return <NoRepositoriesInfo />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoCard
|
||||
title={
|
||||
<Box display="flex" justifyContent="flex-start" alignItems="center">
|
||||
<Typography variant="h5">Open GitHub Issues</Typography>
|
||||
<IconButton color="secondary" onClick={fetchGitHubIssues}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{isLoading && <Progress />}
|
||||
|
||||
<IssueList
|
||||
itemsPerPage={itemsPerPage}
|
||||
issues={issues}
|
||||
totalIssuesInGitHub={totalIssuesInGitHub}
|
||||
setActiveFilter={setActiveFilter}
|
||||
filters={filters}
|
||||
/>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 React, { FunctionComponent } from 'react';
|
||||
import { Typography, Box, Avatar, makeStyles } from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
small: {
|
||||
width: theme.spacing(4),
|
||||
height: theme.spacing(4),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
noAssignees: {
|
||||
height: theme.spacing(4),
|
||||
},
|
||||
}));
|
||||
|
||||
export const Assignees: FunctionComponent<Props> = (props: Props) => {
|
||||
const { name, avatar } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
// todo: many assignees -> NUM assignees + stock images on each other
|
||||
return name ? (
|
||||
<Box display="flex" alignItems="center" marginX={1}>
|
||||
<Typography color="primary" variant="body2" component="p">
|
||||
{name}
|
||||
</Typography>
|
||||
<Avatar alt={name} src={avatar} className={classes.small} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box display="flex" alignItems="center" marginX={1}>
|
||||
<Typography
|
||||
color="primary"
|
||||
variant="body2"
|
||||
component="p"
|
||||
className={classes.noAssignees}
|
||||
>
|
||||
No assignees
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Assignees;
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 React, { FunctionComponent } from 'react';
|
||||
import { ChatIcon } from '@backstage/core-components';
|
||||
import { Box, Badge } from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
commentsCount: number;
|
||||
};
|
||||
|
||||
export const CommentsCount: FunctionComponent<Props> = (props: Props) => {
|
||||
const { commentsCount } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
marginBottom={1}
|
||||
style={{ marginRight: '12px' }}
|
||||
display="flex"
|
||||
justifyContent="flex-start"
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
<Badge badgeContent={commentsCount} color="primary">
|
||||
<ChatIcon />
|
||||
</Badge>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 React, { FunctionComponent } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Box, Paper, Typography, CardActionArea } from '@material-ui/core';
|
||||
import Assignees from './Assignees';
|
||||
import { CommentsCount } from './CommentsCount';
|
||||
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
url: string;
|
||||
authorName: string;
|
||||
assigneeName?: string;
|
||||
assigneeAvatar?: string;
|
||||
authorAvatar?: string;
|
||||
repositoryName: string;
|
||||
commentsCount: number;
|
||||
even: boolean;
|
||||
};
|
||||
|
||||
const getElapsedTime = (isoDate: string) =>
|
||||
DateTime.fromISO(isoDate).toRelative();
|
||||
|
||||
export const IssueCard: FunctionComponent<Props> = (props: Props) => {
|
||||
const {
|
||||
title,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
url,
|
||||
assigneeName,
|
||||
assigneeAvatar,
|
||||
authorName,
|
||||
repositoryName,
|
||||
commentsCount,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Box marginBottom={1}>
|
||||
<Paper variant="outlined">
|
||||
<CardActionArea href={url} target="_blank">
|
||||
<Box padding={1}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography color="primary" variant="body2" component="h2">
|
||||
{repositoryName}
|
||||
</Typography>
|
||||
<Assignees name={assigneeName} avatar={assigneeAvatar} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">
|
||||
<b>{title}</b>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider variant="middle" />
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Box marginY={1}>
|
||||
<Typography variant="body2" component="p">
|
||||
Created at: <strong>{getElapsedTime(createdAt)}</strong> by{' '}
|
||||
<strong>{authorName}</strong>
|
||||
</Typography>
|
||||
{updatedAt && (
|
||||
<Typography variant="body2" component="p">
|
||||
Last update at: <strong>{getElapsedTime(updatedAt)}</strong>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{commentsCount > 0 && (
|
||||
<CommentsCount commentsCount={commentsCount} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardActionArea>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { IssueCard } from './IssueCard';
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { Select } from '@backstage/core-components';
|
||||
import { SelectedItems } from '@backstage/core-components';
|
||||
import { makeStyles, Box } from '@material-ui/core';
|
||||
|
||||
export type FilterItem = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
items: Array<FilterItem>;
|
||||
totalIssuesInGitHub: number;
|
||||
onChange: (active: Array<string>) => void;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
filters: {
|
||||
margin: theme.spacing(0, 0, 2, 0),
|
||||
'& > div': {
|
||||
width: '600px',
|
||||
'& > div': {
|
||||
maxWidth: '600px',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const checkSelectedItems: (
|
||||
onChange: (active: Array<string>) => void,
|
||||
) => (active: SelectedItems) => void = onChange => active => {
|
||||
return onChange(active as Array<string>);
|
||||
};
|
||||
|
||||
export const Filters = ({ items, totalIssuesInGitHub, onChange }: Props) => {
|
||||
const css = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={css.filters}>
|
||||
<Select
|
||||
placeholder={`All repositories (${totalIssuesInGitHub})`}
|
||||
label=""
|
||||
items={items}
|
||||
multiple
|
||||
onChange={checkSelectedItems(onChange)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 './Filters';
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
import { Pagination } from '@material-ui/lab';
|
||||
|
||||
import { IssueCard } from '../IssueCard';
|
||||
import { Issue } from '../../../hooks/useGetIssuesByRepoFromGitHub';
|
||||
import { Filters, FilterItem } from './Filters';
|
||||
|
||||
export type PluginMode = 'page' | 'card';
|
||||
|
||||
export type Props = {
|
||||
itemsPerPage?: number;
|
||||
issues: Array<{
|
||||
node: Issue;
|
||||
}>;
|
||||
filters: Array<FilterItem>;
|
||||
totalIssuesInGitHub: number;
|
||||
setActiveFilter: (active: Array<string>) => void;
|
||||
};
|
||||
|
||||
export const IssueList = ({
|
||||
itemsPerPage = 10,
|
||||
issues,
|
||||
filters,
|
||||
setActiveFilter,
|
||||
totalIssuesInGitHub,
|
||||
}: Props) => {
|
||||
const [currentPage, setCurrentPage] = React.useState(1);
|
||||
|
||||
const displayIssues = issues.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
(currentPage - 1) * itemsPerPage + itemsPerPage,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Filters
|
||||
items={filters}
|
||||
onChange={setActiveFilter}
|
||||
totalIssuesInGitHub={totalIssuesInGitHub}
|
||||
/>
|
||||
|
||||
{displayIssues.length > 0 ? (
|
||||
displayIssues.map(
|
||||
(
|
||||
{
|
||||
node: {
|
||||
title,
|
||||
comments,
|
||||
author,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
repository,
|
||||
assignees,
|
||||
url,
|
||||
},
|
||||
},
|
||||
index,
|
||||
) => (
|
||||
<IssueCard
|
||||
even={Boolean(index % 2)}
|
||||
title={title}
|
||||
createdAt={createdAt}
|
||||
assigneeAvatar={assignees.edges[0]?.node.avatarUrl}
|
||||
assigneeName={assignees.edges[0]?.node.login}
|
||||
authorName={author.login}
|
||||
updatedAt={updatedAt}
|
||||
repositoryName={repository.nameWithOwner}
|
||||
url={url}
|
||||
commentsCount={comments.totalCount}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<h1>No issues 🚀</h1>
|
||||
)}
|
||||
{issues.length / itemsPerPage > 1 ? (
|
||||
<Pagination
|
||||
count={Math.ceil(issues.length / itemsPerPage)}
|
||||
onChange={(_, page) => setCurrentPage(page)}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 './IssuesList';
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { EmptyState } from '@backstage/core-components';
|
||||
|
||||
export const NoRepositoriesInfo = () => {
|
||||
return (
|
||||
<EmptyState
|
||||
title="There are no GitHub repositories connected to this entity."
|
||||
missing="data"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 './NoRepositoriesInfo';
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 './GitHubIssues';
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 React, { FunctionComponent } from 'react';
|
||||
|
||||
import { GitHubIssues, Props as GitHubIssuesProps } from '../GitHubIssues';
|
||||
|
||||
type Props = Omit<GitHubIssuesProps, 'mode'>;
|
||||
|
||||
export const GitHubIssuesCard: FunctionComponent<Props> = props => {
|
||||
return <GitHubIssues mode="card" {...props} />;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 './GitHubIssuesCard';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 React, { FunctionComponent } from 'react';
|
||||
import { GitHubIssues, Props as GitHubIssuesProps } from '../GitHubIssues';
|
||||
|
||||
type Props = Omit<GitHubIssuesProps, 'mode'>;
|
||||
|
||||
export const GitHubIssuesPage: FunctionComponent<Props> = props => {
|
||||
return <GitHubIssues mode="page" {...props} />;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 './GitHubIssuesPage';
|
||||
@@ -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 { Entity } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
catalogApiRef,
|
||||
humanizeEntityRef,
|
||||
useEntity,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug';
|
||||
|
||||
export const getProjectNameFromEntity = (entity: Entity): string => {
|
||||
return entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] ?? '';
|
||||
};
|
||||
|
||||
export function useEntityGitHubRepositories() {
|
||||
const { entity } = useEntity();
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [repositories, setRepositories] = useState<string[]>([]);
|
||||
|
||||
const getRepositoriesNames = useCallback(async () => {
|
||||
if (entity.kind === 'Component' || entity.kind === 'API') {
|
||||
const entityName = getProjectNameFromEntity(entity);
|
||||
|
||||
if (entityName) {
|
||||
setRepositories([entityName]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const entitiesList = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: ['Component', 'API'],
|
||||
'spec.owner': humanizeEntityRef(entity, { defaultKind: 'group' }),
|
||||
},
|
||||
});
|
||||
|
||||
const entitiesNames: string[] = entitiesList.items.map(componentEntity =>
|
||||
getProjectNameFromEntity(componentEntity),
|
||||
);
|
||||
|
||||
setRepositories([...new Set(entitiesNames)].filter(name => name.length));
|
||||
}, [catalogApi, entity]);
|
||||
|
||||
useEffect(() => {
|
||||
getRepositoriesNames();
|
||||
}, [getRepositoriesNames]);
|
||||
|
||||
return {
|
||||
repositories,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
const mockGraphQLQuery = jest.fn(() => 'ti ri ri ri ra!');
|
||||
jest.mock('./useOctokitGraphQL', () => ({
|
||||
useOctokitGraphQL: jest.fn(() => mockGraphQLQuery),
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { useGetIssuesByRepoFromGitHub } from './useGetIssuesByRepoFromGitHub';
|
||||
|
||||
// fragment issues on Repository {
|
||||
// issues(
|
||||
// states: OPEN
|
||||
// first: 40
|
||||
// orderBy: { field: UPDATED_AT, direction: DESC }
|
||||
// ) {
|
||||
// totalCount
|
||||
// edges {
|
||||
// cursor
|
||||
// node {
|
||||
// assignees(first: 10) {
|
||||
// edges {
|
||||
// node {
|
||||
// avatarUrl
|
||||
// login
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// author {
|
||||
// login
|
||||
// avatarUrl
|
||||
// url
|
||||
// }
|
||||
// repository {
|
||||
// nameWithOwner
|
||||
// }
|
||||
// body
|
||||
// title
|
||||
// url
|
||||
// participants {
|
||||
// totalCount
|
||||
// }
|
||||
// updatedAt
|
||||
// createdAt
|
||||
// comments(last: 1) {
|
||||
// totalCount
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// query {
|
||||
// rateLimit {
|
||||
// cost
|
||||
// remaining
|
||||
// }
|
||||
|
||||
// yomovie: repository(name: "yo-movie", owner: "mrwolny") {
|
||||
// ...issues
|
||||
// }
|
||||
// ,
|
||||
// yoanchor: repository(name: "yo-anchor", owner: "mrwolny") {
|
||||
// ...issues
|
||||
// }
|
||||
// ,
|
||||
// yomoviex: repository(name: "yo-movie", owner: "mrwolny") {
|
||||
// ...issues
|
||||
// }
|
||||
// ,
|
||||
// yoanchorx: repository(name: "yo-anchor", owner: "mrwolny") {
|
||||
// ...issues
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
describe('useGetIssuesBeRepoFromGitHub', () => {
|
||||
it('should rock!', async () => {
|
||||
const Helper = () => {
|
||||
const getIssues = useGetIssuesByRepoFromGitHub();
|
||||
|
||||
getIssues(['mrwolny/yo-yo', 'mrwolny/yoyo'], 10);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
render(<Helper />);
|
||||
|
||||
expect(mockGraphQLQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { useOctokitGraphQL } from './useOctokitGraphQL';
|
||||
|
||||
type Assignee = {
|
||||
avatarUrl: string;
|
||||
login: string;
|
||||
};
|
||||
|
||||
export type EdgesWithNodes<T> = {
|
||||
edges: Array<{
|
||||
node: T;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Node<T> = {
|
||||
node: T;
|
||||
};
|
||||
|
||||
type IssueAuthor = {
|
||||
login: string;
|
||||
};
|
||||
|
||||
export type Issue = {
|
||||
cursor: string;
|
||||
assignees: EdgesWithNodes<Assignee>;
|
||||
author: IssueAuthor;
|
||||
repository: {
|
||||
nameWithOwner: string;
|
||||
};
|
||||
body: string;
|
||||
title: string;
|
||||
url: string;
|
||||
participants: {
|
||||
totalCount: number;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
comments: {
|
||||
totalCount: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type RepoIssues = {
|
||||
issues: {
|
||||
totalCount: number;
|
||||
} & EdgesWithNodes<Issue>;
|
||||
};
|
||||
|
||||
export type RepoIssuesQueryResults = Record<string, RepoIssues>;
|
||||
|
||||
const createQuery = (
|
||||
repositories: Array<{
|
||||
safeName: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
}>,
|
||||
itemsPerRepo: number,
|
||||
): string => {
|
||||
const fragment = `
|
||||
fragment issues on Repository {
|
||||
issues(
|
||||
states: OPEN
|
||||
first: ${itemsPerRepo}
|
||||
orderBy: { field: UPDATED_AT, direction: DESC }
|
||||
) {
|
||||
totalCount
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
assignees(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
avatarUrl
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
repository {
|
||||
nameWithOwner
|
||||
}
|
||||
body
|
||||
title
|
||||
url
|
||||
participants {
|
||||
totalCount
|
||||
}
|
||||
updatedAt
|
||||
createdAt
|
||||
comments(last: 1) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const query = `
|
||||
${fragment}
|
||||
|
||||
query {
|
||||
${repositories.map(
|
||||
({ safeName, name, owner }) => `
|
||||
${safeName}: repository(name: "${name}", owner: "${owner}") {
|
||||
...issues
|
||||
}
|
||||
`,
|
||||
)}
|
||||
}
|
||||
`;
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const useGetIssuesByRepoFromGitHub = () => {
|
||||
const graphql = useOctokitGraphQL<RepoIssuesQueryResults>();
|
||||
|
||||
const fn = React.useRef(
|
||||
async (
|
||||
repos: Array<string>,
|
||||
itemsPerRepo: number,
|
||||
): Promise<Record<string, RepoIssues>> => {
|
||||
const safeNames: Array<string> = [];
|
||||
|
||||
const repositories = repos.map(repo => {
|
||||
const [owner, name] = repo.split('/');
|
||||
|
||||
const safeNameRegex = /-|\./gi;
|
||||
let safeName = name.replace(safeNameRegex, '');
|
||||
|
||||
while (safeNames.includes(safeName)) {
|
||||
safeName += 'x';
|
||||
}
|
||||
|
||||
safeNames.push(safeName);
|
||||
|
||||
return {
|
||||
safeName,
|
||||
name,
|
||||
owner,
|
||||
};
|
||||
});
|
||||
|
||||
const issuesByRepo: RepoIssuesQueryResults = await graphql(
|
||||
createQuery(repositories, itemsPerRepo),
|
||||
);
|
||||
|
||||
return repositories.reduce((acc, { safeName, name, owner }) => {
|
||||
acc[`${owner}/${name}`] = issuesByRepo[safeName];
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, RepoIssues>);
|
||||
},
|
||||
);
|
||||
|
||||
return fn.current;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { Issue } from './useGetIssuesByRepoFromGitHub';
|
||||
|
||||
export type GitHubIssues = {
|
||||
getIssues: (repo?: string) => Array<Issue>;
|
||||
getIssuesTotalCountByRepo: () => Record<string, { totalCount: number }>;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { Octokit } from '@octokit/rest';
|
||||
import {
|
||||
useApi,
|
||||
githubAuthApiRef,
|
||||
configApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { readGitHubIntegrationConfigs } from '@backstage/integration';
|
||||
|
||||
let octokit: Octokit;
|
||||
|
||||
export const useOctokitGraphQL = <T>() => {
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
const config = useApi(configApiRef);
|
||||
|
||||
const baseUrl = readGitHubIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.github') ?? [],
|
||||
)[0].apiBaseUrl;
|
||||
|
||||
return (path: string, options?: any): Promise<T> =>
|
||||
auth
|
||||
.getAccessToken(['repo'])
|
||||
.then((token: string) => {
|
||||
if (!octokit) {
|
||||
octokit = new Octokit({ auth: token, ...(baseUrl && { baseUrl }) });
|
||||
}
|
||||
|
||||
return octokit;
|
||||
})
|
||||
.then(octokitInstance => {
|
||||
return octokitInstance.graphql(path, options);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 {
|
||||
gitHubIssuesPlugin,
|
||||
GitHubIssuesPage,
|
||||
GitHubIssuesCard,
|
||||
} from './plugin';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { gitHubIssuesPlugin } from './plugin';
|
||||
|
||||
describe('github-issues', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(gitHubIssuesPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 {
|
||||
createPlugin,
|
||||
createComponentExtension,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
export const gitHubIssuesPlugin = createPlugin({
|
||||
id: 'github-issues',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
export const GitHubIssuesCard = gitHubIssuesPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'GitHubIssuesCard',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/GitHubIssuesCard').then(m => m.GitHubIssuesCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const GitHubIssuesPage = gitHubIssuesPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'GitHubIssuesPage',
|
||||
component: () =>
|
||||
import('./components/GitHubIssuesPage').then(m => m.GitHubIssuesPage),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'github-issues',
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
Reference in New Issue
Block a user