satellite/admin/ui: Migrate to SvelteKit
Migrate the satellite admin UI web app from the Svelte template used to generate a Svelte App scaffolding to SvelteKit. There aren't any functional changes in the application, however, the commit has a lot because: 1. SvelteKit uses a different directory layout and constraints to it, so the files have been moved. 2. The files have changed its formatting due to the new default linter configurations that SvelteKit uses. 3. The linter detected some issues with using `object` and `any` types in Typescript, so they have been replaced by better general types (e.g. Record). The migration allows to use the new tooling rather than Rollup directly, besides that will empower the future of it when it needs more features (e.g. different routes, etc.). Change-Id: Ifa6736c13585708337f6c5a59388077b784eaddd
This commit is contained in:
parent
db0bd38d95
commit
5573ece848
@ -120,7 +120,7 @@ pipeline {
|
|||||||
dir('satellite/admin/ui') {
|
dir('satellite/admin/ui') {
|
||||||
sh 'npm ci --prefer-offline --no-audit'
|
sh 'npm ci --prefer-offline --no-audit'
|
||||||
sh 'npm run build'
|
sh 'npm run build'
|
||||||
sh 'rm -rf public/build' // Remove the build directory for avoiding linting those files.
|
sh 'rm -rf build .svelte-kit' // Remove these directories for avoiding linting their files.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,6 +154,11 @@ pipeline {
|
|||||||
sh 'golangci-lint --config /go/ci/.golangci.yml -j=2 run'
|
sh 'golangci-lint --config /go/ci/.golangci.yml -j=2 run'
|
||||||
sh 'check-mod-tidy -mod ../.build/testsuite.go.mod.orig'
|
sh 'check-mod-tidy -mod ../.build/testsuite.go.mod.orig'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dir("satellite/admin/ui") {
|
||||||
|
sh 'npm run check'
|
||||||
|
sh 'npm run lint'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Cross Compile') {
|
stage('Cross Compile') {
|
||||||
@ -352,7 +357,6 @@ pipeline {
|
|||||||
stage('satellite/admin/ui') {
|
stage('satellite/admin/ui') {
|
||||||
steps {
|
steps {
|
||||||
dir("satellite/admin/ui") {
|
dir("satellite/admin/ui") {
|
||||||
sh 'npm run validate'
|
|
||||||
sh script: 'npm audit', returnStatus: true
|
sh script: 'npm audit', returnStatus: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
Makefile
6
Makefile
@ -178,8 +178,8 @@ multinode-console:
|
|||||||
|
|
||||||
.PHONY: satellite-admin-ui
|
.PHONY: satellite-admin-ui
|
||||||
satellite-admin-ui:
|
satellite-admin-ui:
|
||||||
# build web assets
|
# remove the file that keep the assets directory for not breaking in development due to the `go:embed` directive
|
||||||
rm -rf satellite/admin/ui/public/build
|
rm -rf satellite/admin/ui/assets/.gitignore
|
||||||
# install npm dependencies for being embedded by Go embed.
|
# install npm dependencies for being embedded by Go embed.
|
||||||
docker run --rm -i \
|
docker run --rm -i \
|
||||||
--mount type=bind,src="${PWD}",dst=/go/src/storj.io/storj \
|
--mount type=bind,src="${PWD}",dst=/go/src/storj.io/storj \
|
||||||
@ -187,7 +187,7 @@ satellite-admin-ui:
|
|||||||
-e HOME=/tmp \
|
-e HOME=/tmp \
|
||||||
-u $(shell id -u):$(shell id -g) \
|
-u $(shell id -u):$(shell id -g) \
|
||||||
node:16.11.1 \
|
node:16.11.1 \
|
||||||
/bin/bash -c "npm ci && npm run build"
|
/bin/bash -c "npm ci && npm run build && cp build/* assets"
|
||||||
|
|
||||||
.PHONY: satellite-wasm
|
.PHONY: satellite-wasm
|
||||||
satellite-wasm:
|
satellite-wasm:
|
||||||
|
@ -26,7 +26,7 @@ import (
|
|||||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed ui/public
|
//go:embed ui/assets/*
|
||||||
var ui embed.FS
|
var ui embed.FS
|
||||||
|
|
||||||
// Config defines configuration for debug server.
|
// Config defines configuration for debug server.
|
||||||
@ -109,7 +109,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
|
|||||||
// This handler must be the last one because it uses the root as prefix,
|
// This handler must be the last one because it uses the root as prefix,
|
||||||
// otherwise will try to serve all the handlers set after this one.
|
// otherwise will try to serve all the handlers set after this one.
|
||||||
if config.StaticDir == "" {
|
if config.StaticDir == "" {
|
||||||
uiAssets, err := fs.Sub(ui, "ui/public")
|
uiAssets, err := fs.Sub(ui, "ui/assets")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("invalid embbeded static assets directory, the Admin UI is not enabled")
|
log.Error("invalid embbeded static assets directory, the Admin UI is not enabled")
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,36 +32,18 @@ func TestBasic(t *testing.T) {
|
|||||||
baseURL := "http://" + address.String()
|
baseURL := "http://" + address.String()
|
||||||
|
|
||||||
t.Run("UI", func(t *testing.T) {
|
t.Run("UI", func(t *testing.T) {
|
||||||
t.Run("index.html", func(t *testing.T) {
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/.gitignore", nil)
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(req)
|
response, err := http.DefaultClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||||
|
|
||||||
content, err := ioutil.ReadAll(response.Body)
|
content, err := ioutil.ReadAll(response.Body)
|
||||||
require.NoError(t, response.Body.Close())
|
require.NoError(t, response.Body.Close())
|
||||||
require.NotEmpty(t, content)
|
require.NotEmpty(t, content)
|
||||||
require.Contains(t, string(content), "</html>")
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("css", func(t *testing.T) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/global.css", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(req)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
|
||||||
|
|
||||||
content, err := ioutil.ReadAll(response.Body)
|
|
||||||
require.NoError(t, response.Body.Close())
|
|
||||||
require.NotEmpty(t, content)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NoAccess", func(t *testing.T) {
|
t.Run("NoAccess", func(t *testing.T) {
|
||||||
|
20
satellite/admin/ui/.eslintrc.cjs
Normal file
20
satellite/admin/ui/.eslintrc.cjs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||||
|
plugins: ['svelte3', '@typescript-eslint'],
|
||||||
|
ignorePatterns: ['*.cjs'],
|
||||||
|
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||||
|
settings: {
|
||||||
|
'svelte3/typescript': () => require('typescript')
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2019
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
}
|
||||||
|
};
|
11
satellite/admin/ui/.gitignore
vendored
11
satellite/admin/ui/.gitignore
vendored
@ -1,4 +1,9 @@
|
|||||||
/node_modules/
|
|
||||||
/public/build/
|
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/assets/*
|
||||||
|
!/assets/.keep
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
6
satellite/admin/ui/.prettierrc
Normal file
6
satellite/admin/ui/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
3
satellite/admin/ui/.vscode/extensions.json
vendored
3
satellite/admin/ui/.vscode/extensions.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["svelte.svelte-vscode"]
|
|
||||||
}
|
|
@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
## Implementation details
|
## Implementation details
|
||||||
|
|
||||||
This is a project based on the [Svelte](https://svelte.dev) [template for apps](https://github.com/sveltejs/template).
|
This is a project based on the [Sveltekit](https://kit.svelte.dev).
|
||||||
|
|
||||||
The project templated was converted to Typescript following the instructions on its README.
|
The project is set up with Typescript.
|
||||||
|
|
||||||
The Web App is currently straightforward as we specified that v1 would be.
|
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.).
|
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.
|
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 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.
|
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.
|
||||||
|
|
||||||
@ -25,20 +24,17 @@ Install the dependencies...
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
...then start [Rollup](https://rollupjs.org):
|
...then run the development server with autoreload on changes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Navigate to [localhost:5000](http://localhost:5000). You should see your app running.
|
Navigate to [localhost:3000](http://localhost:3000). 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
|
## Building for production mode
|
||||||
|
|
||||||
To create an optimised version of the app:
|
To create an optimized version of the app:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
|
2
satellite/admin/ui/assets/.gitignore
vendored
Normal file
2
satellite/admin/ui/assets/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
6980
satellite/admin/ui/package-lock.json
generated
6980
satellite/admin/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,33 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "svelte-app",
|
"name": "kit",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"scripts": {
|
||||||
"scripts": {
|
"dev": "svelte-kit dev",
|
||||||
"build": "rollup -c",
|
"build": "svelte-kit build",
|
||||||
"dev": "rollup -c -w",
|
"package": "svelte-kit package",
|
||||||
"start": "sirv public",
|
"preview": "svelte-kit preview",
|
||||||
"validate": "svelte-check"
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
},
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"devDependencies": {
|
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||||
"@rollup/plugin-commonjs": "^17.0.0",
|
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
},
|
||||||
"@rollup/plugin-typescript": "^8.0.0",
|
"devDependencies": {
|
||||||
"@tsconfig/svelte": "^1.0.0",
|
"@sveltejs/adapter-static": "^1.0.0-next.21",
|
||||||
"prettier": "^2.2.1",
|
"@sveltejs/kit": "next",
|
||||||
"prettier-plugin-svelte": "^2.2.0",
|
"@typescript-eslint/eslint-plugin": "^4.31.1",
|
||||||
"rollup": "^2.3.4",
|
"@typescript-eslint/parser": "^4.31.1",
|
||||||
"rollup-plugin-css-only": "^3.1.0",
|
"eslint": "^7.32.0",
|
||||||
"rollup-plugin-livereload": "^2.0.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"rollup-plugin-svelte": "^7.0.0",
|
"eslint-plugin-svelte3": "^3.2.1",
|
||||||
"rollup-plugin-terser": "^7.0.0",
|
"prettier": "^2.4.1",
|
||||||
"svelte": "^3.0.0",
|
"prettier-plugin-svelte": "^2.4.0",
|
||||||
"svelte-check": "^1.0.0",
|
"svelte": "^3.44.0",
|
||||||
"svelte-preprocess": "^4.0.0",
|
"svelte-check": "^2.2.6",
|
||||||
"tslib": "^2.0.0",
|
"svelte-preprocess": "^4.9.4",
|
||||||
"typescript": "^4.0.0"
|
"tslib": "^2.3.1",
|
||||||
},
|
"typescript": "^4.4.3"
|
||||||
"dependencies": {
|
},
|
||||||
"pretty-print-json": "^1.1.0",
|
"type": "module",
|
||||||
"sirv-cli": "^1.0.0"
|
"dependencies": {
|
||||||
}
|
"pretty-print-json": "^1.1.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,91 +0,0 @@
|
|||||||
// 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",
|
|
||||||
},
|
|
||||||
preserveSymlinks: true,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,87 +0,0 @@
|
|||||||
<!--
|
|
||||||
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}
|
|
@ -1,50 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -1,107 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -1,75 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -1,32 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -1,28 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
@ -1,391 +0,0 @@
|
|||||||
// 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, Select } 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>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
bucket: [
|
|
||||||
{
|
|
||||||
name: "get",
|
|
||||||
desc: "Get the information of the specified bucket",
|
|
||||||
params: [
|
|
||||||
["Project ID", new InputText("text", true)],
|
|
||||||
["Bucket name", new InputText("text", true)],
|
|
||||||
],
|
|
||||||
func: async (
|
|
||||||
projectId: string,
|
|
||||||
bucketName: string
|
|
||||||
): Promise<object> => {
|
|
||||||
return this.fetch(
|
|
||||||
"GET",
|
|
||||||
`projects/${projectId}/buckets/${bucketName}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "delete geofencing",
|
|
||||||
desc: "Delete the geofencing configuration of the specified bucket. The bucket MUST be empty",
|
|
||||||
params: [
|
|
||||||
["Project ID", new InputText("text", true)],
|
|
||||||
["Bucket name", new InputText("text", true)],
|
|
||||||
],
|
|
||||||
func: async (
|
|
||||||
projectId: string,
|
|
||||||
bucketName: string
|
|
||||||
): Promise<object> => {
|
|
||||||
return this.fetch(
|
|
||||||
"DELETE",
|
|
||||||
`projects/${projectId}/buckets/${bucketName}/geofence`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "set geofencing",
|
|
||||||
desc: "Set the geofencing configuration of the specified bucket. The bucket MUST be empty",
|
|
||||||
params: [
|
|
||||||
["Project ID", new InputText("text", true)],
|
|
||||||
["Bucket name", new InputText("text", true)],
|
|
||||||
[
|
|
||||||
"Region",
|
|
||||||
new Select(false, true, [
|
|
||||||
{ text: "European Union", value: "EU" },
|
|
||||||
{ text: "European Economic Area", value: "EEA" },
|
|
||||||
{ text: "United States", value: "US" },
|
|
||||||
{ text: "Germany", value: "DE" },
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
func: async (
|
|
||||||
projectId: string,
|
|
||||||
bucketName: string,
|
|
||||||
region: string
|
|
||||||
): Promise<object> => {
|
|
||||||
const query = this.urlQueryFromObject({ region });
|
|
||||||
if (query === "") {
|
|
||||||
throw new APIError("region cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.fetch(
|
|
||||||
"POST",
|
|
||||||
`projects/${projectId}/buckets/${bucketName}/geofence`,
|
|
||||||
query
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
15
satellite/admin/ui/src/app.html
Normal file
15
satellite/admin/ui/src/app.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="/global.css" />
|
||||||
|
<link rel="stylesheet" href="/pretty-print-json.css" />
|
||||||
|
%svelte.head%
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="svelte">%svelte.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
89
satellite/admin/ui/src/lib/Api.svelte
Normal file
89
satellite/admin/ui/src/lib/Api.svelte
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!--
|
||||||
|
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 '$lib/ui-generator';
|
||||||
|
import { Admin } from '$lib/api';
|
||||||
|
|
||||||
|
import UIGen from '$lib/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}
|
||||||
|
{#key selectedOp}
|
||||||
|
<UIGen operation={selectedOp} />
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
104
satellite/admin/ui/src/lib/UIGenerator.svelte
Normal file
104
satellite/admin/ui/src/lib/UIGenerator.svelte
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<!--
|
||||||
|
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 '$lib/ui-generator';
|
||||||
|
|
||||||
|
import UIInputText from '$lib/UIGeneratorInputText.svelte';
|
||||||
|
import UISelect from '$lib/UIGeneratorSelect.svelte';
|
||||||
|
import UITextarea from '$lib/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: opArg[] = new Array(operation.params.length);
|
||||||
|
let result: Promise<Record<string, unknown> | 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>
|
72
satellite/admin/ui/src/lib/UIGeneratorInputText.svelte
Normal file
72
satellite/admin/ui/src/lib/UIGeneratorInputText.svelte
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<!--
|
||||||
|
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 '$lib/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>
|
||||||
|
|
||||||
|
<!-- the empty 'for' avoids Svelte check warnings -->
|
||||||
|
<label for="">
|
||||||
|
{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>
|
33
satellite/admin/ui/src/lib/UIGeneratorSelect.svelte
Normal file
33
satellite/admin/ui/src/lib/UIGeneratorSelect.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!--
|
||||||
|
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 '$lib/ui-generator';
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let config: Select;
|
||||||
|
export let value: boolean | number | string = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- the empty 'for' avoids Svelte check warnings -->
|
||||||
|
<label for="">
|
||||||
|
{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>
|
28
satellite/admin/ui/src/lib/UIGeneratorTextarea.svelte
Normal file
28
satellite/admin/ui/src/lib/UIGeneratorTextarea.svelte
Normal 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 '$lib/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>
|
361
satellite/admin/ui/src/lib/api.ts
Normal file
361
satellite/admin/ui/src/lib/api.ts
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
// 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 '$lib/ui-generator';
|
||||||
|
import { InputText, Select } from '$lib/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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
bucket: [
|
||||||
|
{
|
||||||
|
name: 'get',
|
||||||
|
desc: 'Get the information of the specified bucket',
|
||||||
|
params: [
|
||||||
|
['Project ID', new InputText('text', true)],
|
||||||
|
['Bucket name', new InputText('text', true)]
|
||||||
|
],
|
||||||
|
func: async (projectId: string, bucketName: string): Promise<Record<string, unknown>> => {
|
||||||
|
return this.fetch('GET', `projects/${projectId}/buckets/${bucketName}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete geofencing',
|
||||||
|
desc: 'Delete the geofencing configuration of the specified bucket. The bucket MUST be empty',
|
||||||
|
params: [
|
||||||
|
['Project ID', new InputText('text', true)],
|
||||||
|
['Bucket name', new InputText('text', true)]
|
||||||
|
],
|
||||||
|
func: async (projectId: string, bucketName: string): Promise<Record<string, unknown>> => {
|
||||||
|
return this.fetch('DELETE', `projects/${projectId}/buckets/${bucketName}/geofence`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'set geofencing',
|
||||||
|
desc: 'Set the geofencing configuration of the specified bucket. The bucket MUST be empty',
|
||||||
|
params: [
|
||||||
|
['Project ID', new InputText('text', true)],
|
||||||
|
['Bucket name', new InputText('text', true)],
|
||||||
|
[
|
||||||
|
'Region',
|
||||||
|
new Select(false, true, [
|
||||||
|
{ text: 'European Union', value: 'EU' },
|
||||||
|
{ text: 'European Economic Area', value: 'EEA' },
|
||||||
|
{ text: 'United States', value: 'US' },
|
||||||
|
{ text: 'Germany', value: 'DE' }
|
||||||
|
])
|
||||||
|
]
|
||||||
|
],
|
||||||
|
func: async (
|
||||||
|
projectId: string,
|
||||||
|
bucketName: string,
|
||||||
|
region: string
|
||||||
|
): Promise<Record<string, unknown>> => {
|
||||||
|
const query = this.urlQueryFromObject({ region });
|
||||||
|
if (query === '') {
|
||||||
|
throw new APIError('region cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fetch('POST', `projects/${projectId}/buckets/${bucketName}/geofence`, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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<Record<string, unknown>> => {
|
||||||
|
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?: Record<string, unknown>
|
||||||
|
): Promise<Record<string, unknown> | 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: Record<string, unknown>;
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected urlQueryFromObject(values: Record<string, boolean | number | string>): 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?: Record<string, unknown> | string
|
||||||
|
) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
54
satellite/admin/ui/src/lib/ui-generator.ts
Normal file
54
satellite/admin/ui/src/lib/ui-generator.ts
Normal 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: unknown[]) => Promise<Record<string, unknown> | 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) {}
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
// 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;
|
|
54
satellite/admin/ui/src/routes/index.svelte
Normal file
54
satellite/admin/ui/src/routes/index.svelte
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!--
|
||||||
|
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 '$lib/Api.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Storj Satellite Admin Console</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<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>
|
@ -1,54 +0,0 @@
|
|||||||
// 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) {}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
67
satellite/admin/ui/static/global.css
Normal file
67
satellite/admin/ui/static/global.css
Normal 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;
|
||||||
|
}
|
@ -1,62 +1,62 @@
|
|||||||
/*! pretty-print-json v1.0.3 ~ github.com/center-key/pretty-print-json ~ MIT License */
|
/*! pretty-print-json v1.0.3 ~ github.com/center-key/pretty-print-json ~ MIT License */
|
||||||
.json-key {
|
.json-key {
|
||||||
color: brown;
|
color: brown;
|
||||||
}
|
}
|
||||||
.json-string {
|
.json-string {
|
||||||
color: olive;
|
color: olive;
|
||||||
}
|
}
|
||||||
.json-number {
|
.json-number {
|
||||||
color: navy;
|
color: navy;
|
||||||
}
|
}
|
||||||
.json-boolean {
|
.json-boolean {
|
||||||
color: teal;
|
color: teal;
|
||||||
}
|
}
|
||||||
.json-null {
|
.json-null {
|
||||||
color: dimgray;
|
color: dimgray;
|
||||||
}
|
}
|
||||||
.json-mark {
|
.json-mark {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
a.json-link {
|
a.json-link {
|
||||||
color: purple;
|
color: purple;
|
||||||
transition: all 400ms;
|
transition: all 400ms;
|
||||||
}
|
}
|
||||||
a.json-link:visited {
|
a.json-link:visited {
|
||||||
color: slategray;
|
color: slategray;
|
||||||
}
|
}
|
||||||
a.json-link:hover {
|
a.json-link:hover {
|
||||||
color: blueviolet;
|
color: blueviolet;
|
||||||
}
|
}
|
||||||
a.json-link:active {
|
a.json-link:active {
|
||||||
color: slategray;
|
color: slategray;
|
||||||
}
|
}
|
||||||
.dark-mode .json-key {
|
.dark-mode .json-key {
|
||||||
color: indianred;
|
color: indianred;
|
||||||
}
|
}
|
||||||
.dark-mode .json-string {
|
.dark-mode .json-string {
|
||||||
color: darkkhaki;
|
color: darkkhaki;
|
||||||
}
|
}
|
||||||
.dark-mode .json-number {
|
.dark-mode .json-number {
|
||||||
color: deepskyblue;
|
color: deepskyblue;
|
||||||
}
|
}
|
||||||
.dark-mode .json-boolean {
|
.dark-mode .json-boolean {
|
||||||
color: mediumseagreen;
|
color: mediumseagreen;
|
||||||
}
|
}
|
||||||
.dark-mode .json-null {
|
.dark-mode .json-null {
|
||||||
color: darkorange;
|
color: darkorange;
|
||||||
}
|
}
|
||||||
.dark-mode .json-mark {
|
.dark-mode .json-mark {
|
||||||
color: silver;
|
color: silver;
|
||||||
}
|
}
|
||||||
.dark-mode a.json-link {
|
.dark-mode a.json-link {
|
||||||
color: mediumorchid;
|
color: mediumorchid;
|
||||||
}
|
}
|
||||||
.dark-mode a.json-link:visited {
|
.dark-mode a.json-link:visited {
|
||||||
color: slategray;
|
color: slategray;
|
||||||
}
|
}
|
||||||
.dark-mode a.json-link:hover {
|
.dark-mode a.json-link:hover {
|
||||||
color: violet;
|
color: violet;
|
||||||
}
|
}
|
||||||
.dark-mode a.json-link:active {
|
.dark-mode a.json-link:active {
|
||||||
color: silver;
|
color: silver;
|
||||||
}
|
}
|
27
satellite/admin/ui/svelte.config.js
Normal file
27
satellite/admin/ui/svelte.config.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (C) 2021 Storj Labs, Inc.
|
||||||
|
|
||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import preprocess from 'svelte-preprocess';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: preprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
adapter: adapter(),
|
||||||
|
|
||||||
|
// hydrate the <div id="svelte"> element in src/app.html
|
||||||
|
target: '#svelte',
|
||||||
|
|
||||||
|
ssr: false,
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
preserveSymlinks: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
@ -1,6 +1,31 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
"include": ["src/**/*"],
|
"module": "es2020",
|
||||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
"lib": ["es2020", "DOM"],
|
||||||
|
"target": "es2020",
|
||||||
|
/**
|
||||||
|
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
||||||
|
to enforce using \`import type\` instead of \`import\` for Types.
|
||||||
|
*/
|
||||||
|
"importsNotUsedAsValues": "error",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
/**
|
||||||
|
To have warnings/errors of the Svelte compiler at the correct position,
|
||||||
|
enable source maps by default.
|
||||||
|
*/
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"paths": {
|
||||||
|
"$lib": ["src/lib"],
|
||||||
|
"$lib/*": ["src/lib/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user