Merge pull request #9742 from brexhq/sugupta/contrib-confluence-search
Contributing Confluence search functionality
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Search
|
||||
|
||||
Contributions/extensions to the Search plugin
|
||||
@@ -0,0 +1,95 @@
|
||||
ConfluenceCollator.ts reference
|
||||
|
||||
```ts
|
||||
import { DocumentCollator } from '@backstage/search-common';
|
||||
import fetch from 'cross-fetch';
|
||||
|
||||
export class ConfluenceCollator implements DocumentCollator {
|
||||
public readonly type: string = 'confluence';
|
||||
|
||||
async execute() {
|
||||
const ConfluenceUrlBase =
|
||||
'https://{CONFLUENCE-ORG-NAME}.atlassian.net/wiki/rest/api';
|
||||
|
||||
async function getConfluenceData(requestUrl: string) {
|
||||
var emptyJson = {};
|
||||
try {
|
||||
const res = await fetch(requestUrl, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
Authorization: `Basic ${process.env.CONFLUENCE_TOKEN}`,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
return await res.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return emptyJson;
|
||||
}
|
||||
|
||||
async function getSpaces(): Promise<string[]> {
|
||||
const data = await getConfluenceData(
|
||||
`${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`,
|
||||
);
|
||||
let spacesList = [];
|
||||
if (data['results']) {
|
||||
const results = data['results'];
|
||||
for (const result of results) {
|
||||
spacesList.push(result['key']);
|
||||
}
|
||||
}
|
||||
return spacesList;
|
||||
}
|
||||
|
||||
async function getDocumentsFromSpaces(spaces: string[]): Promise<string[]> {
|
||||
let documentsList = [];
|
||||
for (var space of spaces) {
|
||||
let next = true;
|
||||
let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}`;
|
||||
while (next) {
|
||||
const data = await getConfluenceData(requestUrl);
|
||||
if (data['results']) {
|
||||
const results = data['results'];
|
||||
for (const result of results) {
|
||||
documentsList.push(result['_links']['self']);
|
||||
}
|
||||
if (data['_links']['next']) {
|
||||
requestUrl = data['_links']['base'] + data['_links']['next'];
|
||||
} else {
|
||||
next = false;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return documentsList;
|
||||
}
|
||||
|
||||
async function getDocumentInfo(documents: string[]) {
|
||||
let documentInfo = [];
|
||||
for (var documentUrl of documents) {
|
||||
const data = await getConfluenceData(
|
||||
documentUrl + '?expand=body.storage',
|
||||
);
|
||||
if (data['status'] && data['status'] == 'current') {
|
||||
const documentMetaData = {
|
||||
title: data['title'],
|
||||
text: data['body']['storage']['value'],
|
||||
location: data['_links']['base'] + data['_links']['webui'],
|
||||
};
|
||||
documentInfo.push(documentMetaData);
|
||||
}
|
||||
}
|
||||
return documentInfo;
|
||||
}
|
||||
|
||||
const spacesList = await getSpaces();
|
||||
const documentsList = await getDocumentsFromSpaces(spacesList);
|
||||
const documentMetaDataList = await getDocumentInfo(documentsList);
|
||||
return documentMetaDataList;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
ConfluenceResultListItem.tsx reference
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { IndexableDocument } from '@backstage/search-common';
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
|
||||
type Props = {
|
||||
result: IndexableDocument;
|
||||
};
|
||||
|
||||
export const ConfluenceResultListItem = ({ result }: Props) => {
|
||||
// Remove html tags from document text before displaying
|
||||
const chars = [];
|
||||
let isTag = false;
|
||||
for (const c of result.text.substring(0, 500)) {
|
||||
if (c === '<') {
|
||||
isTag = true;
|
||||
continue;
|
||||
}
|
||||
if (c === '>') {
|
||||
isTag = false;
|
||||
chars.push(' ');
|
||||
continue;
|
||||
}
|
||||
if (!isTag) {
|
||||
chars.push(c);
|
||||
}
|
||||
}
|
||||
const excerpt =
|
||||
chars.join('').substring(0, 80) + (result.text.length > 80 ? '...' : '');
|
||||
|
||||
return (
|
||||
<Link to={result.location}>
|
||||
<ListItem alignItems="center">
|
||||
<ListItemIcon>
|
||||
<img
|
||||
width="20"
|
||||
height="20"
|
||||
src="https://cdn.worldvectorlogo.com/logos/confluence-1.svg"
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={result.title}
|
||||
secondary={excerpt}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Confluence
|
||||
|
||||
These files help you add Confluence as a source to the Backstage Search plugin.
|
||||
To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app.
|
||||
Then, add the following code to your packages/app/src/components/search/SearchPage.tsx:
|
||||
|
||||
```tsx
|
||||
import { ConfluenceResultListItem } from './ConfluenceResultListItem';
|
||||
```
|
||||
|
||||
```tsx
|
||||
case 'confluence':
|
||||
return (
|
||||
<ConfluenceResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
and the following to packages/backend/src/plugins/search.ts:
|
||||
|
||||
```ts
|
||||
import { ConfluenceCollator } from './search/ConfluenceCollator';
|
||||
```
|
||||
|
||||
```ts
|
||||
indexBuilder.addCollator({
|
||||
defaultRefreshIntervalSeconds: 600,
|
||||
collator: new ConfluenceCollator(),
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user