From 8cd8f525666d0ef43fee35a6a51431520d35dd72 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 4 Nov 2018 22:55:58 +0200 Subject: [PATCH 01/23] Base frontend stuff and login view --- maubot/management/frontend/package.json | 1 + .../src/{MaubotManager.js => Home.js} | 15 +-- maubot/management/frontend/src/Login.js | 47 +++++++++ .../management/frontend/src/PrivateRoute.js | 16 ++++ maubot/management/frontend/src/Router.js | 41 ++++++++ maubot/management/frontend/src/Spinner.js | 9 ++ maubot/management/frontend/src/api.js | 86 +++++++++++++++++ maubot/management/frontend/src/index.js | 4 +- .../frontend/src/style/base/body.sass | 7 ++ .../frontend/src/style/base/elements.sass | 95 +++++++++++++++++++ .../management/frontend/src/style/index.sass | 5 + .../frontend/src/style/lib/spinner.sass | 60 ++++++++++++ .../frontend/src/style/pages/login.sass | 44 +++++++++ maubot/management/frontend/yarn.lock | 78 ++++++++++++++- 14 files changed, 492 insertions(+), 16 deletions(-) rename maubot/management/frontend/src/{MaubotManager.js => Home.js} (75%) create mode 100644 maubot/management/frontend/src/Login.js create mode 100644 maubot/management/frontend/src/PrivateRoute.js create mode 100644 maubot/management/frontend/src/Router.js create mode 100644 maubot/management/frontend/src/Spinner.js create mode 100644 maubot/management/frontend/src/api.js create mode 100644 maubot/management/frontend/src/style/base/elements.sass create mode 100644 maubot/management/frontend/src/style/lib/spinner.sass create mode 100644 maubot/management/frontend/src/style/pages/login.sass diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index c5cf653..9ecf46a 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -6,6 +6,7 @@ "node-sass": "^4.9.4", "react": "^16.6.0", "react-dom": "^16.6.0", + "react-router-dom": "^4.3.1", "react-scripts": "2.0.5" }, "scripts": { diff --git a/maubot/management/frontend/src/MaubotManager.js b/maubot/management/frontend/src/Home.js similarity index 75% rename from maubot/management/frontend/src/MaubotManager.js rename to maubot/management/frontend/src/Home.js index 314f47a..53a8466 100644 --- a/maubot/management/frontend/src/MaubotManager.js +++ b/maubot/management/frontend/src/Home.js @@ -15,19 +15,12 @@ // along with this program. If not, see . import React, { Component } from "react" -class MaubotManager extends Component { +class Home extends Component { render() { - return ( -
-
+ return
-
-
- -
-
- ) + } } -export default MaubotManager +export default Home diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/Login.js new file mode 100644 index 0000000..6788168 --- /dev/null +++ b/maubot/management/frontend/src/Login.js @@ -0,0 +1,47 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class Login extends Component { + constructor(props, context) { + super(props, context) + this.state = { + username: "", + password: "", + } + } + + inputChanged = event => this.setState({ [event.target.name]: event.target.value }) + + login = () => { + + } + + render() { + return
+
+

Maubot Manager

+ + + +
+
+ } +} + +export default Login diff --git a/maubot/management/frontend/src/PrivateRoute.js b/maubot/management/frontend/src/PrivateRoute.js new file mode 100644 index 0000000..2180e96 --- /dev/null +++ b/maubot/management/frontend/src/PrivateRoute.js @@ -0,0 +1,16 @@ +import React, { Component } from "react" +import { Route, Redirect } from "react-router-dom" + +const PrivateRoute = ({ component, authed, ...rest }) => ( + authed === true + ? + : } + /> +) + +export default PrivateRoute diff --git a/maubot/management/frontend/src/Router.js b/maubot/management/frontend/src/Router.js new file mode 100644 index 0000000..3eb4faf --- /dev/null +++ b/maubot/management/frontend/src/Router.js @@ -0,0 +1,41 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { BrowserRouter as Router, Route, Redirect } from "react-router-dom" +import PrivateRoute from "./PrivateRoute" +import Home from "./Home" +import Login from "./Login" + +class MaubotRouter extends Component { + constructor(props) { + super(props) + this.state = { + authed: localStorage.accessToken !== undefined, + } + } + + render() { + return +
+ }/> + + +
+
+ } +} + +export default MaubotRouter diff --git a/maubot/management/frontend/src/Spinner.js b/maubot/management/frontend/src/Spinner.js new file mode 100644 index 0000000..4f227d4 --- /dev/null +++ b/maubot/management/frontend/src/Spinner.js @@ -0,0 +1,9 @@ +import React from "react" + +const Spinner = () => ( +
+ + + +
+) diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js new file mode 100644 index 0000000..712eb0d --- /dev/null +++ b/maubot/management/frontend/src/api.js @@ -0,0 +1,86 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const BASE_PATH = "/_matrix/maubot/v1" + +export function login(username, password) { + return fetch(`${BASE_PATH}/auth/login`, { + method: "POST", + body: JSON.stringify({ + username, + password, + }), + }) +} + +function getHeaders(contentType = "application/json") { + return { + "Content-Type": contentType, + "Authorization": `Bearer ${localStorage.accessToken}`, + } +} + +export async function ping() { + const response = await fetch(`${BASE_PATH}/auth/ping`, { + method: "POST", + headers: getHeaders(), + }) + const json = await response.json() + if (json.username) { + return json.username + } else if (json.errcode === "auth_token_missing" || json.errcode === "auth_token_invalid") { + return null + } + throw json +} + +export async function getInstances() { + const resp = await fetch(`${BASE_PATH}/instances`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getInstance(id) { + const resp = await fetch(`${BASE_PATH}/instance/${id}`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getPlugins() { + const resp = await fetch(`${BASE_PATH}/plugins`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getPlugin(id) { + const resp = await fetch(`${BASE_PATH}/plugin/${id}`, { headers: getHeaders() }) + return await resp.json() +} + +export async function uploadPlugin(data) { + const resp = await fetch(`${BASE_PATH}/plugins/upload`, { + headers: getHeaders("application/zip"), + body: data, + }) + return await resp.json() +} + +export async function getClients() { + const resp = await fetch(`${BASE_PATH}/clients`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getClient(id) { + const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() }) + return await resp.json() +} diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js index 8f50972..9e9203f 100644 --- a/maubot/management/frontend/src/index.js +++ b/maubot/management/frontend/src/index.js @@ -16,6 +16,6 @@ import React from "react" import ReactDOM from "react-dom" import "./style/index.sass" -import MaubotManager from "./MaubotManager" +import App from "./Router" -ReactDOM.render(, document.getElementById("root")) +ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index b45c068..01d90e9 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -27,6 +27,13 @@ body right: 0 left: 0 +.maubot-wrapper + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + //.lindeb > header position: absolute diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass new file mode 100644 index 0000000..10a183e --- /dev/null +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -0,0 +1,95 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +=button() + font-family: $font-stack + padding: .375rem 1rem + background-color: $background-color + border: none + border-radius: .25rem + color: $inverted-text-color + box-sizing: border-box + font-size: 1rem + cursor: pointer + + &:hover + background-color: darken($background-color, 10%) + +=link-button() + display: inline-block + text-align: center + text-decoration: none + +=main-color-button() + background-color: $main-color + &:hover + background-color: $dark-color + +button, .button + +button + + &.main-color + +main-color-button + +=button-group() + width: 100% + display: flex + > button, > .button + flex: 1 + border-radius: 0 + + &:first-of-type + border-radius: .25rem 0 0 .25rem + + &:last-of-type + border-radius: 0 .25rem .25rem 0 + + &:first-of-type:last-of-type + border-radius: .25rem + +=vertical-button-group() + display: flex + flex-direction: column + > button, > .button + flex: 1 + border-radius: 0 + + &:first-of-type + border-radius: .25rem .25rem 0 0 + + &:last-of-type + border-radius: 0 0 .25rem .25rem + + &:first-of-type:last-of-type + border-radius: .25rem + +input, textarea + font-family: $font-stack + border: 1px solid $border-color + background-color: $background-color + color: $text-color + box-sizing: border-box + border-radius: .25rem + padding: .375rem 1rem + font-size: 1rem + resize: vertical + + &:hover, &:focus + border-color: $main-color + + &:focus + border-width: 2px + padding: calc(.375rem - 1px) 1rem diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index d3d2eaa..20d193d 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -15,3 +15,8 @@ // along with this program. If not, see . @import base/vars @import base/body +@import base/elements + +@import lib/spinner + +@import pages/login diff --git a/maubot/management/frontend/src/style/lib/spinner.sass b/maubot/management/frontend/src/style/lib/spinner.sass new file mode 100644 index 0000000..3fb4cbc --- /dev/null +++ b/maubot/management/frontend/src/style/lib/spinner.sass @@ -0,0 +1,60 @@ +$green: #008744 +$blue: #0057e7 +$red: #d62d20 +$yellow: #ffa700 +$white: #eee + +$width: 100px + +.loader + position: relative + margin: 0 auto + width: $width + + &:before + content: "" + display: block + padding-top: 100% + + svg + animation: rotate 2s linear infinite + height: 100% + transform-origin: center center + width: 100% + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + margin: auto + + circle + stroke-dasharray: 1, 200 + stroke-dashoffset: 0 + animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite + stroke-linecap: round + +@keyframes rotate + 100% + transform: rotate(360deg) + +@keyframes dash + 0% + stroke-dasharray: 1, 200 + stroke-dashoffset: 0 + 50% + stroke-dasharray: 89, 200 + stroke-dashoffset: -35px + 100% + stroke-dasharray: 89, 200 + stroke-dashoffset: -124px + +@keyframes color + 100%, 0% + stroke: $red + 40% + stroke: $blue + 66% + stroke: $green + 80%, 90% + stroke: $yellow diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass new file mode 100644 index 0000000..f3d04fa --- /dev/null +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -0,0 +1,44 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.maubot-wrapper:not(.authenticated) + background-color: $main-color + + text-align: center + +.login + width: 25rem + height: 23.5rem + display: inline-block + box-sizing: border-box + background-color: white + border-radius: .25rem + margin-top: 3rem + + .title + color: $main-color + margin: 3rem 0 + + input, button + width: calc(100% - 5rem) + margin: .5rem 2.5rem + padding: 1rem + + input:focus + padding: calc(1rem - 1px) + + button + +main-color-button diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock index 64d5190..9b58f83 100644 --- a/maubot/management/frontend/yarn.lock +++ b/maubot/management/frontend/yarn.lock @@ -4316,6 +4316,17 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +history@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" + integrity sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA== + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.2.0" + value-equal "^0.4.0" + warning "^3.0.0" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4330,6 +4341,11 @@ hoek@4.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== +hoist-non-react-statics@^2.5.0: + version "2.5.5" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -4643,7 +4659,7 @@ internal-ip@^3.0.1: default-gateway "^2.6.0" ipaddr.js "^1.5.2" -invariant@^2.2.0, invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -5851,7 +5867,7 @@ loglevel@^1.4.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6895,6 +6911,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30= + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -7698,7 +7721,7 @@ prompts@^0.1.9: kleur "^2.0.1" sisteransi "^0.1.1" -prop-types@^15.6.2: +prop-types@^15.6.1, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== @@ -7916,6 +7939,31 @@ react-error-overlay@^5.0.5: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.0.5.tgz#716ff1a92fda76bb89a39adf9ce046a5d740e71a" integrity sha512-ab0HWBgxdIsngHtMGU8+8gYFdTBXpUGd4AE4lN2VZvOIlBmWx9dtaWEViihuGSIGosCKPeHCnzFoRWB42UtnLg== +react-router-dom@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" + integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== + dependencies: + history "^4.7.2" + invariant "^2.2.4" + loose-envify "^1.3.1" + prop-types "^15.6.1" + react-router "^4.3.1" + warning "^4.0.1" + +react-router@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" + integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== + dependencies: + history "^4.7.2" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.4" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.6.1" + warning "^4.0.1" + react-scripts@2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-2.0.5.tgz#74b8e9fa6a7c5f0f11221dd18c10df2ae3df3d69" @@ -8303,6 +8351,11 @@ resolve-from@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" integrity sha1-six699nWiBvItuZTM17rywoYh0g= +resolve-pathname@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" + integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -9559,6 +9612,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +value-equal@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" + integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -9599,6 +9657,20 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= + dependencies: + loose-envify "^1.0.0" + +warning@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" + integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== + dependencies: + loose-envify "^1.0.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" From f3a0b7bc4fa3f1511ebc2ec89a8b88591c79307f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 6 Nov 2018 23:27:17 +0200 Subject: [PATCH 02/23] Implement login --- maubot/management/frontend/src/Home.js | 2 +- maubot/management/frontend/src/Login.js | 26 +++++++++++--- .../src/{Router.js => MaubotRouter.js} | 35 +++++++++++++++++-- .../management/frontend/src/PrivateRoute.js | 28 ++++++++++----- maubot/management/frontend/src/Spinner.js | 6 ++-- maubot/management/frontend/src/api.js | 12 +++++-- maubot/management/frontend/src/index.js | 2 +- .../frontend/src/style/base/body.sass | 4 +++ .../frontend/src/style/base/elements.sass | 27 ++++++++++---- .../management/frontend/src/style/index.sass | 4 +-- .../frontend/src/style/lib/spinner.sass | 15 +++++--- .../frontend/src/style/pages/login.sass | 25 +++++++++---- 12 files changed, 145 insertions(+), 41 deletions(-) rename maubot/management/frontend/src/{Router.js => MaubotRouter.js} (61%) diff --git a/maubot/management/frontend/src/Home.js b/maubot/management/frontend/src/Home.js index 53a8466..464294d 100644 --- a/maubot/management/frontend/src/Home.js +++ b/maubot/management/frontend/src/Home.js @@ -18,7 +18,7 @@ import React, { Component } from "react" class Home extends Component { render() { return
- + Hello, {localStorage.username}
} } diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/Login.js index 6788168..af95d07 100644 --- a/maubot/management/frontend/src/Login.js +++ b/maubot/management/frontend/src/Login.js @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" +import Spinner from "./Spinner" +import api from "./api" class Login extends Component { constructor(props, context) { @@ -21,24 +23,38 @@ class Login extends Component { this.state = { username: "", password: "", + loading: false, + error: "", } } inputChanged = event => this.setState({ [event.target.name]: event.target.value }) - login = () => { - + login = async () => { + this.setState({ loading: true }) + const resp = await api.login(this.state.username, this.state.password) + if (resp.token) { + await this.props.onLogin(resp.token) + } else if (resp.error) { + this.setState({ error: resp.error, loading: false }) + } else { + this.setState({ error: "Unknown error", loading: false }) + console.log("Unknown error:", resp) + } } render() { return
-
-

Maubot Manager

+
+

Maubot Manager

- + + {this.state.error &&
{this.state.error}
}
} diff --git a/maubot/management/frontend/src/Router.js b/maubot/management/frontend/src/MaubotRouter.js similarity index 61% rename from maubot/management/frontend/src/Router.js rename to maubot/management/frontend/src/MaubotRouter.js index 3eb4faf..891f613 100644 --- a/maubot/management/frontend/src/Router.js +++ b/maubot/management/frontend/src/MaubotRouter.js @@ -18,21 +18,52 @@ import { BrowserRouter as Router, Route, Redirect } from "react-router-dom" import PrivateRoute from "./PrivateRoute" import Home from "./Home" import Login from "./Login" +import Spinner from "./Spinner" +import api from "./api" class MaubotRouter extends Component { constructor(props) { super(props) this.state = { - authed: localStorage.accessToken !== undefined, + pinged: false, + authed: false, } } + async componentWillMount() { + if (localStorage.accessToken) { + await this.ping() + } + this.setState({ pinged: true }) + } + + async ping() { + try { + const username = await api.ping() + if (username) { + localStorage.username = username + this.setState({ authed: true }) + } + } catch (err) { + console.error(err) + } + } + + login = async (token) => { + localStorage.accessToken = token + await this.ping() + } + render() { + if (!this.state.pinged) { + return + } return
}/> - + } + authed={!this.state.authed} to="/dashboard"/>
} diff --git a/maubot/management/frontend/src/PrivateRoute.js b/maubot/management/frontend/src/PrivateRoute.js index 2180e96..745078d 100644 --- a/maubot/management/frontend/src/PrivateRoute.js +++ b/maubot/management/frontend/src/PrivateRoute.js @@ -1,15 +1,27 @@ -import React, { Component } from "react" +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" import { Route, Redirect } from "react-router-dom" -const PrivateRoute = ({ component, authed, ...rest }) => ( +const PrivateRoute = ({ component, render, authed, to = "/login", ...args }) => ( authed === true - ? - : } + ? (component ? React.createElement(component, props) : render()) + : } /> ) diff --git a/maubot/management/frontend/src/Spinner.js b/maubot/management/frontend/src/Spinner.js index 4f227d4..77735f5 100644 --- a/maubot/management/frontend/src/Spinner.js +++ b/maubot/management/frontend/src/Spinner.js @@ -1,9 +1,11 @@ import React from "react" -const Spinner = () => ( -
+const Spinner = (props) => ( +
) + +export default Spinner diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 712eb0d..7e1ef7f 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -16,14 +16,15 @@ const BASE_PATH = "/_matrix/maubot/v1" -export function login(username, password) { - return fetch(`${BASE_PATH}/auth/login`, { +export async function login(username, password) { + const resp = await fetch(`${BASE_PATH}/auth/login`, { method: "POST", body: JSON.stringify({ username, password, }), }) + return await resp.json() } function getHeaders(contentType = "application/json") { @@ -84,3 +85,10 @@ export async function getClient(id) { const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() }) return await resp.json() } + +export default { + login, ping, + getInstances, getInstance, + getPlugins, getPlugin, uploadPlugin, + getClients, getClient, +} diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js index 9e9203f..cbe10ed 100644 --- a/maubot/management/frontend/src/index.js +++ b/maubot/management/frontend/src/index.js @@ -16,6 +16,6 @@ import React from "react" import ReactDOM from "react-dom" import "./style/index.sass" -import App from "./Router" +import App from "./MaubotRouter" ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index 01d90e9..462fe8c 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -34,6 +34,10 @@ body left: 0 right: 0 +.maubot-loading + margin-top: 10rem + width: 10rem + //.lindeb > header position: absolute diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 10a183e..48fb045 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -14,9 +14,11 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -=button() +=button($width: null, $height: null, $padding: .375rem 1rem) font-family: $font-stack - padding: .375rem 1rem + padding: $padding + width: $width + height: $height background-color: $background-color border: none border-radius: .25rem @@ -38,7 +40,7 @@ &:hover background-color: $dark-color -button, .button +.button +button &.main-color @@ -76,15 +78,17 @@ button, .button &:first-of-type:last-of-type border-radius: .25rem -input, textarea +=input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem) font-family: $font-stack border: 1px solid $border-color background-color: $background-color color: $text-color + width: $width + height: $height box-sizing: border-box border-radius: .25rem - padding: .375rem 1rem - font-size: 1rem + padding: $vertical-padding $horizontal-padding + font-size: $font-size resize: vertical &:hover, &:focus @@ -92,4 +96,13 @@ input, textarea &:focus border-width: 2px - padding: calc(.375rem - 1px) 1rem + padding: calc(#{$vertical-padding} - 1px) calc(#{$horizontal-padding} - 1px) + +.input, .textarea + +input + +=notification($color: $error-color) + padding: 1rem + border-radius: .25rem + border: 2px solid $color + background-color: lighten($color, 25%) diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index 20d193d..8486157 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -13,10 +13,10 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +@import lib/spinner + @import base/vars @import base/body @import base/elements -@import lib/spinner - @import pages/login diff --git a/maubot/management/frontend/src/style/lib/spinner.sass b/maubot/management/frontend/src/style/lib/spinner.sass index 3fb4cbc..d164586 100644 --- a/maubot/management/frontend/src/style/lib/spinner.sass +++ b/maubot/management/frontend/src/style/lib/spinner.sass @@ -2,14 +2,11 @@ $green: #008744 $blue: #0057e7 $red: #d62d20 $yellow: #ffa700 -$white: #eee -$width: 100px - -.loader +.spinner position: relative margin: 0 auto - width: $width + width: 5rem &:before content: "" @@ -34,6 +31,14 @@ $width: 100px animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite stroke-linecap: round +=white-spinner() + circle + stroke: white !important + +=thick-spinner($thickness: 5) + svg > circle + stroke-width: $thickness + @keyframes rotate 100% transform: rotate(360deg) diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass index f3d04fa..62b0891 100644 --- a/maubot/management/frontend/src/style/pages/login.sass +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -21,24 +21,37 @@ .login width: 25rem - height: 23.5rem + height: 23rem display: inline-block box-sizing: border-box background-color: white border-radius: .25rem margin-top: 3rem - .title + h1 color: $main-color margin: 3rem 0 input, button - width: calc(100% - 5rem) margin: .5rem 2.5rem - padding: 1rem + height: 3rem + width: 20rem - input:focus - padding: calc(1rem - 1px) + input + +input button + +button($width: 20rem, $height: 3rem, $padding: 0) +main-color-button + + .spinner + +white-spinner + +thick-spinner + width: 2rem + + &.errored + height: 26.5rem + + .error + +notification($error-color) + margin: .5rem 2.5rem From 2ba1f46149b9ea2b10ba499ca929a48d8239e531 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 8 Nov 2018 01:02:46 +0200 Subject: [PATCH 03/23] Reorganize things, add sidebar and topbar --- maubot/management/frontend/src/Login.js | 2 +- .../management/frontend/src/MaubotRouter.js | 19 ++-- .../src/{ => components}/PrivateRoute.js | 0 .../frontend/src/{ => components}/Spinner.js | 0 .../src/dashboard/client/ListEntry.js | 30 +++++ .../src/{Home.js => dashboard/client/View.js} | 8 +- .../frontend/src/dashboard/index.js | 105 ++++++++++++++++++ .../src/dashboard/instance/ListEntry.js | 27 +++++ .../frontend/src/dashboard/instance/View.js | 24 ++++ .../src/dashboard/plugin/ListEntry.js | 27 +++++ .../frontend/src/dashboard/plugin/View.js | 24 ++++ .../frontend/src/res/chevron-right.svg | 1 + .../frontend/src/style/base/elements.sass | 12 +- .../frontend/src/style/base/vars.sass | 20 ++-- .../management/frontend/src/style/index.sass | 1 + .../src/style/pages/dashboard-grid.css | 6 + .../frontend/src/style/pages/dashboard.sass | 57 ++++++++++ .../frontend/src/style/pages/login.sass | 6 +- .../frontend/src/style/pages/sidebar.sass | 53 +++++++++ 19 files changed, 390 insertions(+), 32 deletions(-) rename maubot/management/frontend/src/{ => components}/PrivateRoute.js (100%) rename maubot/management/frontend/src/{ => components}/Spinner.js (100%) create mode 100644 maubot/management/frontend/src/dashboard/client/ListEntry.js rename maubot/management/frontend/src/{Home.js => dashboard/client/View.js} (86%) create mode 100644 maubot/management/frontend/src/dashboard/index.js create mode 100644 maubot/management/frontend/src/dashboard/instance/ListEntry.js create mode 100644 maubot/management/frontend/src/dashboard/instance/View.js create mode 100644 maubot/management/frontend/src/dashboard/plugin/ListEntry.js create mode 100644 maubot/management/frontend/src/dashboard/plugin/View.js create mode 100644 maubot/management/frontend/src/res/chevron-right.svg create mode 100644 maubot/management/frontend/src/style/pages/dashboard-grid.css create mode 100644 maubot/management/frontend/src/style/pages/dashboard.sass create mode 100644 maubot/management/frontend/src/style/pages/sidebar.sass diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/Login.js index af95d07..e342abe 100644 --- a/maubot/management/frontend/src/Login.js +++ b/maubot/management/frontend/src/Login.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import Spinner from "./Spinner" +import Spinner from "./components/Spinner" import api from "./api" class Login extends Component { diff --git a/maubot/management/frontend/src/MaubotRouter.js b/maubot/management/frontend/src/MaubotRouter.js index 891f613..b3c7561 100644 --- a/maubot/management/frontend/src/MaubotRouter.js +++ b/maubot/management/frontend/src/MaubotRouter.js @@ -14,11 +14,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { BrowserRouter as Router, Route, Redirect } from "react-router-dom" -import PrivateRoute from "./PrivateRoute" -import Home from "./Home" +import { BrowserRouter as Router, Switch } from "react-router-dom" +import PrivateRoute from "./components/PrivateRoute" +import Dashboard from "./dashboard" import Login from "./Login" -import Spinner from "./Spinner" +import Spinner from "./components/Spinner" import api from "./api" class MaubotRouter extends Component { @@ -43,6 +43,8 @@ class MaubotRouter extends Component { if (username) { localStorage.username = username this.setState({ authed: true }) + } else { + localStorage.accessToken = undefined } } catch (err) { console.error(err) @@ -60,10 +62,11 @@ class MaubotRouter extends Component { } return
- }/> - - } - authed={!this.state.authed} to="/dashboard"/> + + } + authed={!this.state.authed} to="/"/> + +
} diff --git a/maubot/management/frontend/src/PrivateRoute.js b/maubot/management/frontend/src/components/PrivateRoute.js similarity index 100% rename from maubot/management/frontend/src/PrivateRoute.js rename to maubot/management/frontend/src/components/PrivateRoute.js diff --git a/maubot/management/frontend/src/Spinner.js b/maubot/management/frontend/src/components/Spinner.js similarity index 100% rename from maubot/management/frontend/src/Spinner.js rename to maubot/management/frontend/src/components/Spinner.js diff --git a/maubot/management/frontend/src/dashboard/client/ListEntry.js b/maubot/management/frontend/src/dashboard/client/ListEntry.js new file mode 100644 index 0000000..e19055f --- /dev/null +++ b/maubot/management/frontend/src/dashboard/client/ListEntry.js @@ -0,0 +1,30 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { Link } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" + +const ClientListEntry = ({ client }) => ( + + {client.id.substr(1, + {client.displayname || client.id} + + +) + +export default ClientListEntry diff --git a/maubot/management/frontend/src/Home.js b/maubot/management/frontend/src/dashboard/client/View.js similarity index 86% rename from maubot/management/frontend/src/Home.js rename to maubot/management/frontend/src/dashboard/client/View.js index 464294d..5256549 100644 --- a/maubot/management/frontend/src/Home.js +++ b/maubot/management/frontend/src/dashboard/client/View.js @@ -15,12 +15,10 @@ // along with this program. If not, see . import React, { Component } from "react" -class Home extends Component { +class ClientView extends Component { render() { - return
- Hello, {localStorage.username} -
+ return
{this.props.client.displayname}
} } -export default Home +export default ClientView diff --git a/maubot/management/frontend/src/dashboard/index.js b/maubot/management/frontend/src/dashboard/index.js new file mode 100644 index 0000000..6e40a8f --- /dev/null +++ b/maubot/management/frontend/src/dashboard/index.js @@ -0,0 +1,105 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { Route, Redirect } from "react-router-dom" +import api from "../api" +import InstanceListEntry from "./instance/ListEntry" +import InstanceView from "./instance/View" +import ClientListEntry from "./client/ListEntry" +import ClientView from "./client/View" +import PluginListEntry from "./plugin/ListEntry" +import PluginView from "./plugin/View" + +class Dashboard extends Component { + constructor(props) { + super(props) + this.state = { + instances: {}, + clients: {}, + plugins: {}, + } + global.maubot = this + } + + async componentWillMount() { + const [instanceList, clientList, pluginList] = await Promise.all([ + api.getInstances(), api.getClients(), api.getPlugins()]) + const instances = {} + for (const instance of instanceList) { + instances[instance.id] = instance + } + const clients = {} + for (const client of clientList) { + clients[client.id] = client + } + const plugins = {} + for (const plugin of pluginList) { + plugins[plugin.id] = plugin + } + this.setState({ instances, clients, plugins }) + } + + renderList(field, type) { + return Object.values(this.state[field + "s"]).map(entry => + React.createElement(type, { key: entry.id, [field]: entry })) + } + + renderView(field, type, id) { + const entry = this.state[field + "s"][id] + if (!entry) { + return "Not found :(" + } + return React.createElement(type, { [field]: entry }) + } + + render() { + return
+
+ + Maubot Manager +
+
+ {localStorage.username} +
+ +
+ "Hello, World!"}/> + + this.renderView("instance", InstanceView, match.params.id)}/> + + this.renderView("client", ClientView, match.params.id)}/> + + this.renderView("plugin", PluginView, match.params.id)}/> + }/> +
+
+ } +} + +export default Dashboard diff --git a/maubot/management/frontend/src/dashboard/instance/ListEntry.js b/maubot/management/frontend/src/dashboard/instance/ListEntry.js new file mode 100644 index 0000000..0603e4d --- /dev/null +++ b/maubot/management/frontend/src/dashboard/instance/ListEntry.js @@ -0,0 +1,27 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { Link } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" + +const InstanceListEntry = ({ instance }) => ( + + {instance.id} + + +) + +export default InstanceListEntry diff --git a/maubot/management/frontend/src/dashboard/instance/View.js b/maubot/management/frontend/src/dashboard/instance/View.js new file mode 100644 index 0000000..69bdf9f --- /dev/null +++ b/maubot/management/frontend/src/dashboard/instance/View.js @@ -0,0 +1,24 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class InstanceView extends Component { + render() { + return
{this.props.instance.id}
+ } +} + +export default InstanceView diff --git a/maubot/management/frontend/src/dashboard/plugin/ListEntry.js b/maubot/management/frontend/src/dashboard/plugin/ListEntry.js new file mode 100644 index 0000000..6facdbf --- /dev/null +++ b/maubot/management/frontend/src/dashboard/plugin/ListEntry.js @@ -0,0 +1,27 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { Link } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" + +const PluginListEntry = ({ plugin }) => ( + + {plugin.id} + + +) + +export default PluginListEntry diff --git a/maubot/management/frontend/src/dashboard/plugin/View.js b/maubot/management/frontend/src/dashboard/plugin/View.js new file mode 100644 index 0000000..5b8ccbc --- /dev/null +++ b/maubot/management/frontend/src/dashboard/plugin/View.js @@ -0,0 +1,24 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class PluginView extends Component { + render() { + return
{this.props.plugin.id}
+ } +} + +export default PluginView diff --git a/maubot/management/frontend/src/res/chevron-right.svg b/maubot/management/frontend/src/res/chevron-right.svg new file mode 100644 index 0000000..58ee688 --- /dev/null +++ b/maubot/management/frontend/src/res/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 48fb045..93d10f0 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -36,9 +36,9 @@ text-decoration: none =main-color-button() - background-color: $main-color + background-color: $primary &:hover - background-color: $dark-color + background-color: $primary-dark .button +button @@ -92,7 +92,7 @@ resize: vertical &:hover, &:focus - border-color: $main-color + border-color: $primary &:focus border-width: 2px @@ -101,8 +101,8 @@ .input, .textarea +input -=notification($color: $error-color) +=notification($border: $error-dark, $background: transparentize($error-light, 0.5)) padding: 1rem border-radius: .25rem - border: 2px solid $color - background-color: lighten($color, 25%) + border: 2px solid $border + background-color: $background diff --git a/maubot/management/frontend/src/style/base/vars.sass b/maubot/management/frontend/src/style/base/vars.sass index 9dc77dd..6e9a6c3 100644 --- a/maubot/management/frontend/src/style/base/vars.sass +++ b/maubot/management/frontend/src/style/base/vars.sass @@ -13,16 +13,18 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -$main-color: darken(#50D367, 10%) -$dark-color: darken($main-color, 10%) -$light-color: lighten($main-color, 10%) -$alt-color: darken(#47B9D7, 10%) -$dark-alt-color: darken($alt-color, 10%) -$border-color: #CCC -$error-color: #D35067 +$primary: #00C853 +$primary-dark: #009624 +$primary-light: #5EFC82 +$secondary: #00B8D4 +$secondary-dark: #0088A3 +$secondary-light: #62EBFF +$error: #B71C1C +$error-dark: #7F0000 +$error-light: #F05545 + +$border-color: #DDD $text-color: #212121 $background-color: #FAFAFA $inverted-text-color: $background-color $font-stack: sans-serif -$max-width: 42.5rem -$header-height: 3.5rem diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index 8486157..c2e9a16 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -20,3 +20,4 @@ @import base/elements @import pages/login +@import pages/dashboard diff --git a/maubot/management/frontend/src/style/pages/dashboard-grid.css b/maubot/management/frontend/src/style/pages/dashboard-grid.css new file mode 100644 index 0000000..26906db --- /dev/null +++ b/maubot/management/frontend/src/style/pages/dashboard-grid.css @@ -0,0 +1,6 @@ +.dashboard { + grid-template: + [row1-start] "title topbar" 3.5rem [row1-end] + [row2-start] "sidebar main" auto [row2-end] + / 15rem auto; +} diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass new file mode 100644 index 0000000..4be4111 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -0,0 +1,57 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +@import "dashboard-grid" + +.dashboard + display: grid + height: 100% + + > .title + grid-area: title + display: flex + align-items: center + justify-content: center + + font-size: 1.35rem + font-weight: bold + + color: $text-color + + z-index: 1 + + background-color: $background-color + border-right: 1px solid $primary + border-bottom: 1px solid $border-color + + > img + max-width: 2rem + margin-right: .5rem + + > .topbar + grid-area: topbar + display: flex + align-items: center + justify-content: center + background-color: $primary + width: 110% + margin: 0 -5% + box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .25) + + + @import "sidebar" + + > .dashboard + grid-area: main diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass index 62b0891..6447f54 100644 --- a/maubot/management/frontend/src/style/pages/login.sass +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -15,7 +15,7 @@ // along with this program. If not, see . .maubot-wrapper:not(.authenticated) - background-color: $main-color + background-color: $primary text-align: center @@ -29,7 +29,7 @@ margin-top: 3rem h1 - color: $main-color + color: $primary margin: 3rem 0 input, button @@ -53,5 +53,5 @@ height: 26.5rem .error - +notification($error-color) + +notification($error) margin: .5rem 2.5rem diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass new file mode 100644 index 0000000..a96a2cd --- /dev/null +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -0,0 +1,53 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> .sidebar + grid-area: sidebar + background-color: $background-color + + border-right: 1px solid $border-color + padding: .5rem + + div.list + margin-bottom: 1.5rem + + h3.title + margin: 0 + + a.entry + display: block + color: $text-color + text-decoration: none + + &:not(:hover) > svg + display: none + + > svg + float: right + + padding: .25rem + + &.client + padding: .25rem + + img.avatar + max-height: 1.5rem + border-radius: 100% + vertical-align: middle + + span.displayname + margin-left: .25rem + vertical-align: middle From 9a21c6ade8854f73d56b09536f973408259228df Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 8 Nov 2018 17:25:00 +0200 Subject: [PATCH 04/23] Add endpoint to replace specific plugin --- maubot/management/api/plugin.py | 64 ++++++++++++++++++++---------- maubot/management/api/responses.py | 14 +++++++ maubot/management/api/spec.yaml | 43 ++++++++++++++++++++ 3 files changed, 100 insertions(+), 21 deletions(-) diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 4fbb209..3113124 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -69,6 +69,45 @@ async def reload_plugin(request: web.Request) -> web.Response: return resp.ok +@routes.put("/plugin/{id}") +async def put_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info.get("id", None) + content = await request.read() + file = BytesIO(content) + try: + pid, version = ZippedPluginLoader.verify_meta(file) + except MaubotZipImportError as e: + return resp.plugin_import_error(str(e), traceback.format_exc()) + if pid != plugin_id: + return resp.pid_mismatch + plugin = PluginLoader.id_cache.get(plugin_id, None) + if not plugin: + return await upload_new_plugin(content, pid, version) + elif isinstance(plugin, ZippedPluginLoader): + return await upload_replacement_plugin(plugin, content, version) + else: + return resp.unsupported_plugin_loader + + +@routes.post("/plugins/upload") +async def upload_plugin(request: web.Request) -> web.Response: + content = await request.read() + file = BytesIO(content) + try: + pid, version = ZippedPluginLoader.verify_meta(file) + except MaubotZipImportError as e: + return resp.plugin_import_error(str(e), traceback.format_exc()) + plugin = PluginLoader.id_cache.get(pid, None) + if not plugin: + return await upload_new_plugin(content, pid, version) + elif not request.query.get("allow_override"): + return resp.plugin_exists + elif isinstance(plugin, ZippedPluginLoader): + return await upload_replacement_plugin(plugin, content, version) + else: + return resp.unsupported_plugin_loader + + async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Response: path = os.path.join(get_config()["plugin_directories.upload"], f"{pid}-v{version}.mbp") with open(path, "wb") as p: @@ -86,10 +125,10 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, dirname = os.path.dirname(plugin.path) old_filename = os.path.basename(plugin.path) if plugin.version in old_filename: - filename = old_filename.replace(plugin.version, new_version) - if filename == old_filename: - filename = re.sub(f"{re.escape(plugin.version)}(-ts[0-9]+)?", - f"{new_version}-ts{int(time())}", old_filename) + replacement = (new_version if plugin.version != new_version + else f"{new_version}-ts{int(time())}") + filename = re.sub(f"{re.escape(plugin.version)}(-ts[0-9]+)?", + replacement, old_filename) else: filename = old_filename.rstrip(".mbp") filename = f"{filename}-v{new_version}.mbp" @@ -110,20 +149,3 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, await plugin.start_instances() ZippedPluginLoader.trash(old_path, reason="update") return resp.updated(plugin.to_dict()) - - -@routes.post("/plugins/upload") -async def upload_plugin(request: web.Request) -> web.Response: - content = await request.read() - file = BytesIO(content) - try: - pid, version = ZippedPluginLoader.verify_meta(file) - except MaubotZipImportError as e: - return resp.plugin_import_error(str(e), traceback.format_exc()) - plugin = PluginLoader.id_cache.get(pid, None) - if not plugin: - return await upload_new_plugin(content, pid, version) - elif isinstance(plugin, ZippedPluginLoader): - return await upload_replacement_plugin(plugin, content, version) - else: - return resp.unsupported_plugin_loader diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 9c815c2..be9b3b0 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -61,6 +61,13 @@ class _Response: "errcode": "mxid_mismatch", }, status=HTTPStatus.BAD_REQUEST) + @property + def pid_mismatch(self) -> web.Response: + return web.json_response({ + "error": "The ID in the path does not match the ID of the uploaded plugin", + "errcode": "pid_mismatch", + }, status=HTTPStatus.BAD_REQUEST) + @property def bad_auth(self) -> web.Response: return web.json_response({ @@ -138,6 +145,13 @@ class _Response: "errcode": "user_exists", }, status=HTTPStatus.CONFLICT) + @property + def plugin_exists(self) -> web.Response: + return web.json_response({ + "error": "A plugin with the same ID as the uploaded plugin already exists", + "errcode": "plugin_exists" + }, status=HTTPStatus.CONFLICT) + @property def plugin_in_use(self) -> web.Response: return web.json_response({ diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index 75ec865..efe759e 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -85,6 +85,14 @@ paths: summary: Upload a new plugin description: Upload a new plugin. If the plugin already exists, enabled instances will be restarted. tags: [Plugins] + parameters: + - name: allow_override + in: query + description: Set to allow overriding existing plugins + required: false + schema: + type: boolean + default: false responses: 200: description: Plugin uploaded and replaced current version successfully @@ -102,6 +110,8 @@ paths: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' + 409: + description: Plugin requestBody: content: application/zip: @@ -150,6 +160,39 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + put: + operationId: put_plugin + summary: Upload a new or replacement plugin + description: | + Upload a new or replacement plugin with the specified ID. + A HTTP 400 will be returned if the ID of the uploaded plugin + doesn't match the ID in the path. If the plugin already + exists, enabled instances will be restarted. + tags: [Plugins] + responses: + 200: + description: Plugin uploaded and replaced current version successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 201: + description: New plugin uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) /plugin/{id}/reload: parameters: - name: id From 3e661aa88716b670e5cca754ef2349b19ed81140 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 8 Nov 2018 17:25:22 +0200 Subject: [PATCH 05/23] New buttons in sidebar --- maubot/management/frontend/src/api.js | 20 ++++++-- .../frontend/src/dashboard/client/View.js | 2 +- .../frontend/src/dashboard/index.js | 47 ++++++++++++------- .../frontend/src/dashboard/instance/View.js | 2 +- .../frontend/src/dashboard/plugin/View.js | 2 +- maubot/management/frontend/src/res/plus.svg | 5 ++ .../frontend/src/style/pages/dashboard.sass | 7 +-- .../frontend/src/style/pages/sidebar.sass | 12 ++++- 8 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 maubot/management/frontend/src/res/plus.svg diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 7e1ef7f..d0acbb6 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -68,11 +68,21 @@ export async function getPlugin(id) { return await resp.json() } -export async function uploadPlugin(data) { - const resp = await fetch(`${BASE_PATH}/plugins/upload`, { - headers: getHeaders("application/zip"), - body: data, - }) +export async function uploadPlugin(data, id) { + let resp + if (id) { + resp = await fetch(`${BASE_PATH}/plugin/${id}`, { + headers: getHeaders("applcation/zip"), + body: data, + method: "PUT", + }) + } else { + resp = await fetch(`${BASE_PATH}/plugins/upload`, { + headers: getHeaders("application/zip"), + body: data, + method: "POST", + }) + } return await resp.json() } diff --git a/maubot/management/frontend/src/dashboard/client/View.js b/maubot/management/frontend/src/dashboard/client/View.js index 5256549..b63d58f 100644 --- a/maubot/management/frontend/src/dashboard/client/View.js +++ b/maubot/management/frontend/src/dashboard/client/View.js @@ -17,7 +17,7 @@ import React, { Component } from "react" class ClientView extends Component { render() { - return
{this.props.client.displayname}
+ return
{this.props.displayname}
} } diff --git a/maubot/management/frontend/src/dashboard/index.js b/maubot/management/frontend/src/dashboard/index.js index 6e40a8f..77ea69b 100644 --- a/maubot/management/frontend/src/dashboard/index.js +++ b/maubot/management/frontend/src/dashboard/index.js @@ -14,8 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { Route, Redirect } from "react-router-dom" +import { Route, Switch, Link } from "react-router-dom" import api from "../api" +import { ReactComponent as Plus } from "../res/plus.svg" import InstanceListEntry from "./instance/ListEntry" import InstanceView from "./instance/View" import ClientListEntry from "./client/ListEntry" @@ -62,41 +63,55 @@ class Dashboard extends Component { if (!entry) { return "Not found :(" } - return React.createElement(type, { [field]: entry }) + return React.createElement(type, entry) } render() { return
-
+ Maubot Manager -
+
{localStorage.username}
-
- "Hello, World!"}/> - - this.renderView("instance", InstanceView, match.params.id)}/> - - this.renderView("client", ClientView, match.params.id)}/> - - this.renderView("plugin", PluginView, match.params.id)}/> - }/> +
+ + "Hello, World!"}/> + }/> + }/> + }/> + + this.renderView("instance", InstanceView, match.params.id)}/> + + this.renderView("client", ClientView, match.params.id)}/> + + this.renderView("plugin", PluginView, match.params.id)}/> + "Not found :("}/> +
} diff --git a/maubot/management/frontend/src/dashboard/instance/View.js b/maubot/management/frontend/src/dashboard/instance/View.js index 69bdf9f..9a82cba 100644 --- a/maubot/management/frontend/src/dashboard/instance/View.js +++ b/maubot/management/frontend/src/dashboard/instance/View.js @@ -17,7 +17,7 @@ import React, { Component } from "react" class InstanceView extends Component { render() { - return
{this.props.instance.id}
+ return
{this.props.id}
} } diff --git a/maubot/management/frontend/src/dashboard/plugin/View.js b/maubot/management/frontend/src/dashboard/plugin/View.js index 5b8ccbc..fbcf2c3 100644 --- a/maubot/management/frontend/src/dashboard/plugin/View.js +++ b/maubot/management/frontend/src/dashboard/plugin/View.js @@ -17,7 +17,7 @@ import React, { Component } from "react" class PluginView extends Component { render() { - return
{this.props.plugin.id}
+ return
{this.props.id}
} } diff --git a/maubot/management/frontend/src/res/plus.svg b/maubot/management/frontend/src/res/plus.svg new file mode 100644 index 0000000..8030204 --- /dev/null +++ b/maubot/management/frontend/src/res/plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 4be4111..dd001a0 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -19,7 +19,7 @@ display: grid height: 100% - > .title + > a.title grid-area: title display: flex align-items: center @@ -29,6 +29,7 @@ font-weight: bold color: $text-color + text-decoration: none z-index: 1 @@ -40,7 +41,7 @@ max-width: 2rem margin-right: .5rem - > .topbar + > div.topbar grid-area: topbar display: flex align-items: center @@ -53,5 +54,5 @@ @import "sidebar" - > .dashboard + > main.dashboard grid-area: main diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index a96a2cd..47bbfd7 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -24,8 +24,16 @@ div.list margin-bottom: 1.5rem - h3.title - margin: 0 + div.title + h2 + margin: 0 + display: inline-block + + font-size: 1.25rem + + a + display: inline-block + float: right a.entry display: block From ed16ee88607d103586f17c90e8ccee9b5eee9daf Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 9 Nov 2018 00:54:42 +0200 Subject: [PATCH 06/23] Add initial parts of client view --- .../frontend/src/components/Switch.js | 54 +++++++++ maubot/management/frontend/src/index.js | 2 +- .../frontend/src/{ => pages}/Login.js | 4 +- .../src/{MaubotRouter.js => pages/Main.js} | 10 +- .../frontend/src/pages/dashboard/Client.js | 114 ++++++++++++++++++ .../src/{ => pages}/dashboard/index.js | 15 ++- .../dashboard/instance/ListEntry.js | 2 +- .../{ => pages}/dashboard/instance/View.js | 0 .../{ => pages}/dashboard/plugin/ListEntry.js | 2 +- .../src/{ => pages}/dashboard/plugin/View.js | 0 maubot/management/frontend/src/res/upload.svg | 5 + .../frontend/src/style/base/body.sass | 5 +- .../frontend/src/style/base/elements.sass | 6 +- .../frontend/src/style/base/vars.sass | 6 +- .../management/frontend/src/style/index.sass | 2 +- .../frontend/src/style/lib/switch.sass | 79 ++++++++++++ .../frontend/src/style/pages/client.sass | 109 +++++++++++++++++ .../frontend/src/style/pages/dashboard.sass | 17 +-- .../View.js => style/pages/instance.sass} | 10 +- .../ListEntry.js => style/pages/plugin.sass} | 16 +-- .../frontend/src/style/pages/sidebar.sass | 7 +- 21 files changed, 409 insertions(+), 56 deletions(-) create mode 100644 maubot/management/frontend/src/components/Switch.js rename maubot/management/frontend/src/{ => pages}/Login.js (97%) rename maubot/management/frontend/src/{MaubotRouter.js => pages/Main.js} (92%) create mode 100644 maubot/management/frontend/src/pages/dashboard/Client.js rename maubot/management/frontend/src/{ => pages}/dashboard/index.js (92%) rename maubot/management/frontend/src/{ => pages}/dashboard/instance/ListEntry.js (92%) rename maubot/management/frontend/src/{ => pages}/dashboard/instance/View.js (100%) rename maubot/management/frontend/src/{ => pages}/dashboard/plugin/ListEntry.js (92%) rename maubot/management/frontend/src/{ => pages}/dashboard/plugin/View.js (100%) create mode 100644 maubot/management/frontend/src/res/upload.svg create mode 100644 maubot/management/frontend/src/style/lib/switch.sass create mode 100644 maubot/management/frontend/src/style/pages/client.sass rename maubot/management/frontend/src/{dashboard/client/View.js => style/pages/instance.sass} (80%) rename maubot/management/frontend/src/{dashboard/client/ListEntry.js => style/pages/plugin.sass} (56%) diff --git a/maubot/management/frontend/src/components/Switch.js b/maubot/management/frontend/src/components/Switch.js new file mode 100644 index 0000000..975b47c --- /dev/null +++ b/maubot/management/frontend/src/components/Switch.js @@ -0,0 +1,54 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class Switch extends Component { + constructor(props) { + super(props) + this.state = { + active: props.active, + } + } + + componentWillReceiveProps(nextProps) { + this.setState({ + active: nextProps.active, + }) + } + + toggle = () => { + if (this.props.onToggle) { + this.props.onToggle(!this.state.active) + } else { + this.setState({ active: !this.state.active }) + } + } + + render() { + return ( +
+
+ + {this.props.onText || "On"} + {this.props.offText || "Off"} + +
+
+ ) + } +} + +export default Switch diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js index cbe10ed..12dd05c 100644 --- a/maubot/management/frontend/src/index.js +++ b/maubot/management/frontend/src/index.js @@ -16,6 +16,6 @@ import React from "react" import ReactDOM from "react-dom" import "./style/index.sass" -import App from "./MaubotRouter" +import App from "./pages/Main" ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/pages/Login.js similarity index 97% rename from maubot/management/frontend/src/Login.js rename to maubot/management/frontend/src/pages/Login.js index e342abe..5b97f14 100644 --- a/maubot/management/frontend/src/Login.js +++ b/maubot/management/frontend/src/pages/Login.js @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import Spinner from "./components/Spinner" -import api from "./api" +import Spinner from "../components/Spinner" +import api from "../api" class Login extends Component { constructor(props, context) { diff --git a/maubot/management/frontend/src/MaubotRouter.js b/maubot/management/frontend/src/pages/Main.js similarity index 92% rename from maubot/management/frontend/src/MaubotRouter.js rename to maubot/management/frontend/src/pages/Main.js index b3c7561..efb9ac3 100644 --- a/maubot/management/frontend/src/MaubotRouter.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -15,13 +15,13 @@ // along with this program. If not, see . import React, { Component } from "react" import { BrowserRouter as Router, Switch } from "react-router-dom" -import PrivateRoute from "./components/PrivateRoute" +import PrivateRoute from "../components/PrivateRoute" +import Spinner from "../components/Spinner" +import api from "../api" import Dashboard from "./dashboard" import Login from "./Login" -import Spinner from "./components/Spinner" -import api from "./api" -class MaubotRouter extends Component { +class Main extends Component { constructor(props) { super(props) this.state = { @@ -72,4 +72,4 @@ class MaubotRouter extends Component { } } -export default MaubotRouter +export default Main diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js new file mode 100644 index 0000000..d352b31 --- /dev/null +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -0,0 +1,114 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { Link } from "react-router-dom" +import Switch from "../../components/Switch" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as UploadButton } from "../../res/upload.svg" + +function getAvatarURL(client) { + const id = client.avatar_url.substr("mxc://".length) + return `${client.homeserver}/_matrix/media/r0/download/${id}` +} + +const ClientListEntry = ({ client }) => { + const classes = ["client", "entry"] + if (!client.enabled) { + classes.push("disabled") + } else if (!client.started) { + classes.push("stopped") + } + return ( + + {client.id.substr(1, + {client.displayname || client.id} + + + ) +} + +class Client extends Component { + static ListEntry = ClientListEntry + + constructor(props) { + super(props) + this.state = props + } + + componentWillReceiveProps(nextProps) { + this.setState(nextProps) + } + + inputChange = event => { + this.setState({ [event.target.name]: event.target.value }) + } + + render() { + return
+
+ Avatar + +
+
+
+
User ID
+
+ +
+
+
+
Display name
+
+ +
+
+
+
Homeserver
+
+ +
+
+
+
Access token
+
+ +
+
+
+
Sync
+
+ this.setState({ sync })}/> +
+
+
+
Enabled
+
+ this.setState({ enabled })}/> +
+
+
+ +
+ } +} + +export default Client diff --git a/maubot/management/frontend/src/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js similarity index 92% rename from maubot/management/frontend/src/dashboard/index.js rename to maubot/management/frontend/src/pages/dashboard/index.js index 77ea69b..34dbb0c 100644 --- a/maubot/management/frontend/src/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -15,12 +15,11 @@ // along with this program. If not, see . import React, { Component } from "react" import { Route, Switch, Link } from "react-router-dom" -import api from "../api" -import { ReactComponent as Plus } from "../res/plus.svg" +import api from "../../api" +import { ReactComponent as Plus } from "../../res/plus.svg" import InstanceListEntry from "./instance/ListEntry" import InstanceView from "./instance/View" -import ClientListEntry from "./client/ListEntry" -import ClientView from "./client/View" +import Client from "./Client" import PluginListEntry from "./plugin/ListEntry" import PluginView from "./plugin/View" @@ -88,7 +87,7 @@ class Dashboard extends Component {

Clients

- {this.renderList("client", ClientListEntry)} + {this.renderList("client", Client.ListEntry)}
@@ -98,16 +97,16 @@ class Dashboard extends Component { {this.renderList("plugin", PluginListEntry)}
-
+
"Hello, World!"}/> }/> - }/> + }/> }/> this.renderView("instance", InstanceView, match.params.id)}/> - this.renderView("client", ClientView, match.params.id)}/> + this.renderView("client", Client, match.params.id)}/> this.renderView("plugin", PluginView, match.params.id)}/> "Not found :("}/> diff --git a/maubot/management/frontend/src/dashboard/instance/ListEntry.js b/maubot/management/frontend/src/pages/dashboard/instance/ListEntry.js similarity index 92% rename from maubot/management/frontend/src/dashboard/instance/ListEntry.js rename to maubot/management/frontend/src/pages/dashboard/instance/ListEntry.js index 0603e4d..9b36817 100644 --- a/maubot/management/frontend/src/dashboard/instance/ListEntry.js +++ b/maubot/management/frontend/src/pages/dashboard/instance/ListEntry.js @@ -15,7 +15,7 @@ // along with this program. If not, see . import React from "react" import { Link } from "react-router-dom" -import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as ChevronRight } from "../../../res/chevron-right.svg" const InstanceListEntry = ({ instance }) => ( diff --git a/maubot/management/frontend/src/dashboard/instance/View.js b/maubot/management/frontend/src/pages/dashboard/instance/View.js similarity index 100% rename from maubot/management/frontend/src/dashboard/instance/View.js rename to maubot/management/frontend/src/pages/dashboard/instance/View.js diff --git a/maubot/management/frontend/src/dashboard/plugin/ListEntry.js b/maubot/management/frontend/src/pages/dashboard/plugin/ListEntry.js similarity index 92% rename from maubot/management/frontend/src/dashboard/plugin/ListEntry.js rename to maubot/management/frontend/src/pages/dashboard/plugin/ListEntry.js index 6facdbf..d7563df 100644 --- a/maubot/management/frontend/src/dashboard/plugin/ListEntry.js +++ b/maubot/management/frontend/src/pages/dashboard/plugin/ListEntry.js @@ -15,7 +15,7 @@ // along with this program. If not, see . import React from "react" import { Link } from "react-router-dom" -import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as ChevronRight } from "../../../res/chevron-right.svg" const PluginListEntry = ({ plugin }) => ( diff --git a/maubot/management/frontend/src/dashboard/plugin/View.js b/maubot/management/frontend/src/pages/dashboard/plugin/View.js similarity index 100% rename from maubot/management/frontend/src/dashboard/plugin/View.js rename to maubot/management/frontend/src/pages/dashboard/plugin/View.js diff --git a/maubot/management/frontend/src/res/upload.svg b/maubot/management/frontend/src/res/upload.svg new file mode 100644 index 0000000..f1deea6 --- /dev/null +++ b/maubot/management/frontend/src/res/upload.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index 462fe8c..ba0406b 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -18,7 +18,6 @@ body margin: 0 padding: 0 font-size: 16px - background-color: $background-color #root position: fixed @@ -33,6 +32,10 @@ body bottom: 0 left: 0 right: 0 + background-color: $background-dark + + > * + background-color: $background .maubot-loading margin-top: 10rem diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 93d10f0..76f3a4e 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -19,7 +19,7 @@ padding: $padding width: $width height: $height - background-color: $background-color + background-color: $background border: none border-radius: .25rem color: $inverted-text-color @@ -28,7 +28,7 @@ cursor: pointer &:hover - background-color: darken($background-color, 10%) + background-color: darken($background, 10%) =link-button() display: inline-block @@ -81,7 +81,7 @@ =input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem) font-family: $font-stack border: 1px solid $border-color - background-color: $background-color + background-color: $background color: $text-color width: $width height: $height diff --git a/maubot/management/frontend/src/style/base/vars.sass b/maubot/management/frontend/src/style/base/vars.sass index 6e9a6c3..e179396 100644 --- a/maubot/management/frontend/src/style/base/vars.sass +++ b/maubot/management/frontend/src/style/base/vars.sass @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . + $primary: #00C853 $primary-dark: #009624 $primary-light: #5EFC82 @@ -25,6 +26,7 @@ $error-light: #F05545 $border-color: #DDD $text-color: #212121 -$background-color: #FAFAFA -$inverted-text-color: $background-color +$background: #FAFAFA +$background-dark: #E7E7E7 +$inverted-text-color: $background $font-stack: sans-serif diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index c2e9a16..f6b6033 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -14,10 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . @import lib/spinner - @import base/vars @import base/body @import base/elements +@import lib/switch @import pages/login @import pages/dashboard diff --git a/maubot/management/frontend/src/style/lib/switch.sass b/maubot/management/frontend/src/style/lib/switch.sass new file mode 100644 index 0000000..c087da4 --- /dev/null +++ b/maubot/management/frontend/src/style/lib/switch.sass @@ -0,0 +1,79 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.switch + display: flex + + width: 100% + height: 2rem + + cursor: pointer + + border: 1px solid $primary + border-radius: .25rem + background-color: $background + + box-sizing: border-box + + > .box + box-sizing: border-box + width: 50% + height: 100% + + transition: .5s + text-align: center + + color: $inverted-text-color + border-radius: .15rem 0 0 .15rem + background-color: $primary + + align-items: center + + > .text + box-sizing: border-box + width: 100% + + text-align: center + vertical-align: middle + + color: $inverted-text-color + font-size: 1rem + + user-select: none + + .on + display: none + + .off + display: inline + + + &[data-active=true] + > .box + transform: translateX(100%) + + border-radius: 0 .15rem .15rem 0 + background-color: $primary + + .on + display: inline + + .off + display: none + + + + diff --git a/maubot/management/frontend/src/style/pages/client.sass b/maubot/management/frontend/src/style/pages/client.sass new file mode 100644 index 0000000..a6a6f6a --- /dev/null +++ b/maubot/management/frontend/src/style/pages/client.sass @@ -0,0 +1,109 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> .client + margin: 1rem + + div.avatar-container + position: relative + display: inline-block + width: 8rem + height: 8rem + border-radius: 100% + cursor: pointer + vertical-align: top + + > img.avatar + display: block + max-width: 8rem + max-height: 8rem + border-radius: 100% + position: absolute + left: 50% + top: 50% + -webkit-transform: translateY(-50%) translateX(-50%) + + > svg.upload + position: absolute + display: block + visibility: hidden + + width: 6rem + height: 6rem + + padding: 1rem + + &:hover + > img.avatar + opacity: .25 + + > svg.upload + visibility: visible + + div.info-container + display: inline-table + vertical-align: top + + margin: 1rem 2rem + + > .row + display: table-row + + > .key, > .value + display: table-cell + padding-bottom: .5rem + + > .key + width: 6.5rem + + > .value + margin: .5rem + + > .value > .switch + width: auto + height: 2rem + + > .value > input + border: none + height: 2rem + width: 100% + + box-sizing: border-box + + padding: .375rem 0 + background-color: $background + + font-size: 1rem + + border-bottom: 1px solid transparent + + &:hover:not(:disabled) + border-bottom: 1px solid $primary + + &:focus:not(:disabled) + border-bottom: 2px solid $primary + +//> .client + display: table + + > .field + display: table-row + width: 100% + + > .name, > .value + display: table-cell + width: 50% + text-align: center diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index dd001a0..801fc86 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -18,6 +18,9 @@ .dashboard display: grid height: 100% + max-width: 60rem + margin: auto + box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5) > a.title grid-area: title @@ -31,9 +34,7 @@ color: $text-color text-decoration: none - z-index: 1 - - background-color: $background-color + background-color: white border-right: 1px solid $primary border-bottom: 1px solid $border-color @@ -47,12 +48,14 @@ align-items: center justify-content: center background-color: $primary - width: 110% - margin: 0 -5% - box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .25) + box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .2) @import "sidebar" - > main.dashboard + > main.view grid-area: main + + @import "client" + @import "instance" + @import "plugin" diff --git a/maubot/management/frontend/src/dashboard/client/View.js b/maubot/management/frontend/src/style/pages/instance.sass similarity index 80% rename from maubot/management/frontend/src/dashboard/client/View.js rename to maubot/management/frontend/src/style/pages/instance.sass index b63d58f..7847402 100644 --- a/maubot/management/frontend/src/dashboard/client/View.js +++ b/maubot/management/frontend/src/style/pages/instance.sass @@ -13,12 +13,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -class ClientView extends Component { - render() { - return
{this.props.displayname}
- } -} - -export default ClientView +> .instance + margin: 1rem diff --git a/maubot/management/frontend/src/dashboard/client/ListEntry.js b/maubot/management/frontend/src/style/pages/plugin.sass similarity index 56% rename from maubot/management/frontend/src/dashboard/client/ListEntry.js rename to maubot/management/frontend/src/style/pages/plugin.sass index e19055f..e1376b5 100644 --- a/maubot/management/frontend/src/dashboard/client/ListEntry.js +++ b/maubot/management/frontend/src/style/pages/plugin.sass @@ -13,18 +13,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react" -import { Link } from "react-router-dom" -import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" -const ClientListEntry = ({ client }) => ( - - {client.id.substr(1, - {client.displayname || client.id} - - -) - -export default ClientListEntry +> .plugin + margin: 1rem diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index 47bbfd7..cb48b12 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -16,13 +16,16 @@ > .sidebar grid-area: sidebar - background-color: $background-color + background-color: white border-right: 1px solid $border-color padding: .5rem + overflow-y: auto + div.list - margin-bottom: 1.5rem + &:not(:last-of-type) + margin-bottom: 1.5rem div.title h2 From ef3f4a20f29bb1f9e3b9408980257fbf2e26c295 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 9 Nov 2018 20:03:08 +0200 Subject: [PATCH 07/23] Add endpoint to upload avatars --- maubot/management/api/client.py | 13 +++++++ maubot/management/api/spec.yaml | 69 ++++++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 872c965..051f9be 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -127,3 +127,16 @@ async def delete_client(request: web.Request) -> web.Response: await client.stop() client.delete() return resp.deleted + + +@routes.post("/client/{id}/avatar") +async def upload_avatar(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return resp.client_not_found + content = await request.read() + return web.json_response({ + "content_uri": await client.client.upload_media( + content, request.headers.get("Content-Type", None)), + }) diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index efe759e..af93a65 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -93,6 +93,13 @@ paths: schema: type: boolean default: false + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) responses: 200: description: Plugin uploaded and replaced current version successfully @@ -111,14 +118,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 409: - description: Plugin - requestBody: - content: - application/zip: - schema: - type: string - format: binary - example: The plugin maubot archive (.mbp) + description: Plugin already exists and allow_override was not specified. '/plugin/{id}': parameters: - name: id @@ -169,6 +169,13 @@ paths: doesn't match the ID in the path. If the plugin already exists, enabled instances will be restarted. tags: [Plugins] + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) responses: 200: description: Plugin uploaded and replaced current version successfully @@ -186,13 +193,6 @@ paths: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' - requestBody: - content: - application/zip: - schema: - type: string - format: binary - example: The plugin maubot archive (.mbp) /plugin/{id}/reload: parameters: - name: id @@ -399,6 +399,45 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '/client/{id}/avatar': + parameters: + - name: id + in: path + description: The Matrix user ID of the client to get + required: true + schema: + type: string + post: + operationId: upload_avatar + summary: Upload a profile picture for a bot + tags: [Clients] + requestBody: + content: + image/png: + schema: + type: string + format: binary + example: The avatar to upload + image/jpeg: + schema: + type: string + format: binary + example: The avatar to upload + responses: + 200: + description: The avatar was uploaded successfully + content: + application/json: + schema: + type: object + properties: + content_uri: + type: string + description: The MXC URI of the uploaded avatar + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' components: responses: From 29adf50ae0f308093f32820d9251d7e879faf7fe Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 9 Nov 2018 20:03:26 +0200 Subject: [PATCH 08/23] Finish initial client main view --- maubot/management/frontend/src/api.js | 24 ++- .../src/components/PreferenceTable.js | 55 ++++++ .../frontend/src/pages/dashboard/Client.js | 161 +++++++++++++----- .../frontend/src/pages/dashboard/index.js | 16 +- .../frontend/src/style/base/elements.sass | 10 +- .../management/frontend/src/style/index.sass | 3 + .../src/style/lib/preferencetable.sass | 57 +++++++ .../frontend/src/style/pages/client.sass | 143 ++++++++++------ .../frontend/src/style/pages/dashboard.sass | 12 +- 9 files changed, 370 insertions(+), 111 deletions(-) create mode 100644 maubot/management/frontend/src/components/PreferenceTable.js create mode 100644 maubot/management/frontend/src/style/lib/preferencetable.sass diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index d0acbb6..8721685 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -72,7 +72,7 @@ export async function uploadPlugin(data, id) { let resp if (id) { resp = await fetch(`${BASE_PATH}/plugin/${id}`, { - headers: getHeaders("applcation/zip"), + headers: getHeaders("application/zip"), body: data, method: "PUT", }) @@ -96,9 +96,27 @@ export async function getClient(id) { return await resp.json() } +export async function uploadAvatar(id, data, mime) { + const resp = await fetch(`${BASE_PATH}/client/${id}/avatar`, { + headers: getHeaders(mime), + body: data, + method: "POST", + }) + return await resp.json() +} + +export async function putClient(client) { + const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { + headers: getHeaders(), + body: JSON.stringify(client), + method: "PUT", + }) + return await resp.json() +} + export default { login, ping, getInstances, getInstance, - getPlugins, getPlugin, uploadPlugin, - getClients, getClient, + getPlugins, getPlugin, uploadPlugin, + getClients, getClient, uploadAvatar, putClient, } diff --git a/maubot/management/frontend/src/components/PreferenceTable.js b/maubot/management/frontend/src/components/PreferenceTable.js new file mode 100644 index 0000000..28ddfab --- /dev/null +++ b/maubot/management/frontend/src/components/PreferenceTable.js @@ -0,0 +1,55 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import Switch from "./Switch" + +export const PrefTable = ({ children, wrapperClass }) => { + if (wrapperClass) { + return ( +
+
+ {children} +
+
+ ) + } + return ( +
+ {children} +
+ ) +} + +export const PrefRow = ({ name, children }) => ( +
+
{name}
+
{children}
+
+) + +export const PrefInput = ({ rowName, ...args }) => ( + + + +) + +export const PrefSwitch = ({ rowName, ...args }) => ( + + + +) + +export default PrefTable diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index d352b31..2019444 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -15,11 +15,16 @@ // along with this program. If not, see . import React, { Component } from "react" import { Link } from "react-router-dom" -import Switch from "../../components/Switch" import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as UploadButton } from "../../res/upload.svg" +import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable" +import Spinner from "../../components/Spinner" +import api from "../../api" function getAvatarURL(client) { + if (!client.avatar_url) { + return "" + } const id = client.avatar_url.substr("mxc://".length) return `${client.homeserver}/_matrix/media/r0/download/${id}` } @@ -45,68 +50,136 @@ class Client extends Component { constructor(props) { super(props) - this.state = props + this.state = Object.assign(this.initialState, props.client) + } + + get initialState() { + return { + id: "", + displayname: "", + homeserver: "", + avatar_url: "", + access_token: "", + sync: true, + autojoin: true, + enabled: true, + started: false, + + uploadingAvatar: false, + saving: false, + startingOrStopping: false, + } } componentWillReceiveProps(nextProps) { - this.setState(nextProps) + this.setState(Object.assign(this.initialState, nextProps.client)) } inputChange = event => { + if (!event.target.name) { + return + } this.setState({ [event.target.name]: event.target.value }) } + async readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsArrayBuffer(file) + reader.onload = evt => resolve(evt.target.result) + reader.onerror = err => reject(err) + }) + } + + avatarUpload = async event => { + const file = event.target.files[0] + this.setState({ + uploadingAvatar: true, + }) + const data = await this.readFile(file) + const resp = await api.uploadAvatar(this.state.id, data, file.type) + this.setState({ + uploadingAvatar: false, + avatar_url: resp.content_uri, + }) + } + + save = async () => { + this.setState({ saving: true }) + const resp = await api.putClient(this.state) + if (resp.id) { + resp.saving = false + this.setState(resp) + } else { + console.error(resp) + } + } + + startOrStop = async () => { + this.setState({ startingOrStopping: true }) + const resp = await api.putClient({ + id: this.state.id, + started: !this.state.started, + }) + if (resp.id) { + resp.startingOrStopping = false + this.setState(resp) + } else { + console.error(resp) + } + } + render() { return
-
- Avatar - +
+
+ Avatar + + + {this.state.uploadingAvatar && } +
+ {this.props.client && (<> +
+ + {this.state.started ? "Started" : "Stopped"} +
+ + )}
-
-
User ID
-
- + -
-
-
-
Display name
-
- -
-
-
-
Homeserver
-
- -
-
-
-
Access token
-
- -
-
-
-
Sync
-
- + this.setState({ sync })}/> -
-
-
-
Enabled
-
- this.setState({ autojoin })}/> + this.setState({ enabled })}/> -
-
-
+ + +
} } diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index 34dbb0c..5d9a38d 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -58,11 +58,19 @@ class Dashboard extends Component { } renderView(field, type, id) { - const entry = this.state[field + "s"][id] + const stateField = field + "s" + const entry = this.state[stateField][id] if (!entry) { return "Not found :(" } - return React.createElement(type, entry) + return React.createElement(type, { + [field]: entry, + onChange: newEntry => this.setState({ + [stateField]: Object.assign({}, this.state[stateField], { + [id]: newEntry, + }), + }), + }) } render() { @@ -72,7 +80,9 @@ class Dashboard extends Component { Maubot Manager
- {localStorage.username} +
+ {localStorage.username} +
"Hello, World!"}/> - }/> + this.add("instances", newEntry)}/>}/> this.add("clients", newEntry)}/>}/> - }/> + this.add("plugins", newEntry)}/>}/> - this.renderView("instance", InstanceView, match.params.id)}/> + this.renderView("instance", Instance, match.params.id)}/> this.renderView("client", Client, match.params.id)}/> - this.renderView("plugin", PluginView, match.params.id)}/> + this.renderView("plugin", Plugin, match.params.id)}/> "Not found :("}/>
diff --git a/maubot/management/frontend/src/style/lib/switch.sass b/maubot/management/frontend/src/style/lib/switch.sass index c087da4..570320c 100644 --- a/maubot/management/frontend/src/style/lib/switch.sass +++ b/maubot/management/frontend/src/style/lib/switch.sass @@ -29,6 +29,7 @@ box-sizing: border-box > .box + display: flex box-sizing: border-box width: 50% height: 100% diff --git a/maubot/management/frontend/src/style/pages/client.sass b/maubot/management/frontend/src/style/pages/client.sass deleted file mode 100644 index e0ffb66..0000000 --- a/maubot/management/frontend/src/style/pages/client.sass +++ /dev/null @@ -1,145 +0,0 @@ -// maubot - A plugin-based Matrix bot system. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -> .client - margin: 1rem - display: flex - - > div.sidebar - vertical-align: top - text-align: center - width: 8rem - - > div - margin-bottom: 1rem - - > div.avatar-container - position: relative - width: 8rem - height: 8rem - border-radius: 50% - overflow: hidden - - display: flex - align-items: center - justify-content: center - - > img.avatar - position: absolute - display: block - max-width: 8rem - max-height: 8rem - user-select: none - - > svg.upload - position: absolute - display: block - visibility: hidden - - width: 6rem - height: 6rem - - padding: 1rem - user-select: none - - > input.file-selector - position: absolute - width: 8rem - height: 8rem - user-select: none - opacity: 0 - - > div.spinner - +thick-spinner - - &:not(.uploading) - > input.file-selector - cursor: pointer - - &:hover, &.drag - > img.avatar - opacity: .25 - - > svg.upload - visibility: visible - - &.no-avatar - > img.avatar - visibility: hidden - - > svg.upload - visibility: visible - opacity: .5 - - &.uploading - > img.avatar - opacity: .25 - - > div.started-container - display: inline-flex - - > span.started - display: inline-block - height: 0 - width: 0 - border-radius: 50% - margin: .5rem - - &.true - background-color: $primary - box-shadow: 0 0 .75rem .75rem $primary - - &.false - background-color: $error-light - box-shadow: 0 0 .75rem .75rem $error-light - - &.disabled - background-color: $border-color - box-shadow: 0 0 .75rem .75rem $border-color - - > span.text - display: inline-block - margin-left: 1rem - - > div.info-container - vertical-align: top - - margin: 0 1rem - flex: 1 - - > .buttons - display: flex - - +button-group - - > .error - margin-top: 1rem - +notification($error) - - &:empty - display: none - - button.save, button.delete - +button - +main-color-button - width: 100% - height: 2.5rem - padding: 0 - - > .spinner - +thick-spinner - +white-spinner - width: 2rem diff --git a/maubot/management/frontend/src/style/pages/client/avatar.sass b/maubot/management/frontend/src/style/pages/client/avatar.sass new file mode 100644 index 0000000..9174ba8 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/client/avatar.sass @@ -0,0 +1,77 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.avatar-container + position: relative + width: 8rem + height: 8rem + border-radius: 50% + overflow: hidden + + display: flex + align-items: center + justify-content: center + + > img.avatar + position: absolute + display: block + max-width: 8rem + max-height: 8rem + user-select: none + + > svg.upload + position: absolute + display: block + visibility: hidden + + width: 6rem + height: 6rem + + padding: 1rem + user-select: none + + > input.file-selector + position: absolute + width: 8rem + height: 8rem + user-select: none + opacity: 0 + + > div.spinner + +thick-spinner + + &:not(.uploading) + > input.file-selector + cursor: pointer + + &:hover, &.drag + > img.avatar + opacity: .25 + + > svg.upload + visibility: visible + + &.no-avatar + > img.avatar + visibility: hidden + + > svg.upload + visibility: visible + opacity: .5 + + &.uploading + > img.avatar + opacity: .25 diff --git a/maubot/management/frontend/src/style/pages/client/index.sass b/maubot/management/frontend/src/style/pages/client/index.sass new file mode 100644 index 0000000..f34e43a --- /dev/null +++ b/maubot/management/frontend/src/style/pages/client/index.sass @@ -0,0 +1,61 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.client + display: flex + margin: 2rem + + + > div.sidebar + vertical-align: top + text-align: center + width: 8rem + + > div + margin-bottom: 1rem + + @import avatar + @import started + + > div.info + vertical-align: top + margin-left: 1rem + flex: 1 + + @import instances + + > .buttons + display: flex + +button-group + + > .error + margin-top: 1rem + +notification($error) + + &:empty + display: none + + button.save, button.delete + +button + +main-color-button + width: 100% + height: 2.5rem + padding: 0 + + > .spinner + +thick-spinner + +white-spinner + width: 2rem diff --git a/maubot/management/frontend/src/pages/dashboard/instance/View.js b/maubot/management/frontend/src/style/pages/client/instances.sass similarity index 62% rename from maubot/management/frontend/src/pages/dashboard/instance/View.js rename to maubot/management/frontend/src/style/pages/client/instances.sass index 9a82cba..7d5fb3c 100644 --- a/maubot/management/frontend/src/pages/dashboard/instance/View.js +++ b/maubot/management/frontend/src/style/pages/client/instances.sass @@ -13,12 +13,24 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -class InstanceView extends Component { - render() { - return
{this.props.id}
- } -} +> div.instances + margin-top: 1rem -export default InstanceView + > h3 + margin-bottom: .5rem + + > a.instance + display: block + width: 100% + padding: .375rem .5rem + background-color: white + border-radius: .25rem + color: $text-color + text-decoration: none + box-sizing: border-box + border: 1px solid $primary + + &:hover + color: $inverted-text-color + background-color: $primary diff --git a/maubot/management/frontend/src/pages/dashboard/plugin/View.js b/maubot/management/frontend/src/style/pages/client/started.sass similarity index 55% rename from maubot/management/frontend/src/pages/dashboard/plugin/View.js rename to maubot/management/frontend/src/style/pages/client/started.sass index fbcf2c3..0f63095 100644 --- a/maubot/management/frontend/src/pages/dashboard/plugin/View.js +++ b/maubot/management/frontend/src/style/pages/client/started.sass @@ -13,12 +13,29 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -class PluginView extends Component { - render() { - return
{this.props.id}
- } -} +> div.started-container + display: inline-flex -export default PluginView + > span.started + display: inline-block + height: 0 + width: 0 + border-radius: 50% + margin: .5rem + + &.true + background-color: $primary + box-shadow: 0 0 .75rem .75rem $primary + + &.false + background-color: $error-light + box-shadow: 0 0 .75rem .75rem $error-light + + &.disabled + background-color: $border-color + box-shadow: 0 0 .75rem .75rem $border-color + + > span.text + display: inline-block + margin-left: 1rem diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 184c4db..8ec9563 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -61,11 +61,11 @@ border-radius: .25rem - @import "sidebar" + @import sidebar > main.view grid-area: main - @import "client" - @import "instance" - @import "plugin" + @import client/index + @import instance + @import plugin diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index cb48b12..8015e1a 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -42,6 +42,10 @@ display: block color: $text-color text-decoration: none + padding: .25rem + border-radius: .25rem + height: 2rem + box-sizing: border-box &:not(:hover) > svg display: none @@ -49,16 +53,18 @@ > svg float: right - padding: .25rem + &:hover + background-color: $primary-light + + &.active + background-color: $primary &.client - padding: .25rem - img.avatar max-height: 1.5rem border-radius: 100% vertical-align: middle - span.displayname - margin-left: .25rem - vertical-align: middle + span.displayname, span.id + margin-left: .25rem + vertical-align: middle diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock index 9b58f83..47ad628 100644 --- a/maubot/management/frontend/yarn.lock +++ b/maubot/management/frontend/yarn.lock @@ -824,6 +824,58 @@ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== +"@sentry/core@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.3.0.tgz#f51f86b380637b5f2348cd35fdb96c224023c103" + integrity sha512-VEMRshyF2J2IJkGTF9fnAd2/a/wR+42qrKwCLVeNxHzZm53VAnSfJd8ZA5jnN5RiydQVGDOylqvdfij/LmmsuQ== + dependencies: + "@sentry/hub" "4.3.0" + "@sentry/minimal" "4.3.0" + "@sentry/types" "4.3.0" + "@sentry/utils" "4.3.0" + +"@sentry/hub@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.3.0.tgz#ed7731583d81664057de73e3221a393116362b04" + integrity sha512-9nh4Hcx2tZQVHr5JGy1XZd1RgwC2C+1tNFXu06rcxYB20nhKOKtOAH9STrv64vpnK+70AC4miRdlCgqHOKLdxA== + dependencies: + "@sentry/types" "4.3.0" + "@sentry/utils" "4.3.0" + +"@sentry/minimal@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.3.0.tgz#d83826e713d54c95d4986187d223ef1fabc4e1ed" + integrity sha512-yRN+L3O9mlcjqpWiivqJsm3Cy/YNh0ejQKpxxxPD186tl5nzEkAqRq5jCYelYIAdH1eBvg87H5VNNFr/shHxaQ== + dependencies: + "@sentry/hub" "4.3.0" + "@sentry/types" "4.3.0" + +"@sentry/node@^4.0.0-beta.12": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-4.3.0.tgz#5a966d6d791d19a064eeede2cb265cf057414a3f" + integrity sha512-2TZHi0XYpSVzKGT/M7mMlXidya4+ZmSZxZCMJ8KlWPljuQxzPoUeGwmYGXOHEsktOGG2eGajDv1hjGtHfamlnA== + dependencies: + "@sentry/core" "4.3.0" + "@sentry/hub" "4.3.0" + "@sentry/types" "4.3.0" + "@sentry/utils" "4.3.0" + cookie "0.3.1" + lsmod "1.0.0" + md5 "2.2.1" + stack-trace "0.0.10" + +"@sentry/types@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.3.0.tgz#a5cec425764a72a0817329ac212c5ce085ecdd05" + integrity sha512-oBAusA+/JFJJlNfHgikyBPTyvmCr/YxZsLWL2FNOBNJkMClW+m7tQ63x/ZAqRSEd8lnJQxJSU7LfAYDH6LcChQ== + +"@sentry/utils@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.3.0.tgz#4d278b55deb6175ada2a78b56716aa2df97cd389" + integrity sha512-cEGygJbyc1SUGxbanvk3+JYadiywJupdEkp6L8rRfxur/nNQZq1Jd7tkdm4I2rz5YFVg+yayadAquvr4DIu9nA== + dependencies: + "@sentry/types" "4.3.0" + "@svgr/core@^2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@svgr/core/-/core-2.4.1.tgz#03a407c28c4a1d84305ae95021e8eabfda8fa731" @@ -1044,6 +1096,13 @@ acorn-globals@^4.1.0, acorn-globals@^4.3.0: acorn "^6.0.1" acorn-walk "^6.0.1" +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s= + dependencies: + acorn "^3.0.4" + acorn-jsx@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e" @@ -1056,7 +1115,12 @@ acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.0.tgz#c957f4a1460da46af4a0388ce28b4c99355b0cbc" integrity sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg== -acorn@^5.0.0, acorn@^5.0.3, acorn@^5.5.3, acorn@^5.6.0, acorn@^5.6.2: +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + integrity sha1-ReN/s56No/JbruP/U2niu18iAXo= + +acorn@^5.0.0, acorn@^5.0.3, acorn@^5.5.0, acorn@^5.5.3, acorn@^5.6.0, acorn@^5.6.2: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== @@ -1076,11 +1140,24 @@ ajv-errors@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" integrity sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk= +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= + ajv-keywords@^3.0.0, ajv-keywords@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= +ajv@^4.7.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" @@ -1116,6 +1193,11 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.1.0.tgz#dcfaacc90ef9187de413ec3ef8d5eb981a98808f" integrity sha512-hTv1qPdi+sVEk3jYsdjox5nQI0C9HTbjKShbCdYLKb1LOfNbb7wsF4d7OEKIZoxIHx02tSp3m94jcPW2EfMjmA== +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= + ansi-escapes@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" @@ -2082,7 +2164,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: +chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== @@ -2091,7 +2173,7 @@ chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -2107,6 +2189,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + check-types@^7.3.0: version "7.4.0" resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.4.0.tgz#0378ec1b9616ec71f774931a3c6516fad8c152f4" @@ -2179,6 +2266,13 @@ clean-css@4.2.x: dependencies: source-map "~0.6.0" +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= + dependencies: + restore-cursor "^1.0.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -2312,7 +2406,7 @@ commander@2.17.x, commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.11.0: +commander@^2.11.0, commander@^2.15.1, commander@^2.8.1: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -2362,7 +2456,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.5.0: +concat-stream@^1.4.6, concat-stream@^1.5.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -2536,6 +2630,11 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -2763,6 +2862,13 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8= + dependencies: + es5-ext "^0.10.9" + damerau-levenshtein@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" @@ -2789,7 +2895,7 @@ date-now@^0.1.4: resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -3003,7 +3109,7 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" -doctrine@1.5.0: +doctrine@1.5.0, doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= @@ -3218,6 +3324,65 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.46" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572" + integrity sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + next-tick "1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA= + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE= + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + integrity sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8= + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3240,6 +3405,16 @@ escodegen@^1.11.0, escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + integrity sha1-4Bl16BJ4GhY6ba392AOY3GTIicM= + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-config-react-app@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-3.0.4.tgz#83f394d765e7d5af623d773e64609e9c9f2cbeb5" @@ -3392,6 +3567,53 @@ eslint@5.6.0: table "^4.0.3" text-table "^0.2.0" +eslint@^2.7.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11" + integrity sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE= + dependencies: + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + es6-map "^0.1.3" + escope "^3.6.0" + espree "^3.1.6" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^1.1.1" + glob "^7.0.3" + globals "^9.2.0" + ignore "^3.1.2" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + optionator "^0.8.1" + path-is-absolute "^1.0.0" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.6.0" + strip-json-comments "~1.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.1.6: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" + integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A== + dependencies: + acorn "^5.5.0" + acorn-jsx "^3.0.0" + espree@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634" @@ -3439,6 +3661,14 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -3497,6 +3727,11 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -3695,6 +3930,14 @@ figgy-pudding@^3.1.0, figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3702,6 +3945,14 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +file-entry-cache@^1.1.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8" + integrity sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g= + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -3912,6 +4163,13 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +front-matter@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb" + integrity sha1-91mDufL0E75ljJPf172M5AePXNs= + dependencies: + js-yaml "^3.4.6" + fs-extra@7.0.0, fs-extra@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6" @@ -3921,6 +4179,15 @@ fs-extra@7.0.0, fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" + integrity sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + fs-extra@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" @@ -4001,6 +4268,20 @@ gaze@^1.0.0: dependencies: globule "^1.0.0" +generate-function@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= + dependencies: + is-property "^1.0.0" + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -4093,7 +4374,7 @@ globals@^11.1.0, globals@^11.7.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.8.0.tgz#c1ef45ee9bed6badf0663c5cb90e8d1adec1321d" integrity sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA== -globals@^9.18.0: +globals@^9.18.0, globals@^9.2.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== @@ -4130,6 +4411,19 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" +gonzales-pe-sl@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6" + integrity sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y= + dependencies: + minimist "1.1.x" + +"gonzales-pe-sl@github:srowhani/gonzales-pe#dev": + version "4.2.3" + resolved "https://codeload.github.com/srowhani/gonzales-pe/tar.gz/3b052416074edc280f7d04bbe40b2e410693c4a3" + dependencies: + minimist "1.1.x" + graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -4547,6 +4841,11 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" +ignore@^3.1.2: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -4651,6 +4950,25 @@ inquirer@6.2.0, inquirer@^6.1.0: strip-ansi "^4.0.0" through "^2.3.6" +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + integrity sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34= + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + internal-ip@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" @@ -4732,7 +5050,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.0.2, is-buffer@^1.1.5: +is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -4889,6 +5207,22 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== + +is-my-json-valid@^2.10.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175" + integrity sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q== + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -4954,6 +5288,11 @@ is-promise@^2.1.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= +is-property@^1.0.0, is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= + is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -5475,7 +5814,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: +js-yaml@^3.11.0, js-yaml@^3.12.0, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== @@ -5591,7 +5930,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stable-stringify@^1.0.1: +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= @@ -5613,6 +5952,13 @@ json5@^0.5.0, json5@^0.5.1: resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= +jsonfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" + integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY= + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -5625,6 +5971,11 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -5683,6 +6034,11 @@ kleur@^2.0.1: resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== +known-css-properties@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4" + integrity sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ== + last-call-webpack-plugin@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" @@ -5807,6 +6163,11 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.capitalize@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" + integrity sha1-+CbJtOKoUR2E46yinbBeGk87cqk= + lodash.clonedeep@^4.3.2: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -5817,6 +6178,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.kebabcase@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" + integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -5857,7 +6223,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@~4.17.10: +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -5895,6 +6261,11 @@ lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.3: pseudomap "^1.0.2" yallist "^2.1.2" +lsmod@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b" + integrity sha1-mgD3bco26yP6BTUK/htYXUKZ5ks= + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -5952,6 +6323,15 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +md5@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + mdn-data@~1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" @@ -6137,6 +6517,11 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= +minimist@1.1.x: + version "1.1.3" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" + integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag= + minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -6252,6 +6637,11 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA= + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6303,6 +6693,11 @@ neo-async@^2.5.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.2.tgz#489105ce7bc54e709d736b195f82135048c50fcc" integrity sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw== +next-tick@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -6627,6 +7022,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= + onetime@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" @@ -7000,6 +7400,11 @@ pkg-up@2.0.0: dependencies: find-up "^2.1.0" +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + integrity sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU= + pluralize@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" @@ -7696,6 +8101,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= + progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" @@ -8094,6 +8504,15 @@ readdirp@^2.0.0: micromatch "^3.1.10" readable-stream "^2.0.2" +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + realpath-native@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560" @@ -8313,7 +8732,7 @@ require-main-filename@^1.0.1: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= -require-uncached@^1.0.3: +require-uncached@^1.0.2, require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= @@ -8373,6 +8792,14 @@ resolve@1.8.1, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: dependencies: path-parse "^1.0.5" +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -8416,6 +8843,13 @@ rsvp@^3.3.3: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k= + dependencies: + once "^1.3.0" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -8430,6 +8864,11 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= + rxjs@^6.1.0: version "6.3.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" @@ -8480,6 +8919,39 @@ sass-graph@^2.2.4: scss-tokenizer "^0.2.3" yargs "^7.0.0" +sass-lint-auto-fix@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/sass-lint-auto-fix/-/sass-lint-auto-fix-0.15.0.tgz#503d1b73aaf5e3e033d4fe3b80f6c7ad80bb7a46" + integrity sha512-Q1WFpm9Ro1S3xPEavqWLIMx8KRTmkoHZh83+aBoqemiMtRB1KwD7VtLz4jreiZP6fZfSXee1Ln3BjHpKaC6hbA== + dependencies: + "@sentry/node" "^4.0.0-beta.12" + chalk "^2.3.2" + commander "^2.15.1" + glob "^7.1.2" + gonzales-pe-sl srowhani/gonzales-pe#dev + js-yaml "^3.11.0" + sass-lint "^1.12.1" + +sass-lint@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83" + integrity sha1-Yw9pwhaqIGuCMvsqqQe98zNrbYM= + dependencies: + commander "^2.8.1" + eslint "^2.7.0" + front-matter "2.1.2" + fs-extra "^3.0.1" + glob "^7.0.0" + globule "^1.0.0" + gonzales-pe-sl "^4.2.3" + js-yaml "^3.5.4" + known-css-properties "^0.3.0" + lodash.capitalize "^4.1.0" + lodash.kebabcase "^4.0.0" + merge "^1.2.0" + path-is-absolute "^1.0.0" + util "^0.10.3" + sass-loader@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" @@ -8690,6 +9162,11 @@ shell-quote@1.6.1: array-reduce "~0.0.0" jsonify "~0.0.0" +shelljs@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" + integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg= + shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -8717,6 +9194,11 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= + slice-ansi@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" @@ -8924,6 +9406,11 @@ stable@~0.1.6: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-trace@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" @@ -9088,6 +9575,11 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-json-comments@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E= + style-loader@0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.0.tgz#8377fefab68416a2e05f1cabd8c3a3acfcce74f1" @@ -9149,6 +9641,18 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + integrity sha1-K7xULw/amGGnVdOUf+/Ys/UThV8= + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + table@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" @@ -9222,7 +9726,7 @@ test-exclude@^4.2.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" -text-table@0.2.0, text-table@^0.2.0: +text-table@0.2.0, text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= @@ -9562,6 +10066,13 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + integrity sha1-nHC/2Babwdy/SGBODwS4tJzenp8= + dependencies: + os-homedir "^1.0.0" + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From 53d2264351ca932830742275db859bcddb9ab37d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 12:57:06 +0200 Subject: [PATCH 11/23] Proxy avatar requests through server and improve css --- maubot/management/api/auth.py | 25 +++++++- maubot/management/api/client.py | 13 ++++ maubot/management/api/middleware.py | 9 +-- maubot/management/frontend/src/api.js | 9 ++- .../frontend/src/components/Switch.js | 5 +- maubot/management/frontend/src/pages/Main.js | 2 +- .../frontend/src/pages/dashboard/Client.js | 62 ++++++++----------- .../frontend/src/style/base/body.sass | 28 --------- .../frontend/src/style/base/elements.sass | 2 +- .../frontend/src/style/lib/switch.sass | 9 +-- .../src/style/pages/client/instances.sass | 1 - .../frontend/src/style/pages/dashboard.sass | 1 + .../frontend/src/style/pages/sidebar.sass | 3 +- maubot/server.py | 13 +++- 14 files changed, 92 insertions(+), 90 deletions(-) diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index 34303d1..e754809 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -13,6 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional from time import time import json @@ -39,13 +40,31 @@ def create_token(user: UserID) -> str: }) -@routes.post("/auth/ping") -async def ping(request: web.Request) -> web.Response: +def get_token(request: web.Request) -> str: token = request.headers.get("Authorization", "") if not token or not token.startswith("Bearer "): + token = request.query.get("access_token", None) + else: + token = token[len("Bearer "):] + return token + + +def check_token(request: web.Request) -> Optional[web.Response]: + token = get_token(request) + if not token: + return resp.no_token + elif not is_valid_token(token): + return resp.invalid_token + return None + + +@routes.post("/auth/ping") +async def ping(request: web.Request) -> web.Response: + token = get_token(request) + if not token: return resp.no_token - data = verify_token(get_config()["server.unshared_secret"], token[len("Bearer "):]) + data = verify_token(get_config()["server.unshared_secret"], token) if not data: return resp.invalid_token user = data.get("user_id", None) diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 5bee0af..2975581 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -83,6 +83,8 @@ async def _update_client(client: Client, data: dict) -> web.Response: return resp.bad_client_access_token except MatrixRequestError: return resp.bad_client_access_details + except MatrixConnectionError: + return resp.bad_client_connection_details except ValueError as e: return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):]) await client.update_avatar_url(data.get("avatar_url", None)) @@ -142,3 +144,14 @@ async def upload_avatar(request: web.Request) -> web.Response: "content_uri": await client.client.upload_media( content, request.headers.get("Content-Type", None)), }) + + +@routes.get("/client/{id}/avatar") +async def download_avatar(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return resp.client_not_found + if not client.avatar_url or client.avatar_url == "disable": + return web.Response() + return web.Response(body=await client.client.download_media(client.avatar_url)) diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 2fefbe8..79bc26b 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -19,7 +19,7 @@ import logging from aiohttp import web from .responses import resp -from .auth import is_valid_token +from .auth import check_token Handler = Callable[[web.Request], Awaitable[web.Response]] @@ -28,12 +28,7 @@ Handler = Callable[[web.Request], Awaitable[web.Response]] async def auth(request: web.Request, handler: Handler) -> web.Response: if "/auth/" in request.path: return await handler(request) - token = request.headers.get("Authorization", "") - if not token or not token.startswith("Bearer "): - return resp.no_token - if not is_valid_token(token[len("Bearer "):]): - return resp.invalid_token - return await handler(request) + return check_token(request) or await handler(request) log = logging.getLogger("maubot.server") diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index e9fc926..6342b9d 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const BASE_PATH = "/_matrix/maubot/v1" +export const BASE_PATH = "/_matrix/maubot/v1" export async function login(username, password) { const resp = await fetch(`${BASE_PATH}/auth/login`, { @@ -105,6 +105,10 @@ export async function uploadAvatar(id, data, mime) { return await resp.json() } +export function getAvatarURL(id) { + return `${BASE_PATH}/client/${id}/avatar?access_token=${localStorage.accessToken}` +} + export async function putClient(client) { const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { headers: getHeaders(), @@ -128,8 +132,9 @@ export async function deleteClient(id) { } export default { + BASE_PATH, login, ping, getInstances, getInstance, getPlugins, getPlugin, uploadPlugin, - getClients, getClient, uploadAvatar, putClient, deleteClient, + getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, } diff --git a/maubot/management/frontend/src/components/Switch.js b/maubot/management/frontend/src/components/Switch.js index 975b47c..6ef43fd 100644 --- a/maubot/management/frontend/src/components/Switch.js +++ b/maubot/management/frontend/src/components/Switch.js @@ -37,9 +37,12 @@ class Switch extends Component { } } + toggleKeyboard = evt => (evt.key === " " || evt.key === "Enter") && this.toggle() + render() { return ( -
+
{this.props.onText || "On"} diff --git a/maubot/management/frontend/src/pages/Main.js b/maubot/management/frontend/src/pages/Main.js index efb9ac3..b655085 100644 --- a/maubot/management/frontend/src/pages/Main.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -44,7 +44,7 @@ class Main extends Component { localStorage.username = username this.setState({ authed: true }) } else { - localStorage.accessToken = undefined + delete localStorage.accessToken } } catch (err) { console.error(err) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 1350a31..ef414a0 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -21,14 +21,6 @@ import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTab import Spinner from "../../components/Spinner" import api from "../../api" -function getAvatarURL(client) { - if (!client.avatar_url) { - return "" - } - const id = client.avatar_url.substr("mxc://".length) - return `${client.homeserver}/_matrix/media/r0/download/${id}` -} - const ClientListEntry = ({ client }) => { const classes = ["client", "entry"] if (!client.enabled) { @@ -38,7 +30,7 @@ const ClientListEntry = ({ client }) => { } return ( - {client.id.substr(1, + {client.displayname || client.id} @@ -153,8 +145,8 @@ class Client extends Component { startOrStop = async () => { this.setState({ startingOrStopping: true }) const resp = await api.putClient({ - id: this.state.id, - started: !this.state.started, + id: this.props.client.id, + started: !this.props.client.started, }) if (resp.id) { this.props.onChange(resp) @@ -172,11 +164,11 @@ class Client extends Component { return !Boolean(this.props.client) } - renderSidebar = () => ( + renderSidebar = () => !this.isNew && (
- Avatar + Avatar ) - renderInstances = () => ( + renderPrefButtons = () => <> +
+ {!this.isNew && ( + + )} + +
+
{this.state.error}
+ + + renderInstances = () => !this.isNew && (
-

Instances

+

{this.state.instances.length > 0 ? "Instances" : "No instances :("}

{this.state.instances.map(instance => ( {instance.id} @@ -241,28 +247,14 @@ class Client extends Component {
) - renderInfoContainer = () => ( -
- {this.renderPreferences()} -
- {!this.isNew && ( - - )} - -
-
{this.state.error}
- {this.renderInstances()} -
- ) - render() { return
- {!this.isNew && this.renderSidebar()} - {this.renderInfoContainer()} + {this.renderSidebar()} +
+ {this.renderPreferences()} + {this.renderPrefButtons()} + {this.renderInstances()} +
} } diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index ba0406b..c30dd67 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -34,34 +34,6 @@ body right: 0 background-color: $background-dark - > * - background-color: $background - .maubot-loading margin-top: 10rem width: 10rem - -//.lindeb - > header - position: absolute - top: 0 - height: $header-height - left: 0 - right: 0 - - > main - position: absolute - top: $header-height - bottom: 0 - left: 0 - right: 0 - - text-align: center - - > .lindeb-content - text-align: left - display: inline-block - width: 100% - max-width: $max-width - box-sizing: border-box - padding: 0 1rem diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 6270aba..99a6e5e 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -22,7 +22,7 @@ background-color: $background border: none border-radius: .25rem - color: $inverted-text-color + color: $text-color box-sizing: border-box font-size: 1rem diff --git a/maubot/management/frontend/src/style/lib/switch.sass b/maubot/management/frontend/src/style/lib/switch.sass index 570320c..ac18689 100644 --- a/maubot/management/frontend/src/style/lib/switch.sass +++ b/maubot/management/frontend/src/style/lib/switch.sass @@ -37,7 +37,7 @@ transition: .5s text-align: center - color: $inverted-text-color + color: $text-color border-radius: .15rem 0 0 .15rem background-color: $primary @@ -50,7 +50,7 @@ text-align: center vertical-align: middle - color: $inverted-text-color + color: $text-color font-size: 1rem user-select: none @@ -67,14 +67,9 @@ transform: translateX(100%) border-radius: 0 .15rem .15rem 0 - background-color: $primary .on display: inline .off display: none - - - - diff --git a/maubot/management/frontend/src/style/pages/client/instances.sass b/maubot/management/frontend/src/style/pages/client/instances.sass index 7d5fb3c..24c5407 100644 --- a/maubot/management/frontend/src/style/pages/client/instances.sass +++ b/maubot/management/frontend/src/style/pages/client/instances.sass @@ -32,5 +32,4 @@ border: 1px solid $primary &:hover - color: $inverted-text-color background-color: $primary diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 8ec9563..9eb8ecb 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -21,6 +21,7 @@ max-width: 60rem margin: auto box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5) + background-color: $background > a.title grid-area: title diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index 8015e1a..c086714 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -29,9 +29,8 @@ div.title h2 - margin: 0 display: inline-block - + margin: 0 0 .25rem 0 font-size: 1.25rem a diff --git a/maubot/server.py b/maubot/server.py index 501677a..5788d8a 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -13,16 +13,25 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from aiohttp import web import logging import asyncio +from aiohttp import web +from aiohttp.abc import AbstractAccessLogger + from mautrix.api import PathBuilder, Method from .config import Config from .__meta__ import __version__ +class AccessLogger(AbstractAccessLogger): + def log(self, request: web.Request, response: web.Response, time: int): + self.logger.info(f'{request.remote} "{request.method} {request.path} ' + f'{response.status} {response.body_length} ' + f'in {round(time, 4)}s"') + + class MaubotServer: log: logging.Logger = logging.getLogger("maubot.server") @@ -39,7 +48,7 @@ class MaubotServer: as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) - self.runner = web.AppRunner(self.app) + self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) def add_route(self, method: Method, path: PathBuilder, handler) -> None: self.app.router.add_route(method.value, str(path), handler) From 0a406ac071bfe74359190c5c43de0278aa215783 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 13:49:54 +0200 Subject: [PATCH 12/23] Change more things --- .../management/frontend/src/pages/dashboard/Client.js | 10 ++++++++-- .../management/frontend/src/style/base/elements.sass | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index ef414a0..f6d9ed9 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -222,10 +222,16 @@ class Client extends Component { ) + get hasInstances() { + return this.state.instances.length > 0 + } + renderPrefButtons = () => <>
{!this.isNew && ( - )} @@ -238,7 +244,7 @@ class Client extends Component { renderInstances = () => !this.isNew && (
-

{this.state.instances.length > 0 ? "Instances" : "No instances :("}

+

{this.hasInstances ? "Instances" : "No instances :("}

{this.state.instances.map(instance => ( {instance.id} diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 99a6e5e..91a20c2 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -26,6 +26,9 @@ box-sizing: border-box font-size: 1rem + &.disabled-bg + background-color: $background-dark + &:not(:disabled) cursor: pointer From b0d782906b30064c8e69ea45360387a4d4b1808d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 16:33:12 +0200 Subject: [PATCH 13/23] Implement instance view --- maubot/instance.py | 17 ++ maubot/management/api/instance.py | 1 + maubot/management/frontend/package.json | 3 +- maubot/management/frontend/src/api.js | 101 ++++----- .../src/components/PreferenceTable.js | 7 + .../frontend/src/pages/dashboard/Client.js | 8 +- .../frontend/src/pages/dashboard/Instance.js | 157 ++++++++++++- .../frontend/src/pages/dashboard/index.js | 13 +- .../src/style/lib/preferencetable.sass | 4 + .../src/style/pages/client/index.sass | 23 -- .../frontend/src/style/pages/dashboard.sass | 23 ++ .../frontend/src/style/pages/instance.sass | 14 +- maubot/management/frontend/yarn.lock | 206 +++++++++++++++++- 13 files changed, 478 insertions(+), 99 deletions(-) diff --git a/maubot/instance.py b/maubot/instance.py index 00f8be7..c797466 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -186,10 +186,27 @@ class PluginInstance: self.db_instance.primary_user = client.id self.client.references.remove(self) self.client = client + self.client.references.add(self) await self.start() self.log.debug(f"Primary user switched to {self.client.id}") return True + async def update_type(self, type: str) -> bool: + if not type or type == self.type: + return True + try: + loader = PluginLoader.find(type) + except KeyError: + return False + await self.stop() + self.db_instance.type = loader.id + self.loader.references.remove(self) + self.loader = loader + self.loader.references.add(self) + await self.start() + self.log.debug(f"Type switched to {self.loader.id}") + return True + async def update_started(self, started: bool) -> None: if started is not None and started != self.started: await (self.start() if started else self.stop()) diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py index 57cf2f3..ad7f429 100644 --- a/maubot/management/api/instance.py +++ b/maubot/management/api/instance.py @@ -70,6 +70,7 @@ async def _update_instance(instance: PluginInstance, data: dict) -> web.Response instance.update_enabled(data.get("enabled", None)) instance.update_config(data.get("config", None)) await instance.update_started(data.get("started", None)) + await instance.update_type(data.get("type", None)) instance.db.commit() return resp.updated(instance.to_dict()) diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index 75765fa..7bd08ef 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -7,7 +7,8 @@ "react": "^16.6.0", "react-dom": "^16.6.0", "react-router-dom": "^4.3.1", - "react-scripts": "2.0.5" + "react-scripts": "2.0.5", + "react-select": "^2.1.1" }, "scripts": { "start": "react-scripts start", diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 6342b9d..e266383 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -16,6 +16,40 @@ export const BASE_PATH = "/_matrix/maubot/v1" +function getHeaders(contentType = "application/json") { + return { + "Content-Type": contentType, + "Authorization": `Bearer ${localStorage.accessToken}`, + } +} + +async function defaultDelete(type, id) { + const resp = await fetch(`${BASE_PATH}/${type}/${id}`, { + headers: getHeaders(), + method: "DELETE", + }) + if (resp.status === 204) { + return { + "success": true, + } + } + return await resp.json() +} + +async function defaultPut(type, entry, id = undefined) { + const resp = await fetch(`${BASE_PATH}/${type}/${id || entry.id}`, { + headers: getHeaders(), + body: JSON.stringify(entry), + method: "PUT", + }) + return await resp.json() +} + +async function defaultGet(url) { + const resp = await fetch(`${BASE_PATH}/${url}`, { headers: getHeaders() }) + return await resp.json() +} + export async function login(username, password) { const resp = await fetch(`${BASE_PATH}/auth/login`, { method: "POST", @@ -27,13 +61,6 @@ export async function login(username, password) { return await resp.json() } -function getHeaders(contentType = "application/json") { - return { - "Content-Type": contentType, - "Authorization": `Bearer ${localStorage.accessToken}`, - } -} - export async function ping() { const response = await fetch(`${BASE_PATH}/auth/ping`, { method: "POST", @@ -48,25 +75,13 @@ export async function ping() { throw json } -export async function getInstances() { - const resp = await fetch(`${BASE_PATH}/instances`, { headers: getHeaders() }) - return await resp.json() -} +export const getInstances = () => defaultGet("/instances") +export const getInstance = id => defaultGet(`/instance/${id}`) +export const putInstance = (instance, id) => defaultPut("instance", instance, id) +export const deleteInstance = id => defaultDelete("instance", id) -export async function getInstance(id) { - const resp = await fetch(`${BASE_PATH}/instance/${id}`, { headers: getHeaders() }) - return await resp.json() -} - -export async function getPlugins() { - const resp = await fetch(`${BASE_PATH}/plugins`, { headers: getHeaders() }) - return await resp.json() -} - -export async function getPlugin(id) { - const resp = await fetch(`${BASE_PATH}/plugin/${id}`, { headers: getHeaders() }) - return await resp.json() -} +export const getPlugins = () => defaultGet("/plugins") +export const getPlugin = id => defaultGet(`/plugin/${id}`) export async function uploadPlugin(data, id) { let resp @@ -86,15 +101,8 @@ export async function uploadPlugin(data, id) { return await resp.json() } -export async function getClients() { - const resp = await fetch(`${BASE_PATH}/clients`, { headers: getHeaders() }) - return await resp.json() -} - -export async function getClient(id) { - const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() }) - return await resp.json() -} +export const getClients = () => defaultGet("/clients") +export const getClient = id => defaultGet(`/clients/${id}`) export async function uploadAvatar(id, data, mime) { const resp = await fetch(`${BASE_PATH}/client/${id}/avatar`, { @@ -109,32 +117,13 @@ export function getAvatarURL(id) { return `${BASE_PATH}/client/${id}/avatar?access_token=${localStorage.accessToken}` } -export async function putClient(client) { - const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { - headers: getHeaders(), - body: JSON.stringify(client), - method: "PUT", - }) - return await resp.json() -} - -export async function deleteClient(id) { - const resp = await fetch(`${BASE_PATH}/client/${id}`, { - headers: getHeaders(), - method: "DELETE", - }) - if (resp.status === 204) { - return { - "success": true, - } - } - return await resp.json() -} +export const putClient = client => defaultPut("client", client) +export const deleteClient = id => defaultDelete("client", id) export default { BASE_PATH, login, ping, - getInstances, getInstance, + getInstances, getInstance, putInstance, deleteInstance, getPlugins, getPlugin, uploadPlugin, getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, } diff --git a/maubot/management/frontend/src/components/PreferenceTable.js b/maubot/management/frontend/src/components/PreferenceTable.js index 28ddfab..b58c55b 100644 --- a/maubot/management/frontend/src/components/PreferenceTable.js +++ b/maubot/management/frontend/src/components/PreferenceTable.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React from "react" +import Select from "react-select" import Switch from "./Switch" export const PrefTable = ({ children, wrapperClass }) => { @@ -52,4 +53,10 @@ export const PrefSwitch = ({ rowName, ...args }) => ( ) +export const PrefSelect = ({ rowName, ...args }) => ( + + +export const PrefInput = ({ rowName, fullWidth = false, ...args }) => ( + + ) -export const PrefSwitch = ({ rowName, ...args }) => ( - - +export const PrefSwitch = ({ rowName, fullWidth = false, ...args }) => ( + + ) -export const PrefSelect = ({ rowName, ...args }) => ( - - ) diff --git a/maubot/management/frontend/src/components/Switch.js b/maubot/management/frontend/src/components/Switch.js index 6ef43fd..a063abe 100644 --- a/maubot/management/frontend/src/components/Switch.js +++ b/maubot/management/frontend/src/components/Switch.js @@ -42,7 +42,7 @@ class Switch extends Component { render() { return (
+ tabIndex="0" onKeyPress={this.toggleKeyboard} id={this.props.id}>
{this.props.onText || "On"} diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index dba8eb2..24e91f2 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -193,18 +193,18 @@ class Client extends Component { renderPreferences = () => ( - - + diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 67bcf16..f983938 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -63,8 +63,8 @@ class Instance extends Component { } componentWillReceiveProps(nextProps) { - this.setState(Object.assign(this.initialState, nextProps.instance), () => - this.updateClientOptions()) + this.setState(Object.assign(this.initialState, nextProps.instance)) + this.updateClientOptions() } clientSelectEntry = client => client && { @@ -127,7 +127,9 @@ class Instance extends Component { } get selectedClientEntry() { - return this.clientSelectEntry(this.props.ctx.clients[this.state.primary_user]) + return this.state.primary_user + ? this.clientSelectEntry(this.props.ctx.clients[this.state.primary_user]) + : {} } get selectedPluginEntry() { @@ -159,7 +161,7 @@ class Instance extends Component { + disabled={!this.isNew} fullWidth={true}/> this.setState({ enabled })}/> Maubot Manager -
-
- {localStorage.username} -
+ +
+ {localStorage.username}
@@ -128,12 +133,12 @@ class Dashboard extends Component { this.add("plugins", newEntry)}/>}/> - this.renderView("instance", Instance, match.params.id)}/> + this.renderView("instances", Instance, match.params.id)}/> - this.renderView("client", Client, match.params.id)}/> + this.renderView("clients", Client, match.params.id)}/> this.renderView("plugin", Plugin, match.params.id)}/> - "Not found :("}/> + this.renderNotFound()}/>
diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index c5fe3e9..56269f2 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -69,6 +69,11 @@ @import instance @import plugin + > .not-found + text-align: center + margin-top: 5rem + font-size: 1.5rem + div.buttons +button-group display: flex From 9603f59b96f68b973bb4c793a037a3b932e42955 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 22:16:34 +0200 Subject: [PATCH 18/23] Fix bugs and improve minor UI things --- .../frontend/src/pages/dashboard/Client.js | 2 +- .../frontend/src/pages/dashboard/Instance.js | 4 +-- .../frontend/src/pages/dashboard/Plugin.js | 27 +++++++++---------- .../frontend/src/pages/dashboard/index.js | 2 +- .../src/style/lib/preferencetable.sass | 4 +++ 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 04d195f..0424911 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -157,7 +157,7 @@ class Client extends BaseMainView { renderPreferences = () => ( - + disabled={!this.isNew} fullWidth={true} className="id"/> this.setState({ enabled })}/> + {!this.isNew && + + + }
evt.target.parentElement.classList.remove("drag")}/> {this.state.uploading && }
- {!this.isNew && <> - - - - -
- -
- } + {!this.isNew &&
+ +
}
{this.state.error}
{!this.isNew && this.renderInstances()}
diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index f356db9..ed72dc7 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -137,7 +137,7 @@ class Dashboard extends Component { this.renderView("clients", Client, match.params.id)}/> - this.renderView("plugin", Plugin, match.params.id)}/> + this.renderView("plugins", Plugin, match.params.id)}/> this.renderNotFound()}/>
diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass index 66fcad7..64c2124 100644 --- a/maubot/management/frontend/src/style/lib/preferencetable.sass +++ b/maubot/management/frontend/src/style/lib/preferencetable.sass @@ -62,6 +62,10 @@ border-bottom: 1px solid $background + &.id:disabled + font-family: "Fira Code", monospace + font-weight: bold + &:not(:disabled) border-bottom: 1px dotted $primary From 3a36b862df59ae272cb46a071c6568305baded3d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 23:29:31 +0200 Subject: [PATCH 19/23] Add mobile compatibility to management UI --- .../frontend/src/pages/dashboard/index.js | 23 +++++- .../src/style/lib/preferencetable.sass | 3 + .../src/style/pages/client/avatar.sass | 3 + .../src/style/pages/client/index.sass | 8 +- .../src/style/pages/dashboard-grid.css | 19 +++++ .../frontend/src/style/pages/dashboard.sass | 14 ++++ .../frontend/src/style/pages/instance.sass | 2 - .../frontend/src/style/pages/login.sass | 9 ++- .../frontend/src/style/pages/plugin.sass | 2 - .../frontend/src/style/pages/sidebar.sass | 2 +- .../frontend/src/style/pages/topbar.sass | 74 +++++++++++++++++++ 11 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 maubot/management/frontend/src/style/pages/topbar.sass diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index ed72dc7..567c443 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { Route, Switch, Link } from "react-router-dom" +import { Route, Switch, Link, withRouter } from "react-router-dom" import api from "../../api" import { ReactComponent as Plus } from "../../res/plus.svg" import Instance from "./Instance" @@ -28,10 +28,17 @@ class Dashboard extends Component { instances: {}, clients: {}, plugins: {}, + sidebarOpen: false, } window.maubot = this } + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + this.setState({ sidebarOpen: false }) + } + } + async componentWillMount() { const [instanceList, clientList, pluginList] = await Promise.all([ api.getInstances(), api.getClients(), api.getPlugins()]) @@ -90,15 +97,15 @@ class Dashboard extends Component { ) render() { - return
+ return
Maubot Manager -
{localStorage.username}
+ + +
+
this.setState({ sidebarOpen: !this.state.sidebarOpen })}> + +
+
+
"Hello, World!"}/> @@ -145,4 +160,4 @@ class Dashboard extends Component { } } -export default Dashboard +export default withRouter(Dashboard) diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass index 64c2124..c72dff1 100644 --- a/maubot/management/frontend/src/style/lib/preferencetable.sass +++ b/maubot/management/frontend/src/style/lib/preferencetable.sass @@ -23,6 +23,9 @@ > .entry display: block + + @media screen and (max-width: 55rem) + width: calc(100% - 1rem) width: calc(50% - 1rem) margin: .5rem diff --git a/maubot/management/frontend/src/style/pages/client/avatar.sass b/maubot/management/frontend/src/style/pages/client/avatar.sass index 062a605..25e596a 100644 --- a/maubot/management/frontend/src/style/pages/client/avatar.sass +++ b/maubot/management/frontend/src/style/pages/client/avatar.sass @@ -21,6 +21,9 @@ height: 8rem border-radius: 50% + @media screen and (max-width: 40rem) + margin: 0 auto 1rem + > img.avatar position: absolute display: block diff --git a/maubot/management/frontend/src/style/pages/client/index.sass b/maubot/management/frontend/src/style/pages/client/index.sass index 9444513..3d004db 100644 --- a/maubot/management/frontend/src/style/pages/client/index.sass +++ b/maubot/management/frontend/src/style/pages/client/index.sass @@ -16,7 +16,6 @@ > div.client display: flex - margin: 2rem 4rem > div.sidebar vertical-align: top @@ -36,3 +35,10 @@ > div.instances +instancelist + + @media screen and (max-width: 40rem) + flex-wrap: wrap + + > div.sidebar, > div.info + width: 100% + margin-right: 0 diff --git a/maubot/management/frontend/src/style/pages/dashboard-grid.css b/maubot/management/frontend/src/style/pages/dashboard-grid.css index 88010db..516c7ff 100644 --- a/maubot/management/frontend/src/style/pages/dashboard-grid.css +++ b/maubot/management/frontend/src/style/pages/dashboard-grid.css @@ -5,3 +5,22 @@ [row3-start] "sidebar main" auto [row3-end] / 15rem auto; } + + +@media screen and (max-width: 35rem) { + .dashboard { + grid-template: + [row1-start] "topbar" 3.5rem [row1-end] + [row2-start] "main" auto [row2-end] + / auto; + } + + .dashboard.sidebar-open { + grid-template: + [row1-start] "title topbar" 3.5rem [row1-end] + [row2-start] "user main" 2.5rem [row2-end] + [row3-start] "sidebar main" auto [row3-end] + / 15rem 100%; + overflow-x: hidden; + } +} diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 56269f2..681728e 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -60,11 +60,19 @@ border-radius: .25rem @import sidebar + @import topbar + + @media screen and (max-width: 35rem) + &:not(.sidebar-open) + > nav.sidebar, > a.title, > div.user + display: none !important > main.view grid-area: main border-left: 1px solid $border-color + overflow-y: scroll + @import client/index @import instance @import plugin @@ -74,6 +82,12 @@ margin-top: 5rem font-size: 1.5rem + > div:not(.not-found) + margin: 2rem 4rem + + @media screen and (max-width: 50rem) + margin: 2rem 1rem + div.buttons +button-group display: flex diff --git a/maubot/management/frontend/src/style/pages/instance.sass b/maubot/management/frontend/src/style/pages/instance.sass index 607352c..18b63a0 100644 --- a/maubot/management/frontend/src/style/pages/instance.sass +++ b/maubot/management/frontend/src/style/pages/instance.sass @@ -15,8 +15,6 @@ // along with this program. If not, see . > div.instance - margin: 2rem 4rem - > div.preference-table .select-client display: flex diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass index 6447f54..c1be6a4 100644 --- a/maubot/management/frontend/src/style/pages/login.sass +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -28,6 +28,10 @@ border-radius: .25rem margin-top: 3rem + @media screen and (max-width: 27rem) + margin: 3rem 1rem 0 + width: calc(100% - 2rem) + h1 color: $primary margin: 3rem 0 @@ -35,13 +39,14 @@ input, button margin: .5rem 2.5rem height: 3rem - width: 20rem + width: calc(100% - 5rem) + box-sizing: border-box input +input button - +button($width: 20rem, $height: 3rem, $padding: 0) + +button($width: calc(100% - 5rem), $height: 3rem, $padding: 0) +main-color-button .spinner diff --git a/maubot/management/frontend/src/style/pages/plugin.sass b/maubot/management/frontend/src/style/pages/plugin.sass index b45d867..b9fed11 100644 --- a/maubot/management/frontend/src/style/pages/plugin.sass +++ b/maubot/management/frontend/src/style/pages/plugin.sass @@ -15,8 +15,6 @@ // along with this program. If not, see . > .plugin - margin: 2rem 4rem - > .upload-box +upload-box diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index 58078f4..497ba06 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -> .sidebar +> nav.sidebar grid-area: sidebar background-color: white diff --git a/maubot/management/frontend/src/style/pages/topbar.sass b/maubot/management/frontend/src/style/pages/topbar.sass new file mode 100644 index 0000000..e7d8312 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/topbar.sass @@ -0,0 +1,74 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.topbar + background-color: $primary + + display: flex + justify-items: center + align-items: center + padding: 0 .75rem + + @media screen and (min-width: calc(35rem + 1px)) + display: none + +// Hamburger menu based on "Pure CSS Hamburger fold-out menu" codepen by Erik Terwan (MIT license) +// https://codepen.io/erikterwan/pen/EVzeRP + +.hamburger + display: block + user-select: none + cursor: pointer + + > span + display: block + width: 29px + height: 4px + margin-bottom: 5px + position: relative + + background: white + border-radius: 3px + + z-index: 1 + + transform-origin: 4px 0 + + //transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), opacity 0.55s ease + + &:nth-of-type(1) + transform-origin: 0 0 + + &:nth-of-type(3) + transform-origin: 0 100% + + transform: translateY(2px) + + &.active + transform: translateX(1px) translateY(4px) + + &.active > span + opacity: 1 + + &:nth-of-type(1) + transform: rotate(45deg) translate(-2px, -1px) + + &:nth-of-type(2) + opacity: 0 + transform: rotate(0deg) scale(0.2, 0.2) + + &:nth-of-type(3) + transform: rotate(-45deg) translate(0, -1px) From 0264f7b79447d6b1b0f94a3b0a40669618c8cd5a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 23:46:37 +0200 Subject: [PATCH 20/23] Hide unnecessary scrollbars --- maubot/management/frontend/src/style/pages/dashboard.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 681728e..a5b82bf 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -71,7 +71,7 @@ grid-area: main border-left: 1px solid $border-color - overflow-y: scroll + overflow-y: auto @import client/index @import instance From e0306d29b5074a0d33c83e3e5b2a603f2becfdf6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 00:43:58 +0200 Subject: [PATCH 21/23] Make maubot http server serve frontend for production --- example-config.yaml | 5 ++ maubot/__main__.py | 7 +-- maubot/config.py | 3 ++ maubot/management/api/base.py | 8 ++++ maubot/management/frontend/package.json | 1 + maubot/management/frontend/src/pages/Main.js | 2 +- maubot/server.py | 48 +++++++++++++++++--- 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index b3987f1..1b02d67 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -24,6 +24,11 @@ server: port: 29316 # The base management API path. base_path: /_matrix/maubot/v1 + # The base path for the UI. + ui_base_path: /_matrix/maubot + # Override path from where to load UI resources. + # Set to false to using pkg_resources to find the path. + override_resource_path: false # The base appservice API path. Use / for legacy appservice API and /_matrix/app/v1 for v1. appservice_base_path: /_matrix/app/v1 # The shared secret to sign API access tokens. diff --git a/maubot/__main__.py b/maubot/__main__.py index 3929095..e970c50 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -26,7 +26,7 @@ from .server import MaubotServer from .client import Client, init as init_client_class from .loader.zip import init as init_zip_loader from .instance import init as init_plugin_instance_class -from .management.api import init as init_management +from .management.api import init as init_management_api from .__meta__ import __version__ parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.", @@ -52,8 +52,9 @@ init_zip_loader(config) db_session = init_db(config) clients = init_client_class(db_session, loop) plugins = init_plugin_instance_class(db_session, config, loop) -management_api = init_management(config, loop) -server = MaubotServer(config, management_api, loop) +management_api = init_management_api(config, loop) +server = MaubotServer(config, loop) +server.app.add_subapp(config["server.base_path"], management_api) for plugin in plugins: plugin.load() diff --git a/maubot/config.py b/maubot/config.py index cf39d00..ea8dd3c 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -38,6 +38,9 @@ class Config(BaseFileConfig): copy("server.hostname") copy("server.port") copy("server.listen") + copy("server.base_path") + copy("server.ui_base_path") + copy("server.override_resource_path") copy("server.appservice_base_path") shared_secret = self["server.unshared_secret"] if shared_secret is None or shared_secret == "generate": diff --git a/maubot/management/api/base.py b/maubot/management/api/base.py index d9c2077..4cf9636 100644 --- a/maubot/management/api/base.py +++ b/maubot/management/api/base.py @@ -15,6 +15,7 @@ # along with this program. If not, see . from aiohttp import web +from ...__meta__ import __version__ from ...config import Config routes: web.RouteTableDef = web.RouteTableDef() @@ -28,3 +29,10 @@ def set_config(config: Config) -> None: def get_config() -> Config: return _config + + +@routes.get("/version") +async def version(_: web.Request) -> web.Response: + return web.json_response({ + "version": __version__ + }) diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index c25113f..320679b 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -26,6 +26,7 @@ "last 2 ios_saf versions" ], "proxy": "http://localhost:29316", + "homepage": ".", "devDependencies": { "sass-lint": "^1.12.1", "sass-lint-auto-fix": "^0.15.0" diff --git a/maubot/management/frontend/src/pages/Main.js b/maubot/management/frontend/src/pages/Main.js index b655085..63f419c 100644 --- a/maubot/management/frontend/src/pages/Main.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { BrowserRouter as Router, Switch } from "react-router-dom" +import { HashRouter as Router, Switch } from "react-router-dom" import PrivateRoute from "../components/PrivateRoute" import Spinner from "../components/Spinner" import api from "../api" diff --git a/maubot/server.py b/maubot/server.py index 5788d8a..06f1c4d 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -18,6 +18,7 @@ import asyncio from aiohttp import web from aiohttp.abc import AbstractAccessLogger +import pkg_resources from mautrix.api import PathBuilder, Method @@ -35,21 +36,56 @@ class AccessLogger(AbstractAccessLogger): class MaubotServer: log: logging.Logger = logging.getLogger("maubot.server") - def __init__(self, config: Config, management: web.Application, - loop: asyncio.AbstractEventLoop) -> None: + def __init__(self, config: Config, loop: asyncio.AbstractEventLoop) -> None: self.loop = loop or asyncio.get_event_loop() self.app = web.Application(loop=self.loop) self.config = config - path = PathBuilder(config["server.base_path"]) - self.add_route(Method.GET, path.version, self.version) - self.app.add_subapp(config["server.base_path"], management) - as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) + self.setup_management_ui() + self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) + def setup_management_ui(self) -> None: + ui_base = self.config["server.ui_base_path"] + if ui_base == "/": + ui_base = "" + directory = (self.config["server.override_resource_path"] + or pkg_resources.resource_filename("maubot", "management/frontend/build")) + self.app.router.add_static(f"{ui_base}/static", f"{directory}/static") + self.setup_static_root_files(directory, ui_base) + + with open(f"{directory}/index.html", "r") as file: + index_html = file.read() + + @web.middleware + async def frontend_404_middleware(request, handler): + if hasattr(handler, "__self__") and isinstance(handler.__self__, web.StaticResource): + try: + return await handler(request) + except web.HTTPNotFound: + return web.Response(body=index_html, content_type="text/html") + return await handler(request) + + self.app.middlewares.append(frontend_404_middleware) + self.app.router.add_get(f"{ui_base}/", lambda _: web.Response(body=index_html, + content_type="text/html")) + self.app.router.add_get(ui_base, lambda _: web.HTTPFound(f"{ui_base}/")) + + def setup_static_root_files(self, directory: str, ui_base: str) -> None: + files = { + "asset-manifest.json": "application/json", + "manifest.json": "application/json", + "favicon.png": "image/png", + } + for file, mime in files.items(): + with open(f"{directory}/{file}", "rb") as stream: + data = stream.read() + self.app.router.add_get(f"{ui_base}/{file}", lambda _: web.Response(body=data, + content_type=mime)) + def add_route(self, method: Method, path: PathBuilder, handler) -> None: self.app.router.add_route(method.value, str(path), handler) From 3d88277fd24b0643ef82950b2e71d280608e950a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 00:52:28 +0200 Subject: [PATCH 22/23] Build frontend in dockerfile --- Dockerfile | 6 ++++++ docker/example-config.yaml | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/Dockerfile b/Dockerfile index 710b778..ae90a34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,15 @@ +FROM node:10 AS frontend-builder + +COPY ./maubot/management/frontend /frontend +RUN cd /frontend && yarn --prod && yarn build + FROM alpine:3.8 ENV UID=1337 \ GID=1337 COPY . /opt/maubot +COPY --from=frontend-builder /frontend/build /opt/maubot/frontend WORKDIR /opt/maubot RUN apk add --no-cache \ py3-aiohttp \ diff --git a/docker/example-config.yaml b/docker/example-config.yaml index 77f97f0..a273629 100644 --- a/docker/example-config.yaml +++ b/docker/example-config.yaml @@ -24,6 +24,11 @@ server: port: 29316 # The base management API path. base_path: /_matrix/maubot/v1 + # The base path for the UI. + ui_base_path: /_matrix/maubot + # Override path from where to load UI resources. + # Set to false to using pkg_resources to find the path. + override_resource_path: /opt/maubot/frontend # The base appservice API path. Use / for legacy appservice API and /_matrix/app/v1 for v1. appservice_base_path: /_matrix/app/v1 # The shared secret to sign API access tokens. From 640caa2f2e10a1a72555dc69b07e827b4e70fdd1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 00:58:08 +0200 Subject: [PATCH 23/23] Fix double slash in API paths --- maubot/management/frontend/src/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index bad6458..8d7d9b6 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -45,8 +45,8 @@ async function defaultPut(type, entry, id = undefined) { return await resp.json() } -async function defaultGet(url) { - const resp = await fetch(`${BASE_PATH}/${url}`, { headers: getHeaders() }) +async function defaultGet(path) { + const resp = await fetch(`${BASE_PATH}${path}`, { headers: getHeaders() }) return await resp.json() }