From 8f9415de5d83d900f2059a5f481e90a7427a5453 Mon Sep 17 00:00:00 2001 From: Sebastien DUMETZ Date: Fri, 5 Jul 2024 15:51:16 +0200 Subject: [PATCH] create a minimal openAPI specification to reflect current routes --- source/ui/MainView.ts | 4 + source/ui/composants/TagList.ts | 1 + source/ui/global.d.ts | 6 + source/ui/oas-loader.mjs | 15 ++ source/ui/package-lock.json | 21 +- source/ui/package.json | 3 +- source/ui/screens/Doc/DocHome.ts | 26 ++ source/ui/screens/Doc/index.ts | 230 ++++++++++++++++++ source/ui/screens/Doc/oas.d.ts | 287 ++++++++++++++++++++++ source/ui/screens/Doc/openapi.yml | 380 ++++++++++++++++++++++++++++++ source/ui/state/router.ts | 4 +- source/ui/styles/apidoc.scss | 184 +++++++++++++++ source/ui/styles/layout.scss | 3 + source/ui/styles/theme.scss | 2 +- source/ui/webpack.config.js | 4 + 15 files changed, 1165 insertions(+), 5 deletions(-) create mode 100644 source/ui/oas-loader.mjs create mode 100644 source/ui/screens/Doc/DocHome.ts create mode 100644 source/ui/screens/Doc/index.ts create mode 100644 source/ui/screens/Doc/oas.d.ts create mode 100644 source/ui/screens/Doc/openapi.yml create mode 100644 source/ui/styles/apidoc.scss diff --git a/source/ui/MainView.ts b/source/ui/MainView.ts index ef1c75e2..9bcd36cc 100644 --- a/source/ui/MainView.ts +++ b/source/ui/MainView.ts @@ -21,6 +21,7 @@ import "./screens/UserSettings"; import "./screens/Home"; import "./screens/Tags"; +import "./screens/Doc"; import Notification from "./composants/Notification"; @@ -44,6 +45,8 @@ export default class MainView extends router(i18n(withUser(LitElement))){ static "/ui/admin/.*" = ()=> html``; @route() static "/ui/scenes/:id/" = ({params}) => html``; + @route() + static "/ui/doc/.*" = () => html`` connectedCallback(): void { super.connectedCallback(); @@ -72,6 +75,7 @@ export default class MainView extends router(i18n(withUser(LitElement))){ Collections + Documentation ${(this.user?.isAdministrator)?html`${this.t("ui.administration")}`:""}
diff --git a/source/ui/composants/TagList.ts b/source/ui/composants/TagList.ts index 00df495a..f86f11f6 100644 --- a/source/ui/composants/TagList.ts +++ b/source/ui/composants/TagList.ts @@ -53,6 +53,7 @@ export default class TagList extends LitElement{ .tags-list{ display: flex; gap: 2px; + flex-wrap: wrap; } .tag, .add-tag{ diff --git a/source/ui/global.d.ts b/source/ui/global.d.ts index bc98a772..0d5296b4 100644 --- a/source/ui/global.d.ts +++ b/source/ui/global.d.ts @@ -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 diff --git a/source/ui/oas-loader.mjs b/source/ui/oas-loader.mjs new file mode 100644 index 00000000..1e90c01d --- /dev/null +++ b/source/ui/oas-loader.mjs @@ -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)}`; +} \ No newline at end of file diff --git a/source/ui/package-lock.json b/source/ui/package-lock.json index 2068e9f3..fe9d9075 100644 --- a/source/ui/package-lock.json +++ b/source/ui/package-lock.json @@ -15,7 +15,8 @@ "devDependencies": { "@types/showdown": "^2.0.0", "lit-css-loader": "^2.0.0", - "showdown": "^2.1.0" + "showdown": "^2.1.0", + "yaml": "^2.4.5" } }, "node_modules/@pwrs/lit-css": { @@ -111,6 +112,18 @@ "engines": { "node": ">=6.4.0" } + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } } }, "dependencies": { @@ -185,6 +198,12 @@ "resolved": "https://registry.npmjs.org/uglifycss/-/uglifycss-0.0.29.tgz", "integrity": "sha512-J2SQ2QLjiknNGbNdScaNZsXgmMGI0kYNrXaDlr4obnPW9ni1jljb1NeEVWAiTgZ8z+EBWP2ozfT9vpy03rjlMQ==", "dev": true + }, + "yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true } } } diff --git a/source/ui/package.json b/source/ui/package.json index c11d0061..9c74d7f4 100644 --- a/source/ui/package.json +++ b/source/ui/package.json @@ -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" } } diff --git a/source/ui/screens/Doc/DocHome.ts b/source/ui/screens/Doc/DocHome.ts new file mode 100644 index 00000000..97383147 --- /dev/null +++ b/source/ui/screens/Doc/DocHome.ts @@ -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`
+

Getting started

+

+ You can head over to the main documentation reference + to learn more about the eCorpus database or to the DPO Voyager website to learn more specifically about voyager's features. +

+

Integrating eCorpus

+

+ Developers might want to check out the API doc (work in progress) to start experimenting. +

+

+

` + } +} \ No newline at end of file diff --git a/source/ui/screens/Doc/index.ts b/source/ui/screens/Doc/index.ts new file mode 100644 index 00000000..ef55ad4d --- /dev/null +++ b/source/ui/screens/Doc/index.ts @@ -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{ + 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`` + @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`` + }); + } + + render(){ + let selIndex = definitions.tags.findIndex(t=>this.isActive(t.name)); + return html` +

eCorpus Documentation

+
+
+ Home + API Doc +
+
+ ${this.isActive("/ui/doc/api")?html ` +
+

Sections

+ t.name)} @click=${this.onTagClick}> +
+
+ +
+ `:null} +
+
+ ${this.renderContent()} +
+ + `; + } + + onTagClick = (ev :CustomEvent) =>{ + 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)=>{ + + } + + 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`
+
+

${this.name}

+
+ ${unsafeHTML(this.description)} +
+ +
+
${this.expanded?this.operations().map(([pathname, method, parameters, operation])=>{ + return html`` + }):null}
+
` + } + 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): 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` + + ${methodName} + + + ${this.pathname} + ${(this.expanded && this.parameters?.length)? html` +

Parameters

+ ${this.parameters.map(p=>html`
+
${p.name}
+
+ ${unsafeHTML(p.description)} +
+
`)} + + `:null} +
+ ${unsafeHTML(this.operation.description)} + + `; + } +} \ No newline at end of file diff --git a/source/ui/screens/Doc/oas.d.ts b/source/ui/screens/Doc/oas.d.ts new file mode 100644 index 00000000..a0a00544 --- /dev/null +++ b/source/ui/screens/Doc/oas.d.ts @@ -0,0 +1,287 @@ +export interface Openapi2 { + openapi: string; + info: Info; + servers: Server[]; + tags: Tag[]; + paths: Paths; + components: Components; +} + +export interface Components { + parameters: Parameters; + responses: Responses; + schemas: Schemas; +} + +export interface File { + name: string; + in: string; + required: boolean; + schema: AuthorClass; + description: string; + examples?: Examples; + example?: string; +} + +export interface Examples { + folder: Document; + thumbnail: Document; + document: Document; +} + +export interface Document { + summary: string; + value: string; +} + +export interface AuthorClass { + type: Type; +} + +export enum Type { + Boolean = "boolean", + Object = "object", + String = "string", +} + +export interface Responses { + HTTPError: HTTPError; +} + +export interface HTTPError { + description: string; + content: HTTPErrorContent; +} + +export interface HTTPErrorContent { + "application/json": PurpleApplicationJSON; + "text/plain": TextPlain; + "text/html": TextHTML; +} + +export interface PurpleApplicationJSON { + schema: PurpleSchema; +} + +export interface PurpleSchema { + type: Type; + required: string[]; + properties: SchemaProperties; +} + +export interface SchemaProperties { + code: Code; + message: AuthorClass; +} + +export interface Code { + description: string; + type: string; + format: string; + minimum: number; + maximum: number; +} + +export interface TextHTML { + schema: AuthorClass; +} + +export interface TextPlain { + schema: Username; +} + +export interface Username { + type: Type; + example: string; +} + +export interface Schemas { + Scene: Scene; + User: User; + Uid: Uid; + AccessType: AccessType; +} + +export interface AccessType { + type: Type; + enum: string[]; +} + +export interface Scene { + type: Type; + required: string[]; + properties: SceneProperties; +} + +export interface SceneProperties { + ctime: Time; + mtime: Time; + author_id: AuthorID; + author: AuthorClass; + id: AuthorID; + name: AuthorClass; + thumb: Thumb; + access: Access; +} + +export interface Access { + type: Type; + required: string[]; + properties: AccessProperties; +} + +export interface AccessProperties { + default: Any; + any: Any; + user: Any; +} + +export interface Any { + "$ref:\"#/components/schemas/AccessType\"": null; +} + +export interface AuthorID { + "$ref:\"#/components/schemas/Uid\"": null; +} + +export interface Time { + type: Type; + format: string; +} + +export interface Thumb { + type: Type; + desciption: string; +} + +export interface Uid { + type: Type; + pattern: string; + description: string; +} + +export interface User { + type: Type; + required: string[]; + properties: UserProperties; +} + +export interface UserProperties { + uid: UidElement; + username: Username; + isAdministrator: AuthorClass; +} + +export interface UidElement { + $ref: string; +} + +export interface Info { + title: string; + version: string; + summary: string; + description: string; + contact: Contact; + license: License; +} + +export interface Contact { + name: string; + url: string; + email: string; +} + +export interface License { + name: string; + url: string; +} + + +export type Paths = Record; + +export type Path = Record &{ + summary?: string, + parameters: Parameters, +}; + +export type Parameters = Parameter[] + +export interface Parameter{ + name :string; + in :"path"|"query"; + + description :string; +} + +export interface Operation { + tags: string[]; + operationId: string; + description: string; + responses?: { [key: string]: Response }; + requestBody?: RequestBody; +} + +export type Method = "get"|"post"|"put"|"delete"|"patch"|"x-mkcol"|"x-propfind"|"x-copy"|"x-move" + +export interface RequestBody { + description: string; + required: boolean; + content: RequestBodyContent; +} + +export interface RequestBodyContent { + "application/json"?: TextHTML; + "model/gltf-binary"?: ModelGltfBinary; +} + +export interface ModelGltfBinary { + schema: ModelGltfBinarySchema; +} + +export interface ModelGltfBinarySchema { + type: Type; + format: string; + description: string; +} + +export interface Response { + description?: string; + content?: ResponseContent; + $ref?: string; +} + +export interface ResponseContent { + "application/json": FluffyApplicationJSON; +} + +export interface FluffyApplicationJSON { + schema: FluffySchema; +} + +export interface FluffySchema { + type: string; + items: Items; +} + +export interface Items { + type?: Type; + required?: string[]; + properties?: ItemsProperties; + $ref?: string; +} + +export interface ItemsProperties { + uid: UidElement; + username: AuthorClass; + access: UidElement; +} + + +export interface Server { + url: string; +} + +export interface Tag { + name: string; + description: string; +} diff --git a/source/ui/screens/Doc/openapi.yml b/source/ui/screens/Doc/openapi.yml new file mode 100644 index 00000000..f4761806 --- /dev/null +++ b/source/ui/screens/Doc/openapi.yml @@ -0,0 +1,380 @@ +openapi: '3.1.0' +info: + title: eCorpus + version: '1.0.0' + summary: HTTP API for eCorpus + description: | + This HTTP API provides all necessary routes to access and edit scenes stored on an eCorpus instance under the `/scenes` path. + Additionally it provides a number of namespaced utilities + for **users** management (`/users`), + **authentication** and ACL edition (`/auth`), + changes **history** management (`/history`) + or gathering scenes under **collections** (`/tags`). + + It provides some webDAV utility routes for the `/scenes` resources + but is far from [Class 1](http://www.webdav.org/specs/rfc4918.html#rfc.section.18.1) Compliance: + Only routes that are necessary for proper [Voyager](https://smithsonian.github.io/dpo-voyager/) support are implemented. + WebDAV-specific methods are defined as an extension with a `x-` prefix to prevent breaking openAPI tooling + + Other namespaces tends to adhere to a stricter REST philosophy where possible. + contact: + name: eCorpus Support + url: https://github.com/Holusion/eCorpus + email: contact@holusion.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + +servers: + - url: https://ecorpus.holusion.com + +tags: + - name: admin + description: Administrative tasks routes + - name: auth + description: Authentication, access control querying and edition routes. + - name: history + description: | + history management utilities for the `/scenes` namespace. + Scene names in `/history` directly and uniquely maps to scenes in `/scenes`. + - name: scenes + description: | + Where all the actual data is stored + API design for the `/scenes/*` makes use of the liberal definition of + [GET for collections](https://datatracker.ietf.org/doc/html/rfc2518#section-8.4) in the webDAV specification + to return well-defined JSON documents for those queries, allowing most use cases to bypass cumbersome PROPFIND queries + - name: tags + description: | + collections (tags) management routes. + - name: users + description: Users management + +paths: + /scenes: + summary: Collection of the scenes stored on this service + get: + tags: [scenes] + operationId: getScenes + description: | + get a list of scenes with optional search parameters. Similar to PROPFIND but will return JSON. + Provides advanced search and pagination semantics + responses: + '200': + description: a list of scenes matching this query + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Scene" + post: + tags: [scenes] + operationId: postScenes + description: import an archive of scenes to be extracted into the `scenes/` folder + x-propfind: + tags: [scenes] + operationId: propfindScenes + description: fetch all readable content in `scenes/` + /scenes/{scene}: + parameters: + - $ref: '#/components/parameters/scene' + get: + tags: [scenes] + operationId: getScene + description: get a scene's metadata. + x-mkcol: + tags: [scenes] + operationId: mkScene + description: creates a new empty scene. This scene will essentially be invisible until populated + x-propfind: + tags: [scenes] + operationId: propfindScene + description: fetch the scene's content + delete: + tags: [scenes] + operationId: deleteScene + description: Archives a scene + post: + tags: [scenes] + operationId: postScene + description: creates a new scene using attached data + requestBody: + description: "scene initialization data" + required: true + content: + model/gltf-binary: + schema: + type: string + format: binary + description: A `.glb` model file + + patch: + tags: [scenes] + operationId: patchScene + description: Edit scene's metadata + requestBody: + description: Scene patch data + required: true + content: + application/json: + schema: + type: object + /scenes/{scene}/{file}: + parameters: + - $ref: '#/components/parameters/scene' + - $ref: '#/components/parameters/file' + get: + tags: [scenes] + operationId: getFile + description: get a file in scene + put: + tags: [scenes] + operationId: putFile + description: overwrite the file with new content + x-copy: + tags: [scenes] + operationId: copyFile + description: copy a file to another location in the same scene + x-move: + tags: [scenes] + operationId: moveFile + description: move a file to another location in the same scene + delete: + tags: [scenes] + operationId: deleteFile + description: archives a file. It is still accessible through the history API + x-mkcol: + tags: [scenes] + operationId: mkFolder + description: creates a folder in a scene + x-propfind: + tags: [scenes] + operationId: propfindFile + description: get a file's properties + /history/{scene}: + parameters: + - $ref: '#/components/parameters/scene' + get: + tags: [history] + operationId: getHistory + description: get a full history of a scene's modifications + post: + tags: [history] + operationId: postHistory + description: edit a scene's history + /history/{scene}/files: + parameters: + - $ref: '#/components/parameters/scene' + get: + tags: [history] + operationId: getFileHistory + description: list all files in the scenes in their current state + /tags: + get: + tags: [tags] + operationId: getTags + description: get a list of tags on this server + /tags/{tag}: + parameters: + - name: tag + in: path + required: true + schema: {type: "string"} + description: name of a tag + get: + tags: [tags] + operationId: getTag + description: get all scenes associated with this tag + /users: + get: + tags: [users] + operationId: getUsers + description: get a list of registered users + responses: + '200': + description: An array of all users registered on this server + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/User"} + '401': + $ref: "#/components/responses/HTTPError" + post: + tags: [users] + operationId: postUser + description: create a new user + /users/{uid}: + parameters: + - name: uid + in: path + required: true + schema: {type: "string", pattern: '^\d+$'} + description: unique ID of an user (stays stable through user renames) + delete: + tags: [users] + operationId: deleteUser + description: delete a user + patch: + tags: [users] + operationId: patchUser + description: change a user's data + /auth: + get: + tags: [auth] + operationId: getAuth + description: get login data + post: + tags: [auth] + operationId: postAuth + description: log-in to the server + /auth/login/{username}/link: + parameters: + - name: username + in: path + required: true + schema: {type: "string"} + description: human-readable unique name of an user + get: + tags: [auth] + operationId: getAuthLink + description: get a login link for this user + post: + tags: [auth] + operationId: postAuthLink + description: generate and send a login link for this user + /auth/logout: + post: + tags: [auth] + operationId: postLogout + description: delete this request's credentials + /auth/access/{scene}: + parameters: + - $ref: '#/components/parameters/scene' + get: + tags: [auth] + operationId: getAccess + description: get a scene's access rights + responses: + '200': + description: Access map defined for this scene + content: + application/json: + schema: + type: array + items: + type: object + required: ["uid", "username", "access"] + properties: + uid: { $ref: "#/components/schemas/Uid"} + username: {type: string} + access: { $ref: "#/components/schemas/AccessType" } + '401': + $ref: "#/components/responses/HTTPError" + patch: + tags: [auth] + operationId: patchAccess + description: edit a scene's access rights +# Administrative data. Might contain server configuration routes in the future + /admin/stats: + get: + tags: [admin] + operationId: getAdminStats + description: get server stats + /admin/mailtest: + post: + tags: [admin] + operationId: postAdminMailtest + description: sends a test email + +components: + parameters: + scene: + name: scene + in: path + required: true + schema: {type: string } + description: unique name of a scene + example: foo + file: + name: file + in: path + required: true + schema: {type: "string"} + description: | + relative path to a scene's file. + Might contain slashs, though openAPI spec won't allow them in test queries + examples: + folder: + summary: a file in a nested folder + value: models/foo.glb + thumbnail: + summary: a thumbnail for this scene + value: "scene-image-thumb.jpg" + document: + summary: a voyager scene document file + value: scene.svx.json + responses: + 'HTTPError': + description: Generic HTTP error response whose content depends on the request's "Accept" header + content: + application/json: + schema: + type: object + required: ["code", "message"] + properties: + code: + description: | + [HTTP Status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) code + type: number + format: int32 + minimum: 100 + maximum: 599 + message: + type: string + text/plain: + schema: + type: "string" + example: "Bad Request" + text/html: + schema: + type: "string" + + schemas: + Scene: + type: object + required: ["ctime", "mtime", "author_id", "author", "id", "access"] + properties: + ctime: {type: "string", format: "date-time"} + mtime: {type: "string", format: "date-time"} + author_id: {$ref:"#/components/schemas/Uid"} + author: {type: "string"} + id: {$ref:"#/components/schemas/Uid"} + name: {type: "string"} + thumb: {type: "string", desciption: "URI to the scene's thumbnail representation if it exists"} + access: { + type: object, + required: ["any", "default"], + properties: { + default: {$ref:"#/components/schemas/AccessType"}, + any: {$ref:"#/components/schemas/AccessType"}, + user: {$ref:"#/components/schemas/AccessType"}, + } + } + User: + type: object + required: ["uid", "username", "isAdministrator"] + properties: + uid: {$ref: "#/components/schemas/Uid"} + username: {type: "string", example: "alice" } + isAdministrator: {type: "boolean"} + Uid: + type: "string" + pattern: '^\d+$' + description: + string representation of unique IDs. + Applicable for users or scenes, but uids are not expected to be unique across namespaces + UIDs are often stringified to prevent rounding errors on large intergers in the javascript engine + AccessType: + type: string + enum: ["none", "read", "write", "admin"] diff --git a/source/ui/state/router.ts b/source/ui/state/router.ts index 1847aea8..51f4ca09 100644 --- a/source/ui/state/router.ts +++ b/source/ui/state/router.ts @@ -8,14 +8,14 @@ export interface RouteParams{ export type RouteHandler = (props :RouteParams)=> TemplateResult; -export interface Route{ +export interface RouteDeclaration{ pattern :RegExp; content :RouteHandler; } export declare class Router{ path :string; - static routes :Route[]; + static routes :RouteDeclaration[]; get route() :URL; isActive(pathname :string, strict?:boolean) :boolean; renderContent() :TemplateResult; diff --git a/source/ui/styles/apidoc.scss b/source/ui/styles/apidoc.scss new file mode 100644 index 00000000..bff43695 --- /dev/null +++ b/source/ui/styles/apidoc.scss @@ -0,0 +1,184 @@ + +.tag-line{ + border-bottom: 1px solid var(--color-secondary); +} + +:host(:hover:not([expanded])) .tag-line { + background-color: var(--color-highlight); +} + +.tag-header{ + display: flex; + justify-content: space-between; + align-items: baseline; + cursor: pointer; + h4{ + flex-grow: 1; + padding-right: 1rem; + font-family: var(--font-body); + color: var(--color-text); + } + + .tag-summary{ + max-width: 50%; + p{ + text-align: left; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + +} + + +:host([expanded]) .tag-header{ + cursor: initial; + .tag-summary{ + p{ + white-space: wrap; + } + } + +} +.tag-body{ + box-sizing: border-box; + max-height: 0; + overflow: hidden; + display: grid; + grid-template-columns: auto minmax(auto, 1fr) 1fr 24px; + grid-auto-rows: auto; + align-items: baseline; + transition: max-height .4s ease; + &:not(:empty){ + padding-bottom: 1rem; + } +} + +:host([expanded]) .tag-body{ + max-height: 1200px; + overflow-y: auto; +} + +.path-line{ + display: grid; + grid-column: 1 / span 4; + grid-template-columns: auto minmax(auto, 1fr) 1fr 24px; + grid-template-columns: subgrid; + grid-template-rows: subgrid; + align-items: baseline; + padding: 4px; + &:nth-child(2n){ + background-color: rgba(0, 0, 0, 0.1); + } + &:nth-child(2n+1){ + background-color: rgba(0, 0, 0, 0.3); + } + + &:hover:not([expanded]){ + background-color: var(--color-highlight2); + } + + &:not(.expanded){ + cursor: pointer; + } + + > *{ + min-width: 0; + } + + .method{ + font-weight: bold; + box-sizing: border-box; + width: 12ch; + padding: .5rem .25ch; + background-color: var(--color-highlight); + text-transform: uppercase; + text-align: center; + border-radius: 4px; + + &.get{ + background-color: #2f8132; + } + &.post{ + background-color: #186faf; + } + &.put{ + background-color: #FD7E14; + } + &.delete{ + background-color: #dc3545; + } + &.patch{ + background-color: #ffc107; + } + &.mkcol{ + background-color: #38b3f9; + } + &.propfind{ + background-color: #28a745; + } + &.copy, &.move{ + background-color: #6f42c1; + } + } + + .pathname{ + padding-left: .5rem; + } + + .op-summary > p{ + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + text-align: right; + margin: 0; + } + &[expanded] .op-summary > p{ + white-space: wrap; + text-align: left; + } + + .caret{ + width: 16px; + color: var(--color-text); + fill: currentColor; + } + + .operation-parameters{ + display: flex; + justify-content: space-between; + > * { + flex-grow: 1; + &:first-child{ + flex: 0 0 25%; + } + } + } +} +code{ + background-color: rgba(230, 185, 0, 0.1); +} +a[href], a:-webkit-any-link { + text-decoration: none; + &:hover{ + box-shadow: inset 0 -2px 0 #E6B900; + } +} + + +.caret{ + box-sizing: border-box; + width: 1.5rem; + padding-left: .5rem; + color: var(--color-text); + + &:hover{ + color: #aaa; + } + fill: currentColor; + cursor: pointer; + svg{ + margin-bottom: -8px; + } +} \ No newline at end of file diff --git a/source/ui/styles/layout.scss b/source/ui/styles/layout.scss index 327956cf..7f5e75d2 100644 --- a/source/ui/styles/layout.scss +++ b/source/ui/styles/layout.scss @@ -80,6 +80,9 @@ corpus-list, home-page { 'header' 'content'; } + > *{ + min-width: 0; + } .grid-header{ grid-area: header; } diff --git a/source/ui/styles/theme.scss b/source/ui/styles/theme.scss index 4b50e8e8..16ee6b75 100644 --- a/source/ui/styles/theme.scss +++ b/source/ui/styles/theme.scss @@ -2,6 +2,6 @@ @import url("https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap"); html:root { - --font-body: 'Open Sans', 'Liberation', 'Roboto', 'sans-serif'; + --font-body: 'Open Sans', 'Liberation Sans', 'Roboto', 'sans-serif'; --font-heading: 'Noto Serif', 'serif'; } \ No newline at end of file diff --git a/source/ui/webpack.config.js b/source/ui/webpack.config.js index 6b4775d2..8a89eae8 100644 --- a/source/ui/webpack.config.js +++ b/source/ui/webpack.config.js @@ -154,6 +154,10 @@ module.exports = function createAppConfig(env, argv={}) publicPath: "/dist/", } }, + { + test: /openapi\.ya?ml$/i, + use: path.resolve(dirs.source,"oas-loader.mjs") + } ] },