Skip to content
This repository was archived by the owner on Mar 10, 2024. It is now read-only.

Commit 86b9731

Browse files
committed
Add components and tests
1 parent 7c6c08e commit 86b9731

15 files changed

Lines changed: 361 additions & 2 deletions

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
dist-types/
3+
node_modules/
4+
yarn-error.log
5+
yarn.lock

README.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,43 @@
1-
# backstage-plugin-techdocs-addon-sourcegraph
2-
Backstage TechDocs Sourcegraph Notebooks Addon
1+
# Backstage TechDocs Sourcegraph addon
2+
3+
The `backstage-plugin-techdocs-addon-sourcegraph` extends [Sourcegraph](https://about.sourcegraph.com/) capabilities into Backstage TechDocs. This plugin is a [Backstage TechDocs Addon](https://backstage.io/docs/features/techdocs/addons), which requires Backstage v1.2+.
4+
5+
## Getting started
6+
7+
The plugin provides a component for [embedding Sourcegraph Notebooks](https://docs.sourcegraph.com/notebooks/notebook-embedding). Follow [the official documentation for TechDocs Addons](https://backstage.io/docs/features/techdocs/addons#installing-and-using-addons) to use this addon.
8+
9+
```typescript jsx
10+
import { SourcegraphNotebook } from 'backstage-plugin-techdocs-addon-sourcegraph';
11+
12+
// Sourcegraph instance domain (Required)
13+
const domain = 'sourcegraph.example.com';
14+
15+
// pre-render callback (Optional)
16+
const callback = ({
17+
18+
// div wrapper for embedded Notebook
19+
container: HTMLDivElement,
20+
21+
// embedded notebook iframe
22+
iframe: HTMLIFrameElement,
23+
24+
// notebook id
25+
id: string,
26+
27+
// non-embedded notebook url
28+
url: string
29+
}) => {
30+
31+
// ...some extra handling or DOM manipulation before attaching the
32+
// iframe to the container
33+
container.append(iframe);
34+
});
35+
36+
<TechDocsAddons>
37+
<SourcegraphNotebook domain={domain} callback={callback} />
38+
</TechDocsAddons>
39+
```
40+
41+
## Further resources
42+
43+
- [Sourcegraph Notebooks](https://docs.sourcegraph.com/notebooks)

dev/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createDevApp } from '@backstage/dev-utils';
2+
import { techdocsAddonSourcegraphPlugin } from '../src/plugin';
3+
4+
createDevApp()
5+
.registerPlugin(techdocsAddonSourcegraphPlugin)
6+
.render();

package.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"name": "backstage-plugin-techdocs-addon-sourcegraph",
3+
"version": "0.0.1",
4+
"main": "src/index.ts",
5+
"types": "src/index.ts",
6+
"license": "Apache-2.0",
7+
"author": {
8+
"name": "James Andrew Vaughn",
9+
"email": "jamesvaughn@modethirteen.com",
10+
"url": "https://modethirteen.com"
11+
},
12+
"publishConfig": {
13+
"access": "public",
14+
"main": "dist/index.esm.js",
15+
"types": "dist/index.d.ts"
16+
},
17+
"backstage": {
18+
"role": "frontend-plugin"
19+
},
20+
"keywords": [
21+
"backstage",
22+
"techdocs",
23+
"sourcegraph",
24+
"docs-as-code"
25+
],
26+
"scripts": {
27+
"start": "backstage-cli package start",
28+
"build": "backstage-cli package build",
29+
"lint": "backstage-cli package lint",
30+
"test": "backstage-cli package test",
31+
"clean": "backstage-cli package clean",
32+
"prepack": "backstage-cli package prepack",
33+
"postpack": "backstage-cli package postpack"
34+
},
35+
"dependencies": {
36+
"@backstage/core-components": "^0.10.0",
37+
"@backstage/core-plugin-api": "^1.0.4",
38+
"@backstage/plugin-techdocs-react": "^1.0.2",
39+
"@backstage/theme": "^0.2.16",
40+
"@material-ui/core": "^4.12.2",
41+
"@material-ui/icons": "^4.9.1",
42+
"@material-ui/lab": "^4.0.0-alpha.61",
43+
"react-use": "^17.2.4"
44+
},
45+
"peerDependencies": {
46+
"react": "^16.13.1 || ^17.0.0"
47+
},
48+
"devDependencies": {
49+
"@backstage/cli": "^0.17.2",
50+
"@backstage/core-app-api": "^1.0.3",
51+
"@backstage/dev-utils": "^1.0.3",
52+
"@backstage/plugin-techdocs-addons-test-utils": "^1.0.2",
53+
"@backstage/test-utils": "^1.1.1",
54+
"@testing-library/jest-dom": "^5.10.1",
55+
"@testing-library/react": "^12.1.3",
56+
"@testing-library/user-event": "^14.0.0",
57+
"@types/jest": "*",
58+
"@types/node": "*",
59+
"@types/react": "^17.0.0",
60+
"cross-fetch": "^3.1.5",
61+
"msw": "^0.42.0",
62+
"react": "^17.0.2",
63+
"react-dom": "^17.0.0",
64+
"typescript": "^4.0.0"
65+
},
66+
"files": [
67+
"dist"
68+
]
69+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React from 'react';
2+
import { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils';
3+
import { SourcegraphNotebook } from '../plugin';
4+
import { SourcegraphNotebookCallbackParameters } from './SourcegraphNotebook';
5+
6+
const dom = (
7+
<body>
8+
<table className='language-text highlighttable'>
9+
<tbody>
10+
<tr>
11+
<td className='code'>
12+
<div className='language-text highlight'>
13+
<pre>
14+
<code>https://example.com/notebooks/foo</code>
15+
</pre>
16+
</div>
17+
</td>
18+
</tr>
19+
</tbody>
20+
</table>
21+
<p>plugh</p>
22+
<table className='language-text highlighttable'>
23+
<tbody>
24+
<tr>
25+
<td className='code'>
26+
<div className='language-text highlight'>
27+
<pre>
28+
<code>https://example.com/notebooks/bar</code>
29+
</pre>
30+
</div>
31+
</td>
32+
</tr>
33+
</tbody>
34+
</table>
35+
<p>xyzzy</p>
36+
<table className='highlighttable'>
37+
<tbody>
38+
<tr>
39+
<td className='code'>
40+
<div className='highlight'>
41+
<pre>
42+
<code>https://example.com/notebooks/baz</code>
43+
</pre>
44+
</div>
45+
</td>
46+
</tr>
47+
</tbody>
48+
</table>
49+
<p>fred</p>
50+
<table className='language-text highlighttable'>
51+
<tbody>
52+
<tr>
53+
<td className='code'>
54+
<div className='highlight'>
55+
<pre>
56+
<code>https://example.io/notebooks/gradle</code>
57+
</pre>
58+
</div>
59+
</td>
60+
</tr>
61+
</tbody>
62+
</table>
63+
</body>
64+
);
65+
66+
describe('SourcegraphNotebook', () => {
67+
it('can render', async () => {
68+
const { shadowRoot } = await TechDocsAddonTester.buildAddonsInTechDocs([
69+
<SourcegraphNotebook domain='example.com' />
70+
])
71+
.withDom(dom)
72+
.renderWithEffects();
73+
const foo = shadowRoot?.querySelector('[data-notebook-id="foo"]') as Element;
74+
expect(foo).not.toBeNull();
75+
expect(foo.querySelector('iframe')?.outerHTML)
76+
.toBe('<iframe src="https://example.com/embed/notebooks/foo" frameborder="0" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>');
77+
const bar = shadowRoot?.querySelector('[data-notebook-id="bar"]') as Element;
78+
expect(bar).not.toBeNull();
79+
expect(bar.querySelector('iframe')?.outerHTML)
80+
.toBe('<iframe src="https://example.com/embed/notebooks/bar" frameborder="0" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>');
81+
expect(shadowRoot?.querySelector('[data-notebook-id="baz"]')).toBeNull();
82+
expect(shadowRoot?.querySelector('[data-notebook-id="gradle"]')).toBeNull();
83+
});
84+
it('can render with callback', async () => {
85+
const args: Record<string, {
86+
container: HTMLDivElement,
87+
iframe: HTMLIFrameElement,
88+
id: string,
89+
url: string
90+
}> = {};
91+
const callback = ({ container, iframe, id, url }: SourcegraphNotebookCallbackParameters) => {
92+
args[id] = { container, iframe, id, url };
93+
};
94+
await TechDocsAddonTester.buildAddonsInTechDocs([
95+
<SourcegraphNotebook domain='example.com' callback={callback} />
96+
])
97+
.withDom(dom)
98+
.renderWithEffects();
99+
expect(args.foo.container).not.toBeNull();
100+
expect(args.foo.iframe).not.toBeNull();
101+
expect(args.foo.id).toBe('foo');
102+
expect(args.foo.url).toBe('https://example.com/notebooks/foo');
103+
expect(args.bar.container).not.toBeNull();
104+
expect(args.bar.iframe).not.toBeNull();
105+
expect(args.bar.id).toBe('bar');
106+
expect(args.bar.url).toBe('https://example.com/notebooks/bar');
107+
});
108+
describe('callback', () => {
109+
it('does not auto-append iframe', async () => {
110+
const { shadowRoot } = await TechDocsAddonTester.buildAddonsInTechDocs([
111+
<SourcegraphNotebook domain='example.com' callback={() => {}} />
112+
])
113+
.withDom(dom)
114+
.renderWithEffects();
115+
const iframes = (shadowRoot?.querySelectorAll('iframe') ?? []) as NodeListOf<HTMLIFrameElement>;
116+
expect(iframes.length).toBe(0);
117+
});
118+
it('can append iframe', async () => {
119+
const callback = ({ container, iframe }: SourcegraphNotebookCallbackParameters) => {
120+
container.append(iframe);
121+
};
122+
const { shadowRoot } = await TechDocsAddonTester.buildAddonsInTechDocs([
123+
<SourcegraphNotebook domain='example.com' callback={callback} />
124+
])
125+
.withDom(dom)
126+
.renderWithEffects();
127+
const iframes = (shadowRoot?.querySelectorAll('iframe') ?? []) as NodeListOf<HTMLIFrameElement>;
128+
expect(iframes.length).toBe(2);
129+
});
130+
});
131+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useShadowRootElements } from '@backstage/plugin-techdocs-react';
2+
import { useEffect } from 'react';
3+
4+
export type SourcegraphNotebookCallbackParameters = {
5+
container: HTMLDivElement,
6+
iframe: HTMLIFrameElement,
7+
id: string,
8+
url: string
9+
};
10+
11+
export type SourcegraphNotebookProps = {
12+
domain: string;
13+
callback?: ({
14+
container,
15+
iframe,
16+
id,
17+
url
18+
}: SourcegraphNotebookCallbackParameters) => void;
19+
};
20+
21+
export const SourcegraphNotebookAddon = (props: SourcegraphNotebookProps) => {
22+
const highlightTables = useShadowRootElements<HTMLDivElement>(['.highlighttable']);
23+
useEffect(() => {
24+
for (const highlightTable of highlightTables) {
25+
if (highlightTable.style.display === 'none') {
26+
continue;
27+
}
28+
if (!highlightTable.classList.contains('language-text')) {
29+
continue;
30+
}
31+
const code = highlightTable.querySelector('code');
32+
if (!code) {
33+
continue;
34+
}
35+
const text = code.textContent?.trim();
36+
if (!text) {
37+
continue;
38+
}
39+
const matches = text.match(new RegExp(`^https:\/\/${props.domain}\/notebooks\/(.+?)$`, 'i'));
40+
if (!matches) {
41+
continue;
42+
}
43+
highlightTable.style.display = 'none';
44+
const [_, id] = matches;
45+
const iframe = document.createElement('iframe');
46+
iframe.setAttribute('src', `https://${props.domain}/embed/notebooks/${id}`);
47+
iframe.setAttribute('frameborder', '0');
48+
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
49+
const container = document.createElement('div');
50+
container.dataset.notebookId = id;
51+
if (props.callback) {
52+
props.callback({ container, iframe, id, url: `https://${props.domain}/notebooks/${id}` });
53+
} else {
54+
container.append(iframe);
55+
}
56+
highlightTable.parentNode?.insertBefore(container, highlightTable.nextSibling);
57+
}
58+
}, [highlightTables, props]);
59+
return null;
60+
};

src/SourcegraphNotebook/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { SourcegraphNotebookAddon } from './SourcegraphNotebook';
2+
export type { SourcegraphNotebookProps } from './SourcegraphNotebook';

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { techdocsAddonSourcegraphPlugin, SourcegraphNotebook } from './plugin';

0 commit comments

Comments
 (0)