feat: add explore-backend plugin

Signed-off-by: Andrew Thauer <athauer@wealthsimple.com>
This commit is contained in:
Andrew Thauer
2022-11-28 13:43:28 -05:00
parent 41cceb9a3a
commit 4dec6f16be
42 changed files with 1411 additions and 31 deletions
+4
View File
@@ -35,8 +35,12 @@
"@backstage/catalog-model": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-explore-common": "workspace:^",
"@backstage/plugin-explore-react": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@backstage/plugin-search-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
+66
View File
@@ -0,0 +1,66 @@
/*
* 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 { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import {
GetExploreToolsRequest,
GetExploreToolsResponse,
} from '@backstage/plugin-explore-common';
import { ExploreApi } from './types';
/**
* Default implementation of the ExploreApi.
*
* @public
*/
export class ExploreClient implements ExploreApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
constructor({
discoveryApi,
fetchApi,
}: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
}) {
this.discoveryApi = discoveryApi;
this.fetchApi = fetchApi;
}
async getTools(
request: GetExploreToolsRequest = {},
): Promise<GetExploreToolsResponse> {
const { fetch } = this.fetchApi;
const filter = request.filter ?? {};
const baseUrl = await this.discoveryApi.getBaseUrl('explore');
const tags = filter?.tags?.map(t => `tag=${encodeURIComponent(t)}`) ?? [];
const lifecycles =
filter?.lifecycle?.map(l => `lifecycle=${encodeURIComponent(l)}`) ?? [];
const query = [...tags, ...lifecycles].join('&');
const response = await fetch(`${baseUrl}/tools?${query}`);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<GetExploreToolsResponse>;
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 { ExploreClient } from './client';
export { exploreApiRef } from './types';
export type { ExploreApi } from './types';
+39
View File
@@ -0,0 +1,39 @@
/*
* 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 { createApiRef } from '@backstage/core-plugin-api';
import {
GetExploreToolsRequest,
GetExploreToolsResponse,
} from '@backstage/plugin-explore-common';
/**
* Interface for the explore plugin.
*
* @public
*/
export interface ExploreApi {
/**
* Returns a list of explore tools.
*
* @param request - The The request query options
*/
getTools(request?: GetExploreToolsRequest): Promise<GetExploreToolsResponse>;
}
export const exploreApiRef = createApiRef<ExploreApi>({
id: 'plugin.explore.service',
});
@@ -0,0 +1,113 @@
/*
* 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, { ReactNode } from 'react';
import {
Box,
Chip,
Divider,
ListItem,
ListItemIcon,
ListItemText,
makeStyles,
} from '@material-ui/core';
import { Link } from '@backstage/core-components';
import { useAnalytics } from '@backstage/core-plugin-api';
import {
IndexableDocument,
ResultHighlight,
} from '@backstage/plugin-search-common';
import { HighlightedSearchResultText } from '@backstage/plugin-search-react';
const useStyles = makeStyles({
flexContainer: {
flexWrap: 'wrap',
},
itemText: {
width: '100%',
wordBreak: 'break-all',
marginBottom: '1rem',
},
});
/**
* Props for {@link ToolSearchResultListItem}.
*
* @public
*/
export interface ToolSearchResultListItemProps {
icon?: ReactNode;
result: IndexableDocument;
highlight?: ResultHighlight;
rank?: number;
}
/** @public */
export function ToolSearchResultListItem(props: ToolSearchResultListItemProps) {
const result = props.result as any;
const classes = useStyles();
const analytics = useAnalytics();
const handleClick = () => {
analytics.captureEvent('discover', result.title, {
attributes: { to: result.location },
value: props.rank,
});
};
return (
<Link noTrack to={result.location} onClick={handleClick}>
<ListItem alignItems="flex-start">
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
<div className={classes.flexContainer}>
<ListItemText
className={classes.itemText}
primaryTypographyProps={{ variant: 'h6' }}
primary={
props.highlight?.fields.title ? (
<HighlightedSearchResultText
text={props.highlight.fields.title}
preTag={props.highlight.preTag}
postTag={props.highlight.postTag}
/>
) : (
result.title
)
}
secondary={
props.highlight?.fields.text ? (
<HighlightedSearchResultText
text={props.highlight.fields.text}
preTag={props.highlight.preTag}
postTag={props.highlight.postTag}
/>
) : (
result.text
)
}
/>
<Box>
{result.tags &&
result.tags.map((tag: string) => (
<Chip label={tag} size="small" />
))}
</Box>
</div>
</ListItem>
<Divider component="li" />
</Link>
);
}
@@ -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 './ToolSearchResultListItem';
+1
View File
@@ -16,3 +16,4 @@
export * from './DomainCard';
export * from './ExploreLayout';
export * from './ToolSearchResultListItem';
+1
View File
@@ -20,6 +20,7 @@
* @packageDocumentation
*/
export * from './api';
export * from './components';
export * from './extensions';
export { explorePlugin, explorePlugin as plugin } from './plugin';
+29 -7
View File
@@ -16,21 +16,43 @@
import { exploreToolsConfigRef } from '@backstage/plugin-explore-react';
import { catalogEntityRouteRef, exploreRouteRef } from './routes';
import { exampleTools } from './util/examples';
import { createApiFactory, createPlugin } from '@backstage/core-plugin-api';
import {
createApiFactory,
createPlugin,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { ExploreClient, exploreApiRef } from './api';
/** @public */
export const explorePlugin = createPlugin({
id: 'explore',
apis: [
// Register a default for exploreToolsConfigRef, you may want to override
// the API locally in your app.
createApiFactory({
api: exploreApiRef,
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) =>
new ExploreClient({ discoveryApi, fetchApi }),
}),
/**
* @deprecated Use ExploreApi from `@backstage/plugin-explore` instead
*
* Register a default for exploreToolsConfigRef, you may want to override
* the API locally in your app.
*/
createApiFactory({
api: exploreToolsConfigRef,
deps: {},
factory: () => ({
deps: {
exploreApi: exploreApiRef,
},
factory: ({ exploreApi }) => ({
async getTools() {
return exampleTools;
// TODO: Can we make this backwards compatible so it works off the static frontend example tools?
// return exampleTools;
return (await exploreApi.getTools()).tools;
},
}),
}),