From 44fc901b4ba029d0e18badf17398517d18eb14bd Mon Sep 17 00:00:00 2001 From: Mickael Kerjean Date: Mon, 16 Aug 2021 02:09:24 +1000 Subject: [PATCH] maintain (admin): admin page upgrade --- client/components/animation.js | 26 ++-- client/components/index.js | 1 + client/components/textarea.js | 52 +++++++- client/index.js | 2 +- client/pages/adminpage.js | 184 ++++++++++++---------------- client/pages/adminpage/backend.js | 28 ++--- client/pages/adminpage/backend.scss | 2 +- client/pages/adminpage/home.js | 17 +-- client/pages/adminpage/logger.js | 159 ++++++++++++------------ client/pages/adminpage/loginpage.js | 78 +++++------- client/pages/adminpage/settings.js | 99 +++++++-------- client/pages/adminpage/setup.js | 29 ++--- client/router.js | 18 ++- 13 files changed, 346 insertions(+), 349 deletions(-) diff --git a/client/components/animation.js b/client/components/animation.js index 05c305ff..52d5f0dc 100644 --- a/client/components/animation.js +++ b/client/components/animation.js @@ -7,24 +7,20 @@ import React, { useState, useEffect } from "react"; * to realise it would be easier to write this simpler wrapper than migrate things over */ export function CSSTransition({ transitionName = "animate", children = null, transitionAppearTimeout = 300 }) { - const [child, setChildren] = useState(React.cloneElement(children, { - className: `${children.props.className} ${transitionName}-appear` - })); + const [className, setClassName] = useState(`${transitionName} ${transitionName}-appear`); useEffect(() => { - setChildren(React.cloneElement(child, { - className: `${children.props.className} ${transitionName}-appear ${transitionName}-appear-active` - })) - const timeout = setTimeout(() => { - setChildren(React.cloneElement(child, { - className: `${children.props.className}` - })) - }, transitionAppearTimeout); + setClassName(`${transitionName} ${transitionName}-appear ${transitionName}-appear-active`) - return () => { - clearTimeout(timeout); - }; + const timeout = setTimeout(() => { + setClassName(`${transitionName}`) + }, transitionAppearTimeout); + return () => clearTimeout(timeout); }, []); - return child; + return ( +
+ { children } +
+ ) } diff --git a/client/components/index.js b/client/components/index.js index f17023ee..430788cc 100644 --- a/client/components/index.js +++ b/client/components/index.js @@ -22,3 +22,4 @@ export { MapShot } from "./mapshot"; export { LoggedInOnly, ErrorPage, LoadingPage } from "./decorator"; export { FormBuilder } from "./formbuilder"; export { UploadQueue } from "./upload_queue"; +export { CSSTransition } from "./animation"; diff --git a/client/components/textarea.js b/client/components/textarea.js index f78c83a6..7881a3d8 100644 --- a/client/components/textarea.js +++ b/client/components/textarea.js @@ -1,9 +1,57 @@ -import React from "react"; +import React, { useRef, useState, useLayoutEffect } from "react"; import PropTypes from "prop-types"; import "./textarea.scss"; -export class Textarea extends React.Component { +export function Textarea({ ...props }) { + const $el = useRef(); + const [className, setClassName] = useState( + "component_textarea" + + (/Firefox/.test(navigator.userAgent) ? " firefox" : "") + + (props.value && props.value.length > 0 ? " hasText" : "") + ); + + useLayoutEffect(() => { + if($el.current && $el.current.value.length > 0 && className.indexOf("hasText") === -1){ + setClassName(`${className} hasText`) + } + }, []); + + const disabledEnter = (e) => { + if(e.key === "Enter" && e.shiftKey === false){ + e.preventDefault(); + const $form = getForm($el.current.ref); + if($form){ + $form.dispatchEvent(new Event("submit", { cancelable: true })); + } + } + + function getForm($el){ + if(!$el.parentElement) return $el; + if($el.parentElement.nodeName == "FORM"){ + return $el.parentElement; + } + return getForm($el.parentElement); + } + }; + const inputProps = (p) => { + return Object.keys(p).reduce((acc, key) => { + if(key === "disabledEnter") return acc; + acc[key] = p[key]; + return acc; + }, {}); + }; + + return ( + + ) +} +export class Textarea2 extends React.Component { constructor(props){ super(props); } diff --git a/client/index.js b/client/index.js index 1b99585a..e06fc75c 100644 --- a/client/index.js +++ b/client/index.js @@ -15,7 +15,7 @@ window.addEventListener("DOMContentLoaded", () => { const $loader = document.querySelector("#n-lder"); function render(){ - ReactDOM.render(, document.querySelector("div[role='main']")); + ReactDOM.render(, document.querySelector("div[role='main']")); return Promise.resolve(); }; function waitFor(n){ diff --git a/client/pages/adminpage.js b/client/pages/adminpage.js index 89b49ae9..e568a15c 100644 --- a/client/pages/adminpage.js +++ b/client/pages/adminpage.js @@ -1,118 +1,96 @@ -import React from 'react'; -import Path from 'path'; -import { Route, Switch, Link, NavLink } from 'react-router-dom'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; - -import './error.scss'; -import './adminpage.scss'; -import { Icon, LoadingPage } from '../components/'; -import { Config, Admin } from '../model'; -import { notify } from '../helpers/'; -import { HomePage, BackendPage, SettingsPage, LogPage, SetupPage, LoginPage } from './adminpage/'; -import { t } from '../locales/'; +import React, { useState, useEffect } from "react"; +import Path from "path"; +import { Route, Switch, Link, NavLink, useRouteMatch } from "react-router-dom"; +import "./error.scss"; +import "./adminpage.scss"; +import { Icon, LoadingPage, CSSTransition } from "../components/"; +import { Config, Admin } from "../model"; +import { notify } from "../helpers/"; +import { HomePage, BackendPage, SettingsPage, LogPage, SetupPage, LoginPage } from "./adminpage/"; +import { t } from "../locales/"; function AdminOnly(WrappedComponent){ - return class extends React.Component { - constructor(props){ - super(props); - this.state = { - isAdmin: null - }; - this.admin = () => { - Admin.isAdmin().then((t) => { - this.setState({isAdmin: t}); - }).catch((err) => { - notify.send("Error: " + (err && err.message) , "error"); - }); - }; - this.timeout = window.setInterval(this.admin.bind(this), 30 * 1000); + let initIsAdmin = null; + return function(props) { + const [isAdmin, setIsAdmin] = useState(initIsAdmin); + + const refresh = () => { + Admin.isAdmin().then((t) => { + initIsAdmin = t + setIsAdmin(t) + }).catch((err) => { + notify.send("Error: " + (err && err.message) , "error"); + }); + } + useEffect(() => { + refresh() + const timeout = window.setInterval(refresh, 5 * 1000); + return () => clearInterval(timeout); + }, []); + + if(isAdmin === true) { + return ( ); + } else if(isAdmin === false) { + return ( ); } - componentDidMount(){ - this.admin.call(this); - } - - componentWillUnmount(){ - window.clearInterval(this.timeout); - } - - render(){ - if(this.state.isAdmin === true){ - return ( ); - } else if(this.state.isAdmin === false) { - return ( this.admin()} /> ); - } - return ( ); - } - }; + return ( ); + } } -@AdminOnly -export class AdminPage extends React.Component { - constructor(props){ - super(props); - this.state = { - isAdmin: null, - isSaving: false - }; - } - - isSaving(yesOrNo){ - this.setState({isSaving: yesOrNo}); - } - - render(){ - return ( -
- -
- - - } /> - } /> - } /> - - - - -
+export default AdminOnly((props) => { + const match = useRouteMatch(); + const [isSaving, setIsSaving] = useState(false); + return ( +
+ +
+ + + } /> + } /> + } /> + + + +
- ); - } -} +
+ ); +}); -const SideMenu = (props) => { +function SideMenu(props) { return (
- { props.isLoading ? -
- - -
: - - - - - } -

{ t("Admin console") }

-
    -
  • - - { t("Backend") } + { props.isLoading ? +
    + + +
    : + + + -
  • -
  • - - { t("Settings") } - -
  • -
  • - - { t("Logs") } - -
  • -
+ } +

{ t("Admin console") }

+
    +
  • + + { t("Backend") } + +
  • +
  • + + { t("Settings") } + +
  • +
  • + + { t("Logs") } + +
  • +
); }; diff --git a/client/pages/adminpage/backend.js b/client/pages/adminpage/backend.js index c66e7ac5..55f374c1 100644 --- a/client/pages/adminpage/backend.js +++ b/client/pages/adminpage/backend.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React from "react"; import { FormBuilder, Icon, Input, Alert } from "../../components/"; import { Backend, Config } from "../../model/"; import { FormObjToJSON, notify, format, createFormBackend } from "../../helpers/"; -import { t } from '../../locales/'; +import { t } from "../../locales/"; import "./backend.scss"; @@ -26,7 +26,7 @@ export class BackendPage extends React.Component { let [backend, config] = data; this.setState({ backend_available: backend, - backend_enabled: window.CONFIG["connections"].map((conn) => { + backend_enabled: window.CONFIG["connections"].filter((b) => b).map((conn) => { return createFormBackend(backend, conn); }), config: config @@ -56,12 +56,12 @@ export class BackendPage extends React.Component { return Config.save(json, true, () => { this.props.isSaving(false); }, (err) => { - notify.send(err && err.message || t('Oops'), 'error'); + notify.send(err && err.message || t("Oops"), "error"); this.props.isSaving(false); }); } - addBackend(backend_id){ + addBackend(backend_id){ this.setState({ backend_enabled: this.state.backend_enabled.concat( createFormBackend(this.state.backend_available, { @@ -105,7 +105,7 @@ export class BackendPage extends React.Component { const isActiveBackend = (backend_key) => { return this.state.backend_enabled .map((b) => Object.keys(b)[0]) - .indexOf(backend_key) !== -1; + .indexOf(backend_key) !== -1; }; const isActiveAuth = (auth_key) => { @@ -134,7 +134,7 @@ export class BackendPage extends React.Component {

Authentication Middleware

- + Integrate Filestash with your identity management system @@ -163,8 +163,6 @@ export class BackendPage extends React.Component { Register your interest: mickael@kerjean.me - - ) } @@ -181,14 +179,14 @@ export class BackendPage extends React.Component {
- { let $checkbox = ( - onChange(update.bind(this, e.target.checked))}/> ); if(struct.label === "label"){ @@ -203,13 +201,13 @@ export class BackendPage extends React.Component { { $checkbox } { format(struct.label) }: -
+
{ $input }
-
+
{ struct.description ? (
{struct.description}
) : null } @@ -224,7 +222,7 @@ export class BackendPage extends React.Component { }
: You need to enable a backend first. - } + }
); } diff --git a/client/pages/adminpage/backend.scss b/client/pages/adminpage/backend.scss index 57a7fda5..ccb9b5c4 100644 --- a/client/pages/adminpage/backend.scss +++ b/client/pages/adminpage/backend.scss @@ -1,5 +1,4 @@ .component_dashboard{ - .alert { margin-top: -15px; } .box-container { display: flex; @@ -41,6 +40,7 @@ text-shadow: none; background: var(--emphasis-primary); padding: 18px 0; + @media (max-width: 750px){ padding: 8px 0; } margin: 6px; opacity: 0.95; .icon{ diff --git a/client/pages/adminpage/home.js b/client/pages/adminpage/home.js index 9d597250..d86a403a 100644 --- a/client/pages/adminpage/home.js +++ b/client/pages/adminpage/home.js @@ -1,15 +1,6 @@ -import React from 'react'; -import { Redirect } from 'react-router-dom'; +import React from "react"; +import { Redirect } from "react-router-dom"; -export class HomePage extends React.Component { - constructor(props){ - super(props); - this.state = { - stage: "loading" - } - } - - render(){ - return ( ); - } +export function HomePage() { + return ( ); } diff --git a/client/pages/adminpage/logger.js b/client/pages/adminpage/logger.js index 7751c8ed..a8430529 100644 --- a/client/pages/adminpage/logger.js +++ b/client/pages/adminpage/logger.js @@ -1,91 +1,86 @@ -import React from 'react'; -import { FormBuilder, Loader, Button, Icon } from '../../components/'; -import { Config, Log } from '../../model/'; -import { FormObjToJSON, notify, format } from '../../helpers/'; -import { t } from '../../locales/'; +import React, { useState, useEffect, useRef } from "react"; +import { FormBuilder, Loader, Button, Icon } from "../../components/"; +import { Config, Log } from "../../model/"; +import { FormObjToJSON, notify, format, nop } from "../../helpers/"; +import { t } from "../../locales/"; import "./logger.scss"; -export class LogPage extends React.Component { - constructor(props){ - super(props); - this.state = { - form: {}, - log: "", - config: {} - }; - } - - componentDidMount(){ - Config.all().then((config) => { - this.setState({ - form: {"":{"params":config["log"]}}, - config: FormObjToJSON(config) - }); - }); - Log.get(1024*100).then((log) => { // get only the last 100kb of log - this.setState({log: log}, () => { - this.refs.$log.scrollTop = this.refs.$log.scrollHeight; - }); - }); - } - - onChange(r){ - this.state.config["log"] = r[""].params; - this.state.config["connections"] = window.CONFIG.connections; - this.props.isSaving(true); - Config.save(this.state.config, true, () => { - this.props.isSaving(false); +export function LogPage({ isSaving = nop }) { + const [log, setLog] = useState(""); + const [form, setForm] = useState({}); + const [config, setConfig] = useState({}); + const $log = useRef(); + const filename = () => { + const t = new Date().toISOString().substring(0,10).replace(/-/g, ""); + return `access_${t}.log`; + }; + const onChange = (r) => { + const c = Object.assign({}, config) + c["log"] = r[""]["params"]; + c["connections"] = window.CONFIG.connections; + delete c["constant"] + isSaving(true); + Config.save(c, true, () => { + isSaving(false); }, () => { - notify.send(err && err.message || t('Oops'), 'error'); - this.props.isSaving(false); + isSaving(false); + notify.send(err && err.message || t("Oops"), "error"); }); - } + }; + const fetchLogs = () => { + Log.get(1024*100).then((log) => { // get only the last 100kb of log + setLog(log + "\n\n\n\n\n"); + if($log.current.scrollTop === 0) { + $log.current.scrollTop = $log.current.scrollHeight; + } + }); + }; - render(){ - const filename = () => { - let tmp = "access_"; - tmp += new Date().toISOString().substring(0,10).replace(/-/g, ""); - tmp += ".log"; - }; - return ( -
-

Logging

-
- { - return ( - - ); - }} /> -
+ useEffect(() => { + Config.all().then((config) => { + setForm({"":{"params":config["log"]}}); + setConfig(FormObjToJSON(config)); + }); + fetchLogs(); + const id = setInterval(fetchLogs, 5000); + return () => clearInterval(id) + }, []); -
-                {
-                    this.state.log === "" ?  : this.state.log + "\n\n\n\n\n"
-                }
-              
- + return ( +
+

Logging

+
+ ( + + )} />
- ); - } +
+                { log === "" ?  : log }
+            
+ +
+ ); } diff --git a/client/pages/adminpage/loginpage.js b/client/pages/adminpage/loginpage.js index c7589998..074e102c 100644 --- a/client/pages/adminpage/loginpage.js +++ b/client/pages/adminpage/loginpage.js @@ -1,54 +1,44 @@ -import React from 'react'; -import { Redirect } from 'react-router'; +import React, { useState, useRef } from "react"; +import { Redirect } from "react-router"; -import { Input, Button, Container, Icon, Loader } from '../../components/'; -import { Config, Admin } from '../../model/'; -import { t } from '../../locales/'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import { Input, Button, Container, Icon } from "../../components/"; +import { Admin } from "../../model/"; +import { nop } from "../../helpers/"; +import { t } from "../../locales/"; -export class LoginPage extends React.Component { - constructor(props){ - super(props); - this.state = { - loading: false, - error: null - }; - } - - componentDidMount(){ - this.refs.$input.ref.focus(); - } - - authenticate(e){ +export function LoginPage({ reload = nop }) { + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const $input = useRef(); + const marginTop = () => ({ marginTop: `${parseInt(window.innerHeight / 3)}px` }) + const authenticate = (e) => { e.preventDefault(); - this.setState({loading: true}); - Admin.login(this.refs.$input.ref.value) - .then(() => this.props.reload()) - .catch(() => { - this.refs.$input.ref.value = ""; - this.setState({ - loading: false, - error: true - }, () => { - window.setTimeout(() => { - this.setState({error: false}); - }, 500); - }); + setIsLoading(true); + Admin.login($input.current.ref.value) + .then(() => reload()) + .catch(() => { + $input.current.ref.value = ""; + setIsLoading(false) + setHasError(true); + setTimeout(() => { + setHasError(false); + }, 500); }); } - render(){ - const marginTop = () => { return {marginTop: parseInt(window.innerHeight / 3)+'px'};}; + useRef(() => { + $input.current.ref.focus(); + }, []); - return ( - -
- + + return ( + + + - -
- ); - } + +
+ ) } diff --git a/client/pages/adminpage/settings.js b/client/pages/adminpage/settings.js index 0e50ef02..ebfd7540 100644 --- a/client/pages/adminpage/settings.js +++ b/client/pages/adminpage/settings.js @@ -1,26 +1,12 @@ -import React from 'react'; -import { FormBuilder } from '../../components/'; -import { Config } from '../../model/'; -import { format, notify } from '../../helpers'; -import { t } from '../../locales/'; +import React, { useState, useEffect } from "react"; +import { FormBuilder } from "../../components/"; +import { Config } from "../../model/"; +import { format, notify, nop } from "../../helpers"; +import { t } from "../../locales/"; -export class SettingsPage extends React.Component { - constructor(props){ - super(props); - this.state = { - form: {} - }; - } - - componentDidMount(){ - Config.all().then((c) => { - delete c.constant; // The constant key contains read only global variable that are - // application wide truth => not editable from the admin area - this.setState({form: c}); - }); - } - - format(name){ +export function SettingsPage({ isSaving = nop }) { + const [form, setForm] = useState({}); + const format = (name) => { if(typeof name !== "string"){ return "N/A"; } @@ -34,45 +20,52 @@ export class SettingsPage extends React.Component { }) .join(" "); } - - onChange(form){ - form.connections = window.CONFIG.connections; - this.props.isSaving(true); - Config.save(form, true, () => { - this.props.isSaving(false); + const onChange = (_form) => { + _form.connections = window.CONFIG.connections; + delete _form.constant; + refresh(Math.random()) + isSaving(true) + Config.save(_form, true, () => { + isSaving(false) }, (err) => { - notify.send(err && err.message || t('Oops'), 'error'); - this.props.isSaving(false); + isSaving(false) + notify.send(err && err.message || t("Oops"), "error"); }); } + const [_, refresh] = useState(null); - render(){ - return ( -
- { - return ( -