Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
site
184 changes: 184 additions & 0 deletions js/doc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react'
import EventEmitter3 from 'eventemitter3'
import ClipboardJS from 'clipboard'
import { render } from 'react-dom'
import DOMPurify from 'dompurify'
import { html } from 'htm/react'
import halfmoon from 'halfmoon'
import slugify from 'slugify'
import marked from 'marked'

// Syntax highlighting imports
import { getHighlighterCore } from 'shikiji/core'
import dracula from 'shikiji/themes/dracula.mjs'
import { getWasmInlined } from 'shikiji/wasm'
import yaml from 'shikiji/langs/yaml.mjs'

const supportedLangs = ['yaml'];
const bus = new EventEmitter3();
window.bus = bus;

const clipboard = new ClipboardJS('.copy-url');
clipboard.on('success', e => {
halfmoon.initStickyAlert({
content: "Copied Link!",
timeShown: 2000
})
});

const highlighter = await getHighlighterCore({
loadWasm: getWasmInlined,
themes: [dracula],
langs: [yaml],
})

marked.use({
renderer: {
code: (code, lang, escaped) => {
if (lang === undefined || !supportedLangs.includes(lang)) {
return `<pre><code>${code}</code></pre>`;
} else {
const tokenizedCode = highlighter.codeToHtml(code);
return `<pre class="language-${lang}"><code class="language-${lang}">${tokenizedCode}</code></pre>`;
}
}
}
})

const { Kind, Group, Version, Schema } = JSON.parse(document.getElementById('pageData').textContent);

const properties = Schema.Properties;
if (properties?.apiVersion) delete properties.apiVersion;
if (properties?.kind) delete properties.kind;
if (properties?.metadata?.Type == "object") delete properties.metadata;

function getDescription(schema) {
let desc = schema.Description || '';
if (desc.trim() == '') {
desc = '_No Description Provided._'
}
return DOMPurify.sanitize(marked(desc));
}

function CRD() {
const expandAll = useCallback(() => bus.emit('expand-all'), []);
const collapseAll = useCallback(() => bus.emit('collapse-all'), []);

// this used to go under the codeblock, but our descriptions are a bit useless at the moment
// <p class="font-size-18">${React.createElement('div', { dangerouslySetInnerHTML: { __html: getDescription(Schema) } })}</p>

const gvkCode = `apiVersion: ${Group}/${Version}\nkind: ${Kind}`;
const gvkTokens = highlighter.codeToHtml(gvkCode, { lang: 'yaml', theme: 'dracula' });

return html`
<div class="parts d-md-flex justify-content-between mt-md-20 mb-md-20">
<${PartLabel} type="Kind" value=${Kind} />
<${PartLabel} type="Group" value=${Group} />
<${PartLabel} type="Version" value=${Version} />
</div>

<hr class="mb-md-20" />
${React.createElement("div", { dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(gvkTokens) } })}

<div class="${properties == null ? 'd-none' : 'd-flex'} flex-row-reverse mb-10 mt-10">
<button class="btn ml-10" type="button" onClick=${expandAll}>+ expand all</button>
<button class="btn" type="button" onClick=${collapseAll}>- collapse all</button>
</div>
<div class="collapse-group">
${properties != null
? Object.keys(properties).map(prop => SchemaPart({ key: prop, property: properties[prop] }))
: html`
<p class="font-size-18">
This CRD has an empty or unspecified schema.
</p>
`
}
</div>
`;
}

function SchemaPart({ key, property, parent, parentSlug }) {
const [props, propKeys, required, type, schema] = useMemo(() => {
let schema = property;
let props = property.Properties || {};

let type = property.Type;
if (type === 'array') {
const itemsSchema = property.Items.Schema;
if (itemsSchema.Type !== 'object') {
type = `[]${itemsSchema.Type}`;
} else {
schema = itemsSchema;
props = itemsSchema.Properties || {};
type = `[]object`;
}
}
let propKeys = Object.keys(props);

let required = false;
if (parent && parent.Required && parent.Required.includes(key)) {
required = true;
}
return [props, propKeys, required, type, schema]
}, [parent, property]);

const slug = useMemo(() => slugify((parentSlug ? `${parentSlug}-` : '') + key), [parentSlug, key]);
const fullLink = useMemo(() => {
const url = new URL(location.href);
url.hash = `#${slug}`;
return url.toJSON();
});
const isHyperlinked = useCallback(() => location.hash.substring(1).startsWith(slug), [slug]);

const [isOpen, setIsOpen] = useState((key == "spec" && !parent) || isHyperlinked());

useEffect(() => {
const handleHashChange = () => {
if (!isOpen && isHyperlinked()) {
setIsOpen(true);
}
};
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, [isOpen]);

useEffect(() => {
const collapse = () => setIsOpen(false);
const expand = () => setIsOpen(true);
bus.on('collapse-all', collapse);
bus.on('expand-all', expand);
return () => {
bus.off('collapse-all', collapse);
bus.off('expand-all', expand);
};
}, []);

return html`
<details class="collapse-panel" open="${isOpen}" onToggle=${e => { setIsOpen(e.target.open); e.stopPropagation(); }}>
<summary class="collapse-header position-relative">
${key} <kbd class="text-muted">${type}</kbd> ${required ? html`<span class="badge badge-primary">required</span>` : ''}
<button class="btn btn-sm position-absolute right-0 top-0 m-5 copy-url z-10" type="button" data-clipboard-text="${fullLink}">🔗</button>
</summary>
<div id="${slug}" class="collapse-content">
${React.createElement("div", { className: 'property-description', dangerouslySetInnerHTML: { __html: getDescription(property) } })}
${propKeys.length > 0 ? html`<br />` : ''}
<div class="collapse-group">
${propKeys
.map(propKey => SchemaPart({
parent: schema, parentKey: key, key: propKey, property: props[propKey], parentSlug: slug
}))}
</div>
</div>
</details>`;
}

function PartLabel({ type, value }) {
return html`
<div class="mt-10">
<span class="font-weight-semibold font-size-24">${value}</span>
<br />
<span class="badge text-muted font-size-12">${type}</span>
</div>`;
}

render(html`<${CRD} />`, document.querySelector('#renderTarget'));
84 changes: 84 additions & 0 deletions js/home.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { render } from 'react-dom'
import { html } from 'htm/react'
import { useTable, useSortBy } from 'react-table'

const { Tag, Rows } = JSON.parse(document.getElementById('pageData').textContent);
const data = Rows;

function renderLink(row, linkText) {
const href = `/${Tag}/${row.Group}/${row.Kind}/${row.Version}`;

return html`<a href=${href}>${linkText}</a>`;
}

const columns = [
{
Header: 'Kind',
accessor: 'Kind',
Cell: ({ row: { original }, value }) => renderLink(original, value)
},
{
Header: 'Group',
accessor: 'Group'
},
{
Header: 'Version',
accessor: 'Version'
},
{
Header: 'Operator',
accessor: 'Repo'
}
];

function CRDHeader(column) {
let bla = (column.isSorted
? column.isSortedDesc
? html`<i className="fas fa-sort-down"></i>`
: html`<i className="fas fa-sort-up"></i>`
: html`<i className="fas fa-sort"></i>`)
return html`<th ...${column.getHeaderProps(column.getSortByToggleProps())}>
${column.render('Header')}
<span className="sort-header ${column.isSorted ? 'sort-header-active' : ''}">
${bla}
</span>
</th>`
}

function CRDTable() {
const table = useTable({ columns, data }, useSortBy);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = table;

return html`
<div className="table-responsive">
<table className="table table-striped table-outer-bordered" ...${getTableProps()}>
<thead>
${headerGroups.map(group => html`
<tr ...${group.getHeaderGroupProps()}>
${group.headers.map(CRDHeader)}
</tr>
`)}
</thead>
<tbody ...${getTableBodyProps()}>
${rows.map(row => {
prepareRow(row)
return html`
<tr ...${row.getRowProps()}>
${row.cells.map(cell => html`
<td ...${cell.getCellProps()}>${cell.render('Cell')}</td>
`)}
</tr>
`
})}
</tbody>
</table>
</div>`;
}

render(html`<${CRDTable} />`, document.getElementById("crds"));
5 changes: 5 additions & 0 deletions js/nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import halfmoon from 'halfmoon'

document.addEventListener('DOMContentLoaded', () => {
halfmoon.onDOMContentLoaded()
})
80 changes: 80 additions & 0 deletions js/org.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { render } from 'react-dom'
import { html } from 'htm/react'
import { useTable, useSortBy } from 'react-table'

const { CRDs, Tag, } = JSON.parse(document.getElementById('pageData').textContent);
const data = Object.keys(CRDs).map(key => CRDs[key]);

function renderLink(row, linkText) {
const href = `/${Tag}/${row.Group}/${row.Kind}/${row.Version}`;

return html`<a href=${href}>${linkText}</a>`;
}

const columns = [
{
Header: 'Kind',
accessor: 'Kind',
Cell: ({ row: { original }, value }) => renderLink(original, value)
},
{
Header: 'Group',
accessor: 'Group'
},
{
Header: 'Version',
accessor: 'Version'
}
];

function CRDHeader(column) {
let bla = (column.isSorted
? column.isSortedDesc
? html`<i class="fas fa-sort-down"></i>`
: html`<i class="fas fa-sort-up"></i>`
: html`<i class="fas fa-sort"></i>`)
return html`<th ...${column.getHeaderProps(column.getSortByToggleProps())}>
${column.render('Header')}
<span class="sort-header ${column.isSorted ? 'sort-header-active' : ''}">
${bla}
</span>
</th>`
}

function CRDTable() {
const table = useTable({ columns, data }, useSortBy );
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = table;

return html`
<div class="table-responsive">
<table class="table table-striped table-outer-bordered" ...${getTableProps()}>
<thead>
${headerGroups.map(group => html`
<tr ...${group.getHeaderGroupProps()}>
${group.headers.map(CRDHeader)}
</tr>
`)}
</thead>
<tbody ...${getTableBodyProps()}>
${rows.map(row => {
prepareRow(row)
return html`
<tr ...${row.getRowProps()}>
${row.cells.map(cell => html`
<td ...${cell.getCellProps()}>${cell.render('Cell')}</td>
`)}
</tr>
`
})}
</tbody>
</table>
</div>`;
}

render(html`<${CRDTable} />`, document.getElementById("crds"));
29 changes: 29 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"scripts": {
"build:doc": "esbuild js/doc.js --bundle --minify --sourcemap --format=esm --outfile=static/js/doc.js",
"build:home": "esbuild js/home.js --bundle --minify --sourcemap --outfile=static/js/home.js",
"build:org": "esbuild js/org.js --bundle --minify --sourcemap --outfile=static/js/org.js",
"build:nav": "esbuild js/nav.js --bundle --minify --sourcemap --outfile=static/js/nav.js",
"build": "pnpm build:nav && pnpm build:home && pnpm build:org && pnpm build:doc"
},
"devDependencies": {
"esbuild": "^0.19.10",
"esbuild-plugin-prismjs": "^1.0.8",
"shikiji": "^0.9.9"
},
"dependencies": {
"@fortawesome/fontawesome-free": "5.15.1",
"clipboard": "2.0.6",
"date-fns": "^3.0.0",
"dompurify": "2.2.2",
"eventemitter3": "4.0.7",
"halfmoon": "^1.1.1",
"htm": "3",
"marked": "1.2.4",
"prismjs": "^1.29.0",
"react": "16",
"react-dom": "16",
"react-table": "7",
"slugify": "1.4.6"
}
}
Loading