Skip to content

Commit

Permalink
create a minimal openAPI specification to reflect current routes
Browse files Browse the repository at this point in the history
  • Loading branch information
sdumetz committed Jul 9, 2024
1 parent 493b19a commit d3ed855
Show file tree
Hide file tree
Showing 15 changed files with 1,165 additions and 5 deletions.
4 changes: 4 additions & 0 deletions source/ui/MainView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import "./screens/UserSettings";
import "./screens/Home";
import "./screens/Tags";

import "./screens/Doc";

import Notification from "./composants/Notification";

Expand All @@ -43,6 +44,8 @@ export default class MainView extends router(i18n(withUser(LitElement))){
static "/ui/admin/.*" = ()=> html`<admin-panel></admin-panel>`;
@route()
static "/ui/scenes/:id/" = ({params}) => html`<scene-history name="${params.id}"></scene-history>`;
@route()
static "/ui/doc/.*" = () => html`<user-doc></user-doc>`

connectedCallback(): void {
super.connectedCallback();
Expand Down Expand Up @@ -71,6 +74,7 @@ export default class MainView extends router(i18n(withUser(LitElement))){
</form>
<nav-link .selected=${this.isActive("/ui/tags/")} href="/ui/tags/">Collections</nav-link>
<nav-link .selected=${this.isActive("/ui/doc/")} href="/ui/doc/">Documentation</nav-link>
${(this.user?.isAdministrator)?html`<nav-link .selected=${this.isActive("/ui/admin/")} href="/ui/admin/">${this.t("ui.administration")}</nav-link>`:""}
<div class="divider"></div>
<user-button .selected=${this.isActive("/ui/user/")} .user=${this.user}></user-button>
Expand Down
1 change: 1 addition & 0 deletions source/ui/composants/TagList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default class TagList extends LitElement{
.tags-list{
display: flex;
gap: 2px;
flex-wrap: wrap;
}
.tag, .add-tag{
Expand Down
6 changes: 6 additions & 0 deletions source/ui/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ declare module "*.webp" {
export default path;
}

declare module "*.yml"{
import type { Openapi2 } from 'screens/Doc/oas';
const definition :Openapi2;
export default definition;
}

// Webpack constant: build version
declare const ENV_VERSION: string;
// Webpack constant: true during development build
Expand Down
15 changes: 15 additions & 0 deletions source/ui/oas-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import showdown from "showdown";
import {parse} from 'yaml';

/**
* Simple one-shot yaml loader for webpack to use already-bundled yaml module
*/
export default function (source) {
const converter = new showdown.Converter();
// Apply some transformations to the source...
const obj = parse(source, (key, value)=>{
if(key === "description") return converter.makeHtml(value);
else return value;
});
return `export default ${JSON.stringify(obj)}`;
}
21 changes: 20 additions & 1 deletion source/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion source/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"devDependencies": {
"@types/showdown": "^2.0.0",
"lit-css-loader": "^2.0.0",
"showdown": "^2.1.0"
"showdown": "^2.1.0",
"yaml": "^2.4.5"
}
}
26 changes: 26 additions & 0 deletions source/ui/screens/Doc/DocHome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LitElement, customElement, html } from "lit-element";
import i18n from "../../state/translate";


@customElement("doc-home")
export default class DocHome extends i18n(LitElement){

createRenderRoot() {
return this;
}

render(){
return html`<div>
<h4>Getting started</h4>
<p>
You can head over to the main <a target="_blank" href="https://ecorpus.eu">documentation reference</a>
to learn more about the eCorpus database or to the <a href="https://smithsonian.github.io/dpo-voyager/">DPO Voyager</a> website to learn more specifically about voyager's features.
</p>
<h4>Integrating eCorpus</h4>
<p>
Developers might want to check out the <a href="/ui/doc/api/">API doc</a> (work in progress) to start experimenting.
</p>
<p>
</div>`
}
}
230 changes: 230 additions & 0 deletions source/ui/screens/Doc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@

import { LitElement, css, customElement, html, property } from "lit-element";

import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../../styles/common.scss';
import apiStyles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../../styles/apidoc.scss';

import definitions from "./openapi.yml";
import "./DocHome";


import "../../composants/Icon";
import "../../composants/Button";
import "../../composants/TagList";

import { Method, Operation, Path, Parameters } from "./oas";
import { navigate, route, router } from "../../state/router";


function resolveRefs<T>(t :T):T{
if(typeof t !== "object") return t;
if(Array.isArray(t)) return t.map(i=>resolveRefs(i)) as T;
if(!("$ref" in t)) return t;
if(typeof t["$ref"] !== "string" || !t["$ref"].startsWith("#")){
console.warn("Bad ref :", t["$ref"]);
return t;
}
const refs = t["$ref"].slice(1).split("/");
let ptr = definitions;
for(let ref of refs){
if(!ref) continue;
if(typeof ptr[ref] === "undefined"){
console.warn("Bad ref : ", t["$ref"]);
return t;
}
ptr = ptr[ref];
}
return ptr as T;
}


const operations = resolveRefs(Object.entries(definitions.paths).map(([pathname, {parameters, summary, ...operations}])=>{
return Object.entries(operations).map((op)=>([pathname, op[0], parameters, op[1]]));
}).flat()) as Array<[string, Method, Parameters, Operation]>;




console.log("Definitions : ", typeof definitions, definitions);

@customElement("user-doc")
export default class UserDoc extends router(LitElement){
path = "/ui/doc";

@route()
static "/" = ()=> html`<doc-home></doc-home>`
@route()
static "/api/" = ({parent})=> UserDoc.renderTags(parent);
@route()
static "/api/:tag" = ({parent})=> UserDoc.renderTags(parent);

createRenderRoot() {
return this;
}


static renderTags(that :UserDoc){
return definitions.tags.map((t)=>{
const active = that.isActive(`/ui/doc/api/${t.name}`)
return html`<tag-block ?expanded=${active} @select=${that.onTagClick} name=${t.name} .description=${t.description}></tag-block>`
});
}

render(){
let selIndex = definitions.tags.findIndex(t=>this.isActive(t.name));
return html`
<h2>eCorpus Documentation</h2>
<div class="main-grid">
<div class="grid-header" style="display:flex">
<nav-link .selected=${this.isActive("/ui/doc/", true)} href="${this.path}/">Home</nav-link>
<nav-link .selected=${this.isActive("/ui/doc/api/")} href="${this.path}/api/">API Doc</nav-link>
</div>
<div class="grid-toolbar">
${this.isActive("/ui/doc/api")?html `
<div class="section">
<h4>Sections</h4>
<tag-list .selected=${selIndex} .tags=${definitions.tags.map(t=>t.name)} @click=${this.onTagClick}></tag-list>
</div>
<div class="section">
<ui-button @click=${this.download} class="btn-main" icon="save" text="download openAPI specification"></ui-button>
</div>
`:null}
</div>
<div class="grid-content section">
${this.renderContent()}
</div>
`;
}

onTagClick = (ev :CustomEvent<string>) =>{
navigate(this, this.isActive(`/ui/doc/api/${ev.detail}`)? `/ui/doc/api/`:`/ui/doc/api/${ev.detail}`);
}

download = ()=>{
const el = document.createElement("a");
el.setAttribute("href", `data:application/json;base64,`+btoa(JSON.stringify(definitions, null, 2)));
el.setAttribute("download", "openapi.json");
el.style.display = 'none';
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
}
}

@customElement("tag-block")
export class TagBlock extends LitElement{
@property({attribute: true, type: String})
name :string;
@property({attribute: false, type: String})
description :string;

@property({attribute: true, reflect: true, type: Boolean})
expanded: boolean;

@property({attribute:false, type: String})
selected ?:string;

handleClick = (e:MouseEvent)=>{
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(new CustomEvent("select", {detail: this.name}));
}

handleSelect = (e:CustomEvent<string>)=>{

}

paths() :Array<[string, Path]> {
return Object.entries(definitions.paths);
}

operations() :Array<[string, Method, Parameters, Operation ]>{
return operations.filter(op=>op[3].tags?.indexOf(this.name)!= -1);
}

render(){
return html`<div class="tag-line">
<div class="tag-header" @click=${this.expanded?null:this.handleClick}>
<h4>${this.name}</h4>
<div class="tag-summary">
${unsafeHTML(this.description)}
</div>
<ui-icon @click=${this.handleClick} id="tag-caret" class="caret" name="caret-${this.expanded?"up":"down"}"></ui-icon>
</div>
<div class="tag-body">${this.expanded?this.operations().map(([pathname, method, parameters, operation])=>{
return html`<op-line ?expanded=${this.selected === operation.operationId} method=${method} pathname=${pathname} .parameters=${parameters} .operation=${operation}></op-line>`
}):null}</div>
</div>`
}
static styles = [
styles,
apiStyles,
];
}


@customElement("op-line")
export class OperationLine extends LitElement{
@property({attribute: true, reflect: true, type: String})
method :Method;
@property({attribute: true, reflect: true, type: String})
pathname :string;

@property({attribute: false, type: Object})
operation :Operation;

@property({attribute: false, type: Object})
parameters :Parameters;

@property({attribute: true, reflect: true, type: Boolean})
expanded :boolean = false;

createRenderRoot() {
return this;
}

connectedCallback(): void {
this.classList.add("path-line");
super.connectedCallback();
}

protected update(changedProperties: Map<string | number | symbol, unknown>): void {
if(changedProperties.has("pathname")){
this.id = this.pathname;
}
super.update(changedProperties);
}


onclick = (ev: MouseEvent) => {
ev.stopPropagation();
this.expanded = !this.expanded;
}

render(){
if(this.expanded) console.log("Operation :", this.parameters, this.operation);
const methodName = (this.method.startsWith("x-"))? this.method.slice(2) :this.method;
return html`
<span class="method ${methodName}">
${methodName}
</span>
<span class="pathname">
${this.pathname}
${(this.expanded && this.parameters?.length)? html`
<h4>Parameters</h4>
${this.parameters.map(p=>html`<div class="operation-parameters">
<h5>${p.name}</h5>
<div>
${unsafeHTML(p.description)}
</div>
</div>`)}
`:null}
</span>
<span class="op-summary">${unsafeHTML(this.operation.description)}</span>
<ui-icon id="tag-caret" class="caret" name="caret-${this.expanded?"up":"down"}"></ui-icon>
`;
}
}
Loading

0 comments on commit d3ed855

Please sign in to comment.