Skip to content

Commit 57dc768

Browse files
authored
Merge pull request #16 from stackabletech/refactor/js-bundling
refactor: Add JS and CSS bundling
2 parents caf2b07 + b85ff44 commit 57dc768

38 files changed

+1432
-411
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
node_modules
12
site

js/doc.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import React, { useMemo, useState, useEffect, useCallback } from 'react'
2+
import EventEmitter3 from 'eventemitter3'
3+
import ClipboardJS from 'clipboard'
4+
import { render } from 'react-dom'
5+
import DOMPurify from 'dompurify'
6+
import { html } from 'htm/react'
7+
import halfmoon from 'halfmoon'
8+
import slugify from 'slugify'
9+
import marked from 'marked'
10+
11+
// Syntax highlighting imports
12+
import { getHighlighterCore } from 'shikiji/core'
13+
import dracula from 'shikiji/themes/dracula.mjs'
14+
import { getWasmInlined } from 'shikiji/wasm'
15+
import yaml from 'shikiji/langs/yaml.mjs'
16+
17+
const supportedLangs = ['yaml'];
18+
const bus = new EventEmitter3();
19+
window.bus = bus;
20+
21+
const clipboard = new ClipboardJS('.copy-url');
22+
clipboard.on('success', e => {
23+
halfmoon.initStickyAlert({
24+
content: "Copied Link!",
25+
timeShown: 2000
26+
})
27+
});
28+
29+
const highlighter = await getHighlighterCore({
30+
loadWasm: getWasmInlined,
31+
themes: [dracula],
32+
langs: [yaml],
33+
})
34+
35+
marked.use({
36+
renderer: {
37+
code: (code, lang, escaped) => {
38+
if (lang === undefined || !supportedLangs.includes(lang)) {
39+
return `<pre><code>${code}</code></pre>`;
40+
} else {
41+
const tokenizedCode = highlighter.codeToHtml(code);
42+
return `<pre class="language-${lang}"><code class="language-${lang}">${tokenizedCode}</code></pre>`;
43+
}
44+
}
45+
}
46+
})
47+
48+
const { Kind, Group, Version, Schema } = JSON.parse(document.getElementById('pageData').textContent);
49+
50+
const properties = Schema.Properties;
51+
if (properties?.apiVersion) delete properties.apiVersion;
52+
if (properties?.kind) delete properties.kind;
53+
if (properties?.metadata?.Type == "object") delete properties.metadata;
54+
55+
function getDescription(schema) {
56+
let desc = schema.Description || '';
57+
if (desc.trim() == '') {
58+
desc = '_No Description Provided._'
59+
}
60+
return DOMPurify.sanitize(marked(desc));
61+
}
62+
63+
function CRD() {
64+
const expandAll = useCallback(() => bus.emit('expand-all'), []);
65+
const collapseAll = useCallback(() => bus.emit('collapse-all'), []);
66+
67+
// this used to go under the codeblock, but our descriptions are a bit useless at the moment
68+
// <p class="font-size-18">${React.createElement('div', { dangerouslySetInnerHTML: { __html: getDescription(Schema) } })}</p>
69+
70+
const gvkCode = `apiVersion: ${Group}/${Version}\nkind: ${Kind}`;
71+
const gvkTokens = highlighter.codeToHtml(gvkCode, { lang: 'yaml', theme: 'dracula' });
72+
73+
return html`
74+
<div class="parts d-md-flex justify-content-between mt-md-20 mb-md-20">
75+
<${PartLabel} type="Kind" value=${Kind} />
76+
<${PartLabel} type="Group" value=${Group} />
77+
<${PartLabel} type="Version" value=${Version} />
78+
</div>
79+
80+
<hr class="mb-md-20" />
81+
${React.createElement("div", { dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(gvkTokens) } })}
82+
83+
<div class="${properties == null ? 'd-none' : 'd-flex'} flex-row-reverse mb-10 mt-10">
84+
<button class="btn ml-10" type="button" onClick=${expandAll}>+ expand all</button>
85+
<button class="btn" type="button" onClick=${collapseAll}>- collapse all</button>
86+
</div>
87+
<div class="collapse-group">
88+
${properties != null
89+
? Object.keys(properties).map(prop => SchemaPart({ key: prop, property: properties[prop] }))
90+
: html`
91+
<p class="font-size-18">
92+
This CRD has an empty or unspecified schema.
93+
</p>
94+
`
95+
}
96+
</div>
97+
`;
98+
}
99+
100+
function SchemaPart({ key, property, parent, parentSlug }) {
101+
const [props, propKeys, required, type, schema] = useMemo(() => {
102+
let schema = property;
103+
let props = property.Properties || {};
104+
105+
let type = property.Type;
106+
if (type === 'array') {
107+
const itemsSchema = property.Items.Schema;
108+
if (itemsSchema.Type !== 'object') {
109+
type = `[]${itemsSchema.Type}`;
110+
} else {
111+
schema = itemsSchema;
112+
props = itemsSchema.Properties || {};
113+
type = `[]object`;
114+
}
115+
}
116+
let propKeys = Object.keys(props);
117+
118+
let required = false;
119+
if (parent && parent.Required && parent.Required.includes(key)) {
120+
required = true;
121+
}
122+
return [props, propKeys, required, type, schema]
123+
}, [parent, property]);
124+
125+
const slug = useMemo(() => slugify((parentSlug ? `${parentSlug}-` : '') + key), [parentSlug, key]);
126+
const fullLink = useMemo(() => {
127+
const url = new URL(location.href);
128+
url.hash = `#${slug}`;
129+
return url.toJSON();
130+
});
131+
const isHyperlinked = useCallback(() => location.hash.substring(1).startsWith(slug), [slug]);
132+
133+
const [isOpen, setIsOpen] = useState((key == "spec" && !parent) || isHyperlinked());
134+
135+
useEffect(() => {
136+
const handleHashChange = () => {
137+
if (!isOpen && isHyperlinked()) {
138+
setIsOpen(true);
139+
}
140+
};
141+
window.addEventListener('hashchange', handleHashChange);
142+
return () => window.removeEventListener('hashchange', handleHashChange);
143+
}, [isOpen]);
144+
145+
useEffect(() => {
146+
const collapse = () => setIsOpen(false);
147+
const expand = () => setIsOpen(true);
148+
bus.on('collapse-all', collapse);
149+
bus.on('expand-all', expand);
150+
return () => {
151+
bus.off('collapse-all', collapse);
152+
bus.off('expand-all', expand);
153+
};
154+
}, []);
155+
156+
return html`
157+
<details class="collapse-panel" open="${isOpen}" onToggle=${e => { setIsOpen(e.target.open); e.stopPropagation(); }}>
158+
<summary class="collapse-header position-relative">
159+
${key} <kbd class="text-muted">${type}</kbd> ${required ? html`<span class="badge badge-primary">required</span>` : ''}
160+
<button class="btn btn-sm position-absolute right-0 top-0 m-5 copy-url z-10" type="button" data-clipboard-text="${fullLink}">🔗</button>
161+
</summary>
162+
<div id="${slug}" class="collapse-content">
163+
${React.createElement("div", { className: 'property-description', dangerouslySetInnerHTML: { __html: getDescription(property) } })}
164+
${propKeys.length > 0 ? html`<br />` : ''}
165+
<div class="collapse-group">
166+
${propKeys
167+
.map(propKey => SchemaPart({
168+
parent: schema, parentKey: key, key: propKey, property: props[propKey], parentSlug: slug
169+
}))}
170+
</div>
171+
</div>
172+
</details>`;
173+
}
174+
175+
function PartLabel({ type, value }) {
176+
return html`
177+
<div class="mt-10">
178+
<span class="font-weight-semibold font-size-24">${value}</span>
179+
<br />
180+
<span class="badge text-muted font-size-12">${type}</span>
181+
</div>`;
182+
}
183+
184+
render(html`<${CRD} />`, document.querySelector('#renderTarget'));

js/home.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { render } from 'react-dom'
2+
import { html } from 'htm/react'
3+
import { useTable, useSortBy } from 'react-table'
4+
5+
const { Tag, Rows } = JSON.parse(document.getElementById('pageData').textContent);
6+
const data = Rows;
7+
8+
function renderLink(row, linkText) {
9+
const href = `/${Tag}/${row.Group}/${row.Kind}/${row.Version}`;
10+
11+
return html`<a href=${href}>${linkText}</a>`;
12+
}
13+
14+
const columns = [
15+
{
16+
Header: 'Kind',
17+
accessor: 'Kind',
18+
Cell: ({ row: { original }, value }) => renderLink(original, value)
19+
},
20+
{
21+
Header: 'Group',
22+
accessor: 'Group'
23+
},
24+
{
25+
Header: 'Version',
26+
accessor: 'Version'
27+
},
28+
{
29+
Header: 'Operator',
30+
accessor: 'Repo'
31+
}
32+
];
33+
34+
function CRDHeader(column) {
35+
let bla = (column.isSorted
36+
? column.isSortedDesc
37+
? html`<i className="fas fa-sort-down"></i>`
38+
: html`<i className="fas fa-sort-up"></i>`
39+
: html`<i className="fas fa-sort"></i>`)
40+
return html`<th ...${column.getHeaderProps(column.getSortByToggleProps())}>
41+
${column.render('Header')}
42+
<span className="sort-header ${column.isSorted ? 'sort-header-active' : ''}">
43+
${bla}
44+
</span>
45+
</th>`
46+
}
47+
48+
function CRDTable() {
49+
const table = useTable({ columns, data }, useSortBy);
50+
const {
51+
getTableProps,
52+
getTableBodyProps,
53+
headerGroups,
54+
rows,
55+
prepareRow,
56+
} = table;
57+
58+
return html`
59+
<div className="table-responsive">
60+
<table className="table table-striped table-outer-bordered" ...${getTableProps()}>
61+
<thead>
62+
${headerGroups.map(group => html`
63+
<tr ...${group.getHeaderGroupProps()}>
64+
${group.headers.map(CRDHeader)}
65+
</tr>
66+
`)}
67+
</thead>
68+
<tbody ...${getTableBodyProps()}>
69+
${rows.map(row => {
70+
prepareRow(row)
71+
return html`
72+
<tr ...${row.getRowProps()}>
73+
${row.cells.map(cell => html`
74+
<td ...${cell.getCellProps()}>${cell.render('Cell')}</td>
75+
`)}
76+
</tr>
77+
`
78+
})}
79+
</tbody>
80+
</table>
81+
</div>`;
82+
}
83+
84+
render(html`<${CRDTable} />`, document.getElementById("crds"));

js/nav.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import halfmoon from 'halfmoon'
2+
3+
document.addEventListener('DOMContentLoaded', () => {
4+
halfmoon.onDOMContentLoaded()
5+
})

js/org.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { render } from 'react-dom'
2+
import { html } from 'htm/react'
3+
import { useTable, useSortBy } from 'react-table'
4+
5+
const { CRDs, Tag, } = JSON.parse(document.getElementById('pageData').textContent);
6+
const data = Object.keys(CRDs).map(key => CRDs[key]);
7+
8+
function renderLink(row, linkText) {
9+
const href = `/${Tag}/${row.Group}/${row.Kind}/${row.Version}`;
10+
11+
return html`<a href=${href}>${linkText}</a>`;
12+
}
13+
14+
const columns = [
15+
{
16+
Header: 'Kind',
17+
accessor: 'Kind',
18+
Cell: ({ row: { original }, value }) => renderLink(original, value)
19+
},
20+
{
21+
Header: 'Group',
22+
accessor: 'Group'
23+
},
24+
{
25+
Header: 'Version',
26+
accessor: 'Version'
27+
}
28+
];
29+
30+
function CRDHeader(column) {
31+
let bla = (column.isSorted
32+
? column.isSortedDesc
33+
? html`<i class="fas fa-sort-down"></i>`
34+
: html`<i class="fas fa-sort-up"></i>`
35+
: html`<i class="fas fa-sort"></i>`)
36+
return html`<th ...${column.getHeaderProps(column.getSortByToggleProps())}>
37+
${column.render('Header')}
38+
<span class="sort-header ${column.isSorted ? 'sort-header-active' : ''}">
39+
${bla}
40+
</span>
41+
</th>`
42+
}
43+
44+
function CRDTable() {
45+
const table = useTable({ columns, data }, useSortBy );
46+
const {
47+
getTableProps,
48+
getTableBodyProps,
49+
headerGroups,
50+
rows,
51+
prepareRow,
52+
} = table;
53+
54+
return html`
55+
<div class="table-responsive">
56+
<table class="table table-striped table-outer-bordered" ...${getTableProps()}>
57+
<thead>
58+
${headerGroups.map(group => html`
59+
<tr ...${group.getHeaderGroupProps()}>
60+
${group.headers.map(CRDHeader)}
61+
</tr>
62+
`)}
63+
</thead>
64+
<tbody ...${getTableBodyProps()}>
65+
${rows.map(row => {
66+
prepareRow(row)
67+
return html`
68+
<tr ...${row.getRowProps()}>
69+
${row.cells.map(cell => html`
70+
<td ...${cell.getCellProps()}>${cell.render('Cell')}</td>
71+
`)}
72+
</tr>
73+
`
74+
})}
75+
</tbody>
76+
</table>
77+
</div>`;
78+
}
79+
80+
render(html`<${CRDTable} />`, document.getElementById("crds"));

package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"scripts": {
3+
"build:doc": "esbuild js/doc.js --bundle --minify --sourcemap --format=esm --outfile=static/js/doc.js",
4+
"build:home": "esbuild js/home.js --bundle --minify --sourcemap --outfile=static/js/home.js",
5+
"build:org": "esbuild js/org.js --bundle --minify --sourcemap --outfile=static/js/org.js",
6+
"build:nav": "esbuild js/nav.js --bundle --minify --sourcemap --outfile=static/js/nav.js",
7+
"build": "pnpm build:nav && pnpm build:home && pnpm build:org && pnpm build:doc"
8+
},
9+
"devDependencies": {
10+
"esbuild": "^0.19.10",
11+
"esbuild-plugin-prismjs": "^1.0.8",
12+
"shikiji": "^0.9.9"
13+
},
14+
"dependencies": {
15+
"@fortawesome/fontawesome-free": "5.15.1",
16+
"clipboard": "2.0.6",
17+
"date-fns": "^3.0.0",
18+
"dompurify": "2.2.2",
19+
"eventemitter3": "4.0.7",
20+
"halfmoon": "^1.1.1",
21+
"htm": "3",
22+
"marked": "1.2.4",
23+
"prismjs": "^1.29.0",
24+
"react": "16",
25+
"react-dom": "16",
26+
"react-table": "7",
27+
"slugify": "1.4.6"
28+
}
29+
}

0 commit comments

Comments
 (0)