satellite/admin/ui: Create Web UI v1

This commit crate the Satellite Admin Web UI v1

* Bootstrap the files installing the project template:
  https://github.com/sveltejs/template
* Add the prettier prettier-plugin-svelte NPM packages as dev
  dependencies.
* Add an empty prettierrc.toml.
* Transform the project template to use Typescript rather than
  Javasript. See: https://github.com/sveltejs/template#using-typescript
* Replace the default favicon by the Storj logo.
* Create components in companion of some Typescript code which allows to
  generate a simple HTML page based on an opinionated definition of API.
* Implement all the Admin API client calls in the format that the UI
  generator components requires for rendering the UI from them.

Change-Id: I58fa586d68dc8998e5d89db169b8e90204f0a96d
This commit is contained in:
Ivan Fraixedes 2021-03-19 17:19:14 +01:00 committed by Ivan Fraixedes
parent 3122de4564
commit 583cdc6e0e
21 changed files with 3584 additions and 0 deletions

4
satellite/admin/ui/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/public/build/
.DS_Store

View File

View File

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

View File

@ -0,0 +1,45 @@
# Admin UI
## Implementation details
This is a project based on the [Svelte](https://svelte.dev) [template for apps](https://github.com/sveltejs/template).
The project templated was converted to Typescript following the instructions on its README.
The Web App is currently straightforward as we specified that v1 would be.
The v1 is just a simple web page that exposes the Admin API through some forms and allow to a call the API without needing to use some HTTP REST clients (e.g. Postman, cURL, etc.).
It doesn't offer any user authentication; the user has to know the API authorization token for using it.
The UI has a set of Svelte components that collaborate together to render an HTML form with input elements from the Admin API client.
The Svelte components expect some values of a certain Typescript interfaces, types, and classes, for being able to dynamically render the HTML form and elements.
Each source has a brief doc comment about its functionality.
## Development
Install the dependencies...
```bash
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
## Building for production mode
To create an optimised version of the app:
```bash
npm run build
```

2487
satellite/admin/ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
{
"name": "svelte-app",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public",
"validate": "svelte-check"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^8.0.0",
"@tsconfig/svelte": "^1.0.0",
"prettier": "^2.2.1",
"prettier-plugin-svelte": "^2.2.0",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0",
"svelte-check": "^1.0.0",
"svelte-preprocess": "^4.0.0",
"tslib": "^2.0.0",
"typescript": "^4.0.0"
},
"dependencies": {
"pretty-print-json": "^1.1.0",
"sirv-cli": "^1.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,67 @@
html,
body {
position: relative;
width: 100%;
height: 100%;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0, 100, 200);
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0, 80, 160);
}
label {
display: block;
}
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Storj Satellite Admin Console</title>
<link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="stylesheet" href="/global.css" />
<link rel="stylesheet" href="/pretty-print-json.css" />
<link rel="stylesheet" href="/build/bundle.css" />
<script defer src="/build/bundle.js"></script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,62 @@
/*! pretty-print-json v1.0.3 ~ github.com/center-key/pretty-print-json ~ MIT License */
.json-key {
color: brown;
}
.json-string {
color: olive;
}
.json-number {
color: navy;
}
.json-boolean {
color: teal;
}
.json-null {
color: dimgray;
}
.json-mark {
color: black;
}
a.json-link {
color: purple;
transition: all 400ms;
}
a.json-link:visited {
color: slategray;
}
a.json-link:hover {
color: blueviolet;
}
a.json-link:active {
color: slategray;
}
.dark-mode .json-key {
color: indianred;
}
.dark-mode .json-string {
color: darkkhaki;
}
.dark-mode .json-number {
color: deepskyblue;
}
.dark-mode .json-boolean {
color: mediumseagreen;
}
.dark-mode .json-null {
color: darkorange;
}
.dark-mode .json-mark {
color: silver;
}
.dark-mode a.json-link {
color: mediumorchid;
}
.dark-mode a.json-link:visited {
color: slategray;
}
.dark-mode a.json-link:hover {
color: violet;
}
.dark-mode a.json-link:active {
color: silver;
}

View File

@ -0,0 +1,90 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import svelte from "rollup-plugin-svelte";
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import livereload from "rollup-plugin-livereload";
import { terser } from "rollup-plugin-terser";
import sveltePreprocess from "svelte-preprocess";
import typescript from "@rollup/plugin-typescript";
import css from "rollup-plugin-css-only";
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require("child_process").spawn(
"npm",
["run", "start", "--", "--dev"],
{
stdio: ["ignore", "inherit", "inherit"],
shell: true,
}
);
process.on("SIGTERM", toExit);
process.on("exit", toExit);
},
};
}
export default {
input: "src/main.ts",
output: {
sourcemap: true,
format: "iife",
name: "app",
file: "public/build/bundle.js",
},
plugins: [
svelte({
preprocess: sveltePreprocess({ sourceMap: !production }),
compilerOptions: {
// enable run-time checks when not in production
dev: !production,
},
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: "bundle.css" }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ["svelte"],
}),
commonjs(),
typescript({
sourceMap: !production,
inlineSources: !production,
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload("public"),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser(),
],
watch: {
clearScreen: false,
},
};

View File

@ -0,0 +1,87 @@
<!--
Copyright (C) 2021 Storj Labs, Inc.
See LICENSE for copying information.
This component is the glue between the Admin API client and the "UIGenerator"
component.
It list all the operations that the API exposes and render the Web UI, through
the `UIGenerator.svelte` component, when the user selects the operation that
wants to perform.
-->
<script lang="ts">
import type { Operation } from "./ui-generator";
import { Admin } from "./api";
import UIGen from "./UIGenerator.svelte";
const baseURL = `${window.location.protocol}//${window.location.host}/api`;
let api: Admin;
let selectedGroupOp: Operation[];
let selectedOp: Operation;
let authToken: string;
function confirmAuthToken() {
if (authToken) {
api = new Admin(baseURL, authToken);
} else {
api = null;
}
}
</script>
<p>
In order to use the API you have to set the authentication token in the input
box and press enter or click the "confirm" button.
</p>
<p>
Token: <input
bind:value={authToken}
on:focus={() => {
api = null;
// This allows to select the empty item of the second select.
selectedOp = null;
}}
on:keyup={(e) => {
if (e.key.toLowerCase() === "enter") confirmAuthToken();
}}
type="password"
size="48"
/>
<button on:click={confirmAuthToken}>confirm</button>
</p>
{#if api}
<p>
Operation:
<select
bind:value={selectedGroupOp}
on:change={() => {
// This allows hiding the UIGen component when this select change until
// a new operations is selected in the following select element and also
// selecting the empty item of the select.
selectedOp = null;
}}
>
<option selected />
{#each Object.keys(api.operations) as group}
<option value={api.operations[group]}>{group}</option>
{/each}
</select>
{#if selectedGroupOp}
<select bind:value={selectedOp}>
<option selected />
{#each selectedGroupOp as op (op)}
<option value={op}>{op.name}</option>
{/each}
</select>
{/if}
</p>
<hr />
<p>
{#if selectedOp}
<UIGen operation={selectedOp} />
{/if}
</p>
{/if}

View File

@ -0,0 +1,50 @@
<!--
Copyright (C) 2021 Storj Labs, Inc.
See LICENSE for copying information.
The Svelte root component for this Web App.
-->
<script lang="ts">
import Api from "./Api.svelte";
</script>
<header>
<h1>Satellite Admin</h1>
</header>
<main>
<Api />
</main>
<footer>
<p>version: v1</p>
</footer>
<style>
header {
text-align: center;
padding: 1em;
max-width: 60ch;
margin: 0 auto;
}
footer {
position: fixed;
bottom: 0;
width: 97vw;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
}
@media (min-width: 640px) {
header {
max-width: none;
}
}
</style>

View File

@ -0,0 +1,107 @@
<!--
Copyright (C) 2021 Storj Labs, Inc.
See LICENSE for copying information.
This component render the passed "operation" value as an HTML form and a set of
HTML elements to enter data and specify values.
See `ui-generator.ts` file for knowing about the `Operation` interface which is
the type of the "operation" value to be passed in.
-->
<script lang="ts">
import { prettyPrintJson as prettyJSON } from "pretty-print-json";
import type { Operation } from "./ui-generator";
import UIInputText from "./UIGeneratorInputText.svelte";
import UISelect from "./UIGeneratorSelect.svelte";
import UITextarea from "./UIGeneratorTextarea.svelte";
type opArg = boolean | number | string;
function execOperation(op: Operation, args: opArg[]) {
result = op.func(...args).then((data) => {
form.reset();
return data;
});
}
$: {
if (operation !== prevOperation) {
opArgs = new Array(operation.params.length);
result = undefined;
prevOperation = operation;
}
}
export let operation: Operation;
let prevOperation: Operation;
let opArgs: any[] = new Array(operation.params.length);
let result: Promise<object | null>;
let form: HTMLFormElement;
let componentsMap = {
InputText: UIInputText,
Select: UISelect,
Textarea: UITextarea,
};
</script>
<div>
<p>{operation.desc}</p>
<form
bind:this={form}
on:submit|preventDefault={() => execOperation(operation, opArgs)}
>
{#each operation.params as param, i}
<svelte:component
this={componentsMap[param[1].constructor.name]}
label={param[0]}
config={param[1]}
bind:value={opArgs[i]}
/>
{/each}
<input type="submit" value="submit" />
</form>
</div>
<output>
{#if result !== undefined}
{#await result}
<p>Sending...</p>
{:then data}
<p class="successful">
<b>Operation successful</b>
{#if data != null}
<br /><br />
HTTP Response body:
<pre>{@html prettyJSON.toHtml(data)}</pre>
{/if}
</p>
{:catch err}
<p class="failure">
<b>Operation failed</b>
<br /><br />
{err.name}: {err.message}
{#if err.responseStatusCode}
<br />
HTTP Response status code: {err.responseStatusCode}
{#if err.responseBody}
<br />
HTTP Response body:
<pre>{@html prettyJSON.toHtml(err.responseBody)}</pre>
{/if}
{/if}
</p>
{/await}
{/if}
</output>
<style>
.failure b {
color: red;
text-decoration: underline;
}
.successful b {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,75 @@
<!--
Copyright (C) 2021 Storj Labs, Inc.
See LICENSE for copying information.
Children Svelte component of the `UIGenerator.svelte` component which renders
an HTML input element.
-->
<script lang="ts">
import { onMount } from "svelte";
import type { InputText } from "./ui-generator";
export let label: string;
export let config: InputText;
export let value: boolean | number | string = undefined;
// For avoiding Svelte validate errors with Typescript types, we cannot map
// the `value` variable directly to HTML elements.
let boolValue: boolean = undefined;
let numValue: number = undefined;
let strValue: string = undefined;
// Map the initial value property when has some value to the HTML element.
onMount(() => {
if (value) {
switch (typeof value) {
case "boolean":
boolValue = value;
break;
case "number":
numValue = value;
break;
case "string":
strValue = value;
break;
}
}
});
$: {
if (boolValue !== undefined) {
value = boolValue;
}
if (numValue !== undefined) {
value = numValue;
}
if (strValue !== undefined) {
value = strValue;
}
}
</script>
<label>
{label}
{#if config.required}<sup>*</sup>{/if}:
{#if config.type === "checkbox"}
<input
type="checkbox"
bind:checked={boolValue}
required={config.required}
/>
{:else if config.type === "email"}
<input type="email" bind:value={strValue} required={config.required} />
{:else if config.type === "number"}
<input type="number" bind:value={numValue} required={config.required} />
{:else if config.type === "password"}
<input type="password" bind:value={strValue} required={config.required} />
{:else if config.type === "text"}
<input type="text" bind:value={strValue} required={config.required} />
{:else}
<p style="color: red;">BUG: not mapped input type: {config.type}</p>
{/if}
</label>

View File

@ -0,0 +1,32 @@
<!--
Copyright (C) 2021 Storj Labs, Inc.
See LICENSE for copying information.
Children Svelte component of the `UIGenerator.svelte` component which renders
an HTML select element
-->
<script lang="ts">
import type { Select } from "./ui-generator";
export let label: string;
export let config: Select;
export let value: boolean | number | string = undefined;
</script>
<label>
{label}
{#if config.required}<sup>*</sup>{/if}:
{#if config.multiple}
<select bind:value required={config.required} multiple>
{#each config.options as option}
<option value={option.value}>{option.text}</option>
{/each}
</select>
{:else}
<select bind:value required={config.required}>
{#each config.options as option}
<option value={option.value}>{option.text}</option>
{/each}
</select>
{/if}
</label>

View File

@ -0,0 +1,28 @@
<!--
Copyright (C) 2021 Storj Labs, Inc.
See LICENSE for copying information.
Children Svelte component of the `UIGenerator.svelte` component which renders
an HTML text area element.
-->
<script lang="ts">
import type { Textarea } from "./ui-generator";
export let label: string;
export let config: Textarea;
export let value: string = undefined;
</script>
<label>
<div>
{label}{#if config.required}<sup>*</sup>{/if}:
</div>
<textarea required={config.required} bind:value />
</label>
<style>
label {
display: grid;
row-gap: 1em;
}
</style>

View File

@ -0,0 +1,322 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
/*
* This is the Admin API client exposing the API operations using the
* interfaces, types and classes of the `ui-generator.ts` for allowing the
* `UIGenerator.svelte` component to dynamically render their Web UI interface.
*/
import type { Operation } from "./ui-generator";
import { InputText, Textarea } from "./ui-generator";
// API must be implemented by any class which expose the access to a specific
// API.
export interface API {
operations: {
[key: string]: Operation[];
};
}
export class Admin {
readonly operations = {
APIKeys: [
{
name: "delete key",
desc: "Delete an API key",
params: [["API key", new InputText("text", true)]],
func: async (apiKey: string): Promise<null> => {
return this.fetch("DELETE", `apikeys/${apiKey}`) as Promise<null>;
},
},
],
project: [
{
name: "create",
desc: "Add a new project to a specific user",
params: [
["Owner ID (user ID)", new InputText("text", true)],
["Project Name", new InputText("text", true)],
],
func: async (ownerId: string, projectName: string): Promise<object> => {
return this.fetch("POST", "projects", null, { ownerId, projectName });
},
},
{
name: "delete",
desc: "Delete a specific project",
params: [["Project ID", new InputText("text", true)]],
func: async (projectId: string): Promise<null> => {
return this.fetch("DELETE", `projects/${projectId}`) as Promise<null>;
},
},
{
name: "get",
desc: "Get the information of a specific project",
params: [["Project ID", new InputText("text", true)]],
func: async (projectId: string): Promise<object> => {
return this.fetch("GET", `projects/${projectId}`);
},
},
{
name: "update",
desc: "Update the information of a specific project",
params: [
["Project ID", new InputText("text", true)],
["Project Name", new InputText("text", true)],
["Description", new InputText("text", false)],
],
func: async (
projectId: string,
projectName: string,
description: string
): Promise<null> => {
return this.fetch("PUT", `projects/${projectId}`, null, {
projectName,
description,
}) as Promise<null>;
},
},
{
name: "create API key",
desc: "Create a new API key for a specific project",
params: [
["Project ID", new InputText("text", true)],
["API key name", new InputText("text", true)],
],
func: async (projectId: string, name: string): Promise<object> => {
return this.fetch("POST", `projects/${projectId}/apikeys`, null, {
name,
});
},
},
{
name: "delete API key",
desc: "Delete a API key of a specific project",
params: [
["Project ID", new InputText("text", true)],
["API Key name", new InputText("text", true)],
],
func: async (projectId: string, apiKeyName: string): Promise<null> => {
return this.fetch(
"DELETE",
`projects/${projectId}/apikeys/${apiKeyName}`
) as Promise<null>;
},
},
{
name: "get API keys",
desc: "Get the API keys of a specific project",
params: [["Project ID", new InputText("text", true)]],
func: async (projectId: string): Promise<object> => {
return this.fetch("GET", `projects/${projectId}/apiKeys`);
},
},
{
name: "get project usage",
desc: "Get the current usage of a specific project",
params: [["Project ID", new InputText("text", true)]],
func: async (projectId: string): Promise<object> => {
return this.fetch("GET", `projects/${projectId}/usage`);
},
},
{
name: "get project limits",
desc: "Get the current limits of a specific project",
params: [["Project ID", new InputText("text", true)]],
func: async (projectId: string): Promise<object> => {
return this.fetch("GET", `projects/${projectId}/limit`);
},
},
{
name: "update project limits",
desc: "Update the limits of a specific project",
params: [
["Project ID", new InputText("text", true)],
["Storage (in bytes)", new InputText("number", false)],
["Bandwidth (in bytes)", new InputText("number", false)],
["Rate (requests per second)", new InputText("number", false)],
["Buckets (maximum number)", new InputText("number", false)],
],
func: async (
projectId: string,
usage: number,
bandwidth: number,
rate: number,
buckets: number
): Promise<null> => {
const query = this.urlQueryFromObject({
usage,
bandwidth,
rate,
buckets,
});
if (query === "") {
throw new APIError(
"nothing to update, at least one limit must be set"
);
}
return this.fetch(
"PUT",
`projects/${projectId}/limit`,
query
) as Promise<null>;
},
},
],
user: [
{
name: "create",
desc: "Create a new user account",
params: [
["email", new InputText("email", true)],
["full name", new InputText("text", false)],
["password", new InputText("password", true)],
],
func: async (
email: string,
fullName: string,
password: string
): Promise<object> => {
return this.fetch("POST", "users", null, {
email,
fullName,
password,
});
},
},
{
name: "delete",
desc: "Delete a user's account",
params: [["email", new InputText("email", true)]],
func: async (email: string): Promise<null> => {
return this.fetch("DELETE", `users/${email}`) as Promise<null>;
},
},
{
name: "get",
desc: "Get the information of a user's account",
params: [["email", new InputText("email", true)]],
func: async (email: string): Promise<object> => {
return this.fetch("GET", `users/${email}`);
},
},
{
name: "update",
desc: `Update the information of a user's account.
Blank fields will not be updated.`,
params: [
["current user's email", new InputText("email", true)],
["new email", new InputText("email", false)],
["full name", new InputText("text", false)],
["short name", new InputText("text", false)],
["partner ID", new InputText("text", false)],
["password hash", new InputText("text", false)],
],
func: async (
currentEmail: string,
email?: string,
fullName?: string,
shortName?: string,
partnerID?: string,
passwordHash?: string
): Promise<null> => {
return this.fetch("PUT", `users/${currentEmail}`, null, {
email,
fullName,
shortName,
partnerID,
passwordHash,
}) as Promise<null>;
},
},
],
};
private readonly baseURL: string;
constructor(baseURL: string, private readonly authToken: string) {
this.baseURL = baseURL.endsWith("/")
? baseURL.substring(0, baseURL.length - 1)
: baseURL;
}
protected async fetch(
method: "DELETE" | "GET" | "POST" | "PUT",
path: string,
query?: string,
data?: object
): Promise<object | null> {
const url = this.apiURL(path, query);
const headers = new window.Headers({
Authorization: this.authToken,
});
let body: string;
if (data) {
headers.set("Content-Type", "application/json");
body = JSON.stringify(data);
}
const resp = await window.fetch(url, { method, headers, body });
if (!resp.ok) {
let body: object;
if (resp.headers.get("Content-Type") === "application/json") {
body = await resp.json();
}
throw new APIError("server response error", resp.status, body);
}
if (resp.headers.get("Content-Type") === "application/json") {
return resp.json();
}
return null;
}
protected apiURL(path: string, query?: string): string {
path = path.startsWith("/") ? path.substring(1) : path;
if (!query) {
query = "";
} else {
query = "?" + query;
}
return `${this.baseURL}/${path}${query}`;
}
/**
* Transform an object to a URL query string.
* It discards any object field whose value is undefined.
*
* NOTE it doesn't recurs on values which are objects.
*/
protected urlQueryFromObject(values: object): string {
const queryParts = [];
for (const name of Object.keys(values)) {
const val = values[name];
if (val === undefined) {
continue;
}
queryParts.push(`${name}=${encodeURIComponent(val)}`);
}
return queryParts.join("&");
}
}
class APIError extends Error {
constructor(
public readonly msg: string,
public readonly responseStatusCode?: number,
public readonly responseBody?: object | string
) {
super(msg);
}
}

View File

@ -0,0 +1,14 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
/*
* Svelte App entry point.
*/
import App from "./App.svelte";
const app = new App({
target: document.body,
});
export default app;

View File

@ -0,0 +1,54 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
/*
* A set of interfaces, classes and types that allow the `UIGenerator.svelte`
* component to generate an HTML form with a set of HTML elements to enter data
* and specify values.
*
* A REST API can use these types to dynamically generate a Web UI for it.
*/
export interface Operation {
// name is the operation name.
name: string;
// desc is the description of the operation.
desc: string;
// params is an array of tuples where each tuple corresponds to one parameter
// of 'func'. Each tuple has 2 elements, the first is the parameters name and
// the second how it's mapped to the UI. The orders must match the order of
// the 'func' parameters.
// The parameter's name is what is going to be show next to the input field,
// so it has to be descriptive for the users to know that they have to set.
params: [string, ParamUI][];
// func is the API function call. They always have to return a promise which
// resolves with an object or null.
// On a resolved promise, an object is the response body of an API call and
// null is used when the API operation doesn't return any payload (e.g. PUT
// operations).
func: (...p: any) => Promise<object | null>;
}
type ParamUI = InputText | Select | Textarea;
export class InputText {
constructor(
public readonly type: "checkbox" | "email" | "number" | "password" | "text",
public readonly required: boolean
) {}
}
export class Select {
constructor(
public readonly multiple: boolean,
public readonly required: boolean,
public readonly options: {
text: string;
value: boolean | number | string;
}[]
) {}
}
export class Textarea {
constructor(public readonly required: boolean) {}
}

View File

@ -0,0 +1,6 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}