mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 10:07:15 +08:00 
			
		
		
		
	maintain (admin): admin page upgrade
This commit is contained in:
		| @ -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 ( | ||||
|         <div className={className}> | ||||
|             { children } | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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 ( | ||||
|         <textarea | ||||
|             onKeyPress={disabledEnter} | ||||
|             {...inputProps(props)} | ||||
|             className={className} | ||||
|             ref={$el}> | ||||
|         </textarea> | ||||
|     ) | ||||
| } | ||||
| export class Textarea2 extends React.Component { | ||||
|     constructor(props){ | ||||
|         super(props); | ||||
|     } | ||||
|  | ||||
| @ -15,7 +15,7 @@ window.addEventListener("DOMContentLoaded", () => { | ||||
|     const $loader = document.querySelector("#n-lder"); | ||||
|  | ||||
|     function render(){ | ||||
|         ReactDOM.render(<Router/>, document.querySelector("div[role='main']")); | ||||
|         ReactDOM.render(<React.StrictMode><Router/></React.StrictMode>, document.querySelector("div[role='main']")); | ||||
|         return Promise.resolve(); | ||||
|     }; | ||||
|     function waitFor(n){ | ||||
|  | ||||
| @ -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 ( <WrappedComponent {...props} /> ); | ||||
|         } else if(isAdmin === false) { | ||||
|             return ( <LoginPage reload={refresh} /> ); | ||||
|         } | ||||
|  | ||||
|         componentDidMount(){ | ||||
|             this.admin.call(this); | ||||
|         } | ||||
|  | ||||
|         componentWillUnmount(){ | ||||
|             window.clearInterval(this.timeout); | ||||
|         } | ||||
|  | ||||
|         render(){ | ||||
|             if(this.state.isAdmin === true){ | ||||
|                 return ( <WrappedComponent {...this.props} /> ); | ||||
|             } else if(this.state.isAdmin === false) { | ||||
|                 return ( <LoginPage reload={() => this.admin()} /> ); | ||||
|             } | ||||
|             return ( <LoadingPage />); | ||||
|         } | ||||
|     }; | ||||
|         return ( <LoadingPage /> ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @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 ( | ||||
|             <div className="component_page_admin"> | ||||
|               <SideMenu url={this.props.match.url} isLoading={this.state.isSaving}/> | ||||
|               <div className="page_container scroll-y"> | ||||
|                 <ReactCSSTransitionGroup key={window.location.pathname} transitionName="adminpage" transitionLeave={true} transitionEnter={true} transitionLeaveTimeout={15000} transitionEnterTimeout={20000} transitionAppear={true} transitionAppearTimeout={20000}> | ||||
|                   <Switch> | ||||
|                     <Route path={this.props.match.url + "/backend"} render={()=><BackendPage isSaving={this.isSaving.bind(this)}/>} /> | ||||
|                     <Route path={this.props.match.url + "/settings"} render={()=><SettingsPage isSaving={this.isSaving.bind(this)}/>} /> | ||||
|                     <Route path={this.props.match.url + "/logs"} render={() =><LogPage isSaving={this.isSaving.bind(this)}/>} /> | ||||
|                     <Route path={this.props.match.url + "/setup"} component={SetupPage} /> | ||||
|                     <Route path={this.props.match.url} component={HomePage} /> | ||||
|                   </Switch> | ||||
|                 </ReactCSSTransitionGroup> | ||||
|               </div> | ||||
| export default AdminOnly((props) => { | ||||
|     const match = useRouteMatch(); | ||||
|     const [isSaving, setIsSaving] = useState(false); | ||||
|     return ( | ||||
|         <div className="component_page_admin"> | ||||
|             <SideMenu url={match.url} isLoading={isSaving}/> | ||||
|             <div className="page_container scroll-y"> | ||||
|                 <CSSTransition key={location.pathname} transitionName="adminpage" transitionAppearTimeout={30000}> | ||||
|                     <Switch> | ||||
|                         <Route path={match.url + "/backend"} render={()=><BackendPage isSaving={setIsSaving}/>} /> | ||||
|                         <Route path={match.url + "/settings"} render={()=><SettingsPage isSaving={setIsSaving}/>} /> | ||||
|                         <Route path={match.url + "/logs"} render={() =><LogPage isSaving={setIsSaving}/>} /> | ||||
|                         <Route path={match.url + "/setup"} component={SetupPage} /> | ||||
|                         <Route path={match.url} component={HomePage} /> | ||||
|                     </Switch> | ||||
|                 </CSSTransition> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|         </div> | ||||
|     ); | ||||
| }); | ||||
|  | ||||
| const SideMenu = (props) => { | ||||
| function SideMenu(props) { | ||||
|     return ( | ||||
|         <div className="component_menu_sidebar no-select"> | ||||
|           { props.isLoading ? | ||||
|             <div className="header"> | ||||
|               <Icon name="arrow_left" style={{"opacity": 0}}/> | ||||
|               <Icon name="loading" /> | ||||
|             </div> : | ||||
|             <NavLink to="/" className="header"> | ||||
|               <Icon name="arrow_left" /> | ||||
|               <img src="/assets/logo/android-chrome-512x512.png" /> | ||||
|             </NavLink> | ||||
|           } | ||||
|           <h2>{ t("Admin console") }</h2> | ||||
|           <ul> | ||||
|             <li> | ||||
|               <NavLink activeClassName="active" to={props.url + "/backend"}> | ||||
|                 { t("Backend") } | ||||
|             { props.isLoading ? | ||||
|               <div className="header"> | ||||
|                   <Icon name="arrow_left" style={{"opacity": 0}}/> | ||||
|                   <Icon name="loading" /> | ||||
|               </div> : | ||||
|               <NavLink to="/" className="header"> | ||||
|                   <Icon name="arrow_left" /> | ||||
|                   <img src="/assets/logo/android-chrome-512x512.png" /> | ||||
|               </NavLink> | ||||
|             </li> | ||||
|             <li> | ||||
|               <NavLink activeClassName="active" to={props.url + "/settings"}> | ||||
|                 { t("Settings") } | ||||
|               </NavLink> | ||||
|             </li> | ||||
|             <li> | ||||
|               <NavLink activeClassName="active" to={props.url + "/logs"}> | ||||
|                 { t("Logs") } | ||||
|               </NavLink> | ||||
|             </li> | ||||
|           </ul> | ||||
|             } | ||||
|             <h2>{ t("Admin console") }</h2> | ||||
|             <ul> | ||||
|                 <li> | ||||
|                     <NavLink activeClassName="active" to={props.url + "/backend"}> | ||||
|                         { t("Backend") } | ||||
|                     </NavLink> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <NavLink activeClassName="active" to={props.url + "/settings"}> | ||||
|                         { t("Settings") } | ||||
|                     </NavLink> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <NavLink activeClassName="active" to={props.url + "/logs"}> | ||||
|                         { t("Logs") } | ||||
|                     </NavLink> | ||||
|                 </li> | ||||
|             </ul> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -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 { | ||||
|               </div> | ||||
|  | ||||
|               <h2>Authentication Middleware</h2> | ||||
|                | ||||
|  | ||||
|               <Alert> | ||||
|                 Integrate Filestash with your identity management system | ||||
|               </Alert> | ||||
| @ -163,8 +163,6 @@ export class BackendPage extends React.Component { | ||||
|                         <Alert className="success"> | ||||
|                           <i><strong>Register your interest: <a href={`mailto:mickael@kerjean.me?Subject=Filestash - Authentication Middleware - ${this.state.auth_enabled}`}>mickael@kerjean.me</a></strong></i> | ||||
|                         </Alert> | ||||
|  | ||||
|                          | ||||
|                       </React.Fragment> | ||||
|                   ) | ||||
|               } | ||||
| @ -181,14 +179,14 @@ export class BackendPage extends React.Component { | ||||
|                                     <div className="icons no-select" onClick={this.removeBackend.bind(this, index)}> | ||||
|                                       <Icon name="delete" /> | ||||
|                                     </div> | ||||
|                                     <FormBuilder onChange={this.onChange.bind(this)}  | ||||
|                                                  idx={index}  | ||||
|                                     <FormBuilder onChange={this.onChange.bind(this)} | ||||
|                                                  idx={index} | ||||
|                                                  key={index} | ||||
|                                                  form={{"": backend_enable}} | ||||
|                                                  autoComplete="new-password" | ||||
|                                                  render={ ($input, props, struct, onChange) => { | ||||
|                                                      let $checkbox = ( | ||||
|                                                          <Input type="checkbox" style={{width: "inherit", marginRight: '6px', top: '6px'}} | ||||
|                                                          <Input type="checkbox" style={{width: "inherit", marginRight: "6px", top: "6px"}} | ||||
|                                                                 checked={enable(struct)} onChange={(e) => 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) }: | ||||
|                                                              </span> | ||||
|                                                              <div style={{width: '100%'}}> | ||||
|                                                              <div style={{width: "100%"}}> | ||||
|                                                                { $input } | ||||
|                                                              </div> | ||||
|                                                            </div> | ||||
|                                                            <div> | ||||
|                                                              <span className="nothing"></span> | ||||
|                                                              <div style={{width: '100%'}}> | ||||
|                                                              <div style={{width: "100%"}}> | ||||
|                                                                { | ||||
|                                                                    struct.description ? (<div className="description">{struct.description}</div>) : null | ||||
|                                                                } | ||||
| @ -224,7 +222,7 @@ export class BackendPage extends React.Component { | ||||
|                       } | ||||
|                     </form> | ||||
|                   </div> : <Alert>You need to enable a backend first.</Alert> | ||||
|               }  | ||||
|               } | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @ -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{ | ||||
|  | ||||
| @ -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 ( <Redirect to="/admin/backend" /> ); | ||||
|     } | ||||
| export function HomePage() { | ||||
|     return ( <Redirect to="/admin/backend" /> ); | ||||
| } | ||||
|  | ||||
| @ -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 ( | ||||
|             <div className="component_logpage"> | ||||
|               <h2>Logging</h2> | ||||
|               <div style={{minHeight: '150px'}}> | ||||
|                 <FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} | ||||
|                              render={ ($input, props, struct, onChange) => { | ||||
|                                  return ( | ||||
|                                      <label className={"no-select input_type_" + props.params["type"]}> | ||||
|                                        <div> | ||||
|                                          <span> | ||||
|                                            { format(struct.label) }: | ||||
|                                          </span> | ||||
|                                          <div style={{width: '100%'}}> | ||||
|                                            { $input } | ||||
|                                          </div> | ||||
|                                        </div> | ||||
|                                        <div> | ||||
|                                          <span className="nothing"></span> | ||||
|                                          <div style={{width: '100%'}}> | ||||
|                                            { | ||||
|                                                struct.description ? (<div className="description">{struct.description}</div>) : null | ||||
|                                            } | ||||
|                                          </div> | ||||
|                                        </div> | ||||
|                                      </label> | ||||
|                                  ); | ||||
|                                  }} /> | ||||
|               </div> | ||||
|     useEffect(() => { | ||||
|         Config.all().then((config) => { | ||||
|             setForm({"":{"params":config["log"]}}); | ||||
|             setConfig(FormObjToJSON(config)); | ||||
|         }); | ||||
|         fetchLogs(); | ||||
|         const id = setInterval(fetchLogs, 5000); | ||||
|         return () => clearInterval(id) | ||||
|     }, []); | ||||
|  | ||||
|               <pre style={{height: '350px'}} ref="$log"> | ||||
|                 { | ||||
|                     this.state.log === "" ? <Loader/> : this.state.log + "\n\n\n\n\n" | ||||
|                 } | ||||
|               </pre> | ||||
|               <div> | ||||
|                 <a href={Log.url()} download={filename()}><Button className="primary">{ t("Download") }</Button></a> | ||||
|               </div> | ||||
|     return ( | ||||
|         <div className="component_logpage"> | ||||
|             <h2>Logging</h2> | ||||
|             <div style={{minHeight: "150px"}}> | ||||
|                 <FormBuilder | ||||
|                     form={form} | ||||
|                     onChange={onChange} | ||||
|                     render={ ($input, props, struct, onChange) => ( | ||||
|                         <label className={"no-select input_type_" + props.params["type"]}> | ||||
|                             <div> | ||||
|                                 <span> | ||||
|                                     { format(struct.label) }: | ||||
|                                 </span> | ||||
|                                 <div style={{width: "100%"}}> | ||||
|                                     { $input } | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <div> | ||||
|                                 <span className="nothing"></span> | ||||
|                                 <div style={{width: "100%"}}> | ||||
|                                     { | ||||
|                                         struct.description ? (<div className="description">{struct.description}</div>) : null | ||||
|                                     } | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </label> | ||||
|                     )} /> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|             <pre style={{height: "350px"}} ref={$log}> | ||||
|                 { log === "" ? <Loader/> : log } | ||||
|             </pre> | ||||
|             <div> | ||||
|                 <a href={Log.url()} download={filename()}><Button className="primary">{ t("Download") }</Button></a> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| @ -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 ( | ||||
|             <Container maxWidth="300px" className="sharepage_component"> | ||||
|               <form className={this.state.error ? "error" : ""} onSubmit={this.authenticate.bind(this)} style={marginTop()}> | ||||
|                 <Input ref="$input" type="password" placeholder={ t("Password") } /> | ||||
|  | ||||
|     return ( | ||||
|         <Container maxWidth="300px" className="sharepage_component"> | ||||
|             <form className={hasError ? "error" : ""} onSubmit={authenticate} style={marginTop()}> | ||||
|                 <Input ref={$input} type="password" placeholder={ t("Password") } /> | ||||
|                 <Button theme="transparent"> | ||||
|                   <Icon name={this.state.loading ? "loading" : "arrow_right"}/> | ||||
|                     <Icon name={isLoading ? "loading" : "arrow_right"}/> | ||||
|                 </Button> | ||||
|               </form> | ||||
|             </Container> | ||||
|         ); | ||||
|     } | ||||
|             </form> | ||||
|         </Container> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @ -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 ( | ||||
|             <form className="sticky"> | ||||
|                 <FormBuilder form={this.state.form}  | ||||
|                              onChange={this.onChange.bind(this)}  | ||||
|                              autoComplete="new-password" | ||||
|                              render={ ($input, props, struct, onChange) => { | ||||
|                     return ( | ||||
|                         <label className={"no-select input_type_" + props.params["type"]}> | ||||
|                           <div> | ||||
|     useEffect(() => { | ||||
|         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 | ||||
|             setForm(c) | ||||
|         }); | ||||
|     }, []); | ||||
|  | ||||
|     return ( | ||||
|         <form className="sticky"> | ||||
|             <FormBuilder | ||||
|                 form={form} | ||||
|                 onChange={onChange} | ||||
|                 autoComplete="new-password" | ||||
|                 render={ ($input, props, struct, onChange) => ( | ||||
|                     <label className={"no-select input_type_" + props.params["type"]}> | ||||
|                         <div> | ||||
|                             <span> | ||||
|                               { format(struct.label) }: | ||||
|                                 { format(struct.label) }: | ||||
|                             </span> | ||||
|                             <div style={{width: '100%'}}> | ||||
|                               { $input } | ||||
|                             <div style={{width: "100%"}}> | ||||
|                                 { $input } | ||||
|                             </div> | ||||
|                           </div> | ||||
|                           <div> | ||||
|                         </div> | ||||
|                         <div> | ||||
|                             <span className="nothing"></span> | ||||
|                             <div style={{width: '100%'}}> | ||||
|                               { struct.description ? (<div className="description">{struct.description}</div>) : null } | ||||
|                             <div style={{width: "100%"}}> | ||||
|                                 { struct.description ? (<div className="description">{struct.description}</div>) : null } | ||||
|                             </div> | ||||
|                           </div> | ||||
|                         </label> | ||||
|                     ); | ||||
|                 }}/> | ||||
|                         </div> | ||||
|                     </label> | ||||
|                 )} /> | ||||
|             </form> | ||||
|         ); | ||||
|     } | ||||
|     ); | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import React from 'react'; | ||||
| import React, { createRef } from "react"; | ||||
|  | ||||
| import { Input, Button, Container, Icon, NgIf, Loader } from '../../components/'; | ||||
| import { Config, Admin } from '../../model/'; | ||||
| import { notify, FormObjToJSON, alert, prompt } from '../../helpers'; | ||||
| import { bcrypt_password } from '../../helpers/bcrypt'; | ||||
| import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; | ||||
| import { Input, Button, Container, Icon, NgIf, Loader, CSSTransition } from "../../components/"; | ||||
| import { Config, Admin } from "../../model/"; | ||||
| import { notify, FormObjToJSON, alert, prompt } from "../../helpers"; | ||||
| import { bcrypt_password } from "../../helpers/bcrypt"; | ||||
| //import ReactCSSTransitionGroup from "react-addons-css-transition-group"; | ||||
|  | ||||
| import "./setup.scss"; | ||||
|  | ||||
| @ -28,12 +28,12 @@ export class SetupPage extends React.Component { | ||||
|                 this.unlisten(); | ||||
|                 alert.now(( | ||||
|                     <div> | ||||
|                       <p style={{textAlign: 'justify'}}> | ||||
|                       <p style={{textAlign: "justify"}}> | ||||
|                         Help making this software better by sending crash reports and anonymous usage statistics | ||||
|                       </p> | ||||
|                       <form onSubmit={start.bind(this)} style={{fontSize: '0.9em', marginTop: '10px'}}> | ||||
|                       <form onSubmit={start.bind(this)} style={{fontSize: "0.9em", marginTop: "10px"}}> | ||||
|                         <label> | ||||
|                           <Input type="checkbox" style={{width: 'inherit', marginRight: '10px'}} onChange={(e) => this.enableLog(e.target.checked)} defaultChecked={config.log.telemetry.value} /> | ||||
|                           <Input type="checkbox" style={{width: "inherit", marginRight: "10px"}} onChange={(e) => this.enableLog(e.target.checked)} defaultChecked={config.log.telemetry.value} /> | ||||
|                           I accept but the data is not to be share with any third party | ||||
|                         </label> | ||||
|                       </form> | ||||
| @ -134,6 +134,7 @@ class MultiStepForm extends React.Component { | ||||
|             has_answered_password: false, | ||||
|             deps: [] | ||||
|         }; | ||||
|         this.$input = createRef() | ||||
|     } | ||||
|  | ||||
|     componentDidMount(){ | ||||
| @ -166,17 +167,17 @@ class MultiStepForm extends React.Component { | ||||
|                   <FormStage navleft={false} navright={this.state.has_answered_password === true} current={this.state.current} onStepChange={this.onStepChange.bind(this)}> | ||||
|                     Admin Password | ||||
|                   </FormStage> | ||||
|                   <ReactCSSTransitionGroup transitionName="stepper-form" transitionEnterTimeout={600} transitionAppearTimeout={600} transitionAppear={true} transitionEnter={true} transitionLeave={false}> | ||||
|                   <CSSTransition transitionName="stepper-form" transitionEnterTimeout={600} transitionAppearTimeout={600} transitionAppear={true} transitionEnter={true} transitionLeave={false}> | ||||
|                     <div key={this.state.current}> | ||||
|                       <p>Create your instance admin password: </p> | ||||
|                       <form onSubmit={this.onAdminPassword.bind(this)}> | ||||
|                         <Input ref="$input" type="password" placeholder="Password" value={this.state.answer_password} onChange={(e) => this.setState({answer_password: e.target.value})}/> | ||||
|                         <Input ref={this.$input} type="password" placeholder="Password" value={this.state.answer_password} onChange={(e) => this.setState({answer_password: e.target.value})}/> | ||||
|                         <Button theme="transparent"> | ||||
|                           <Icon name={this.props.loading ? "loading" : "arrow_right"}/> | ||||
|                         </Button> | ||||
|                       </form> | ||||
|                     </div> | ||||
|                   </ReactCSSTransitionGroup> | ||||
|                   </CSSTransition> | ||||
|                 {hideMenu} | ||||
|                 </div> | ||||
|             ); | ||||
| @ -186,7 +187,7 @@ class MultiStepForm extends React.Component { | ||||
|                   <FormStage navleft={true} navright={false} current={this.state.current} onStepChange={this.onStepChange.bind(this)}> | ||||
|                     Summary | ||||
|                   </FormStage> | ||||
|                   <ReactCSSTransitionGroup transitionName="stepper-form" transitionEnterTimeout={600} transitionAppearTimeout={600} transitionAppear={true} transitionEnter={true} transitionLeave={false}> | ||||
|                   <CSSTransition transitionName="stepper-form" transitionEnterTimeout={600} transitionAppearTimeout={600} transitionAppear={true} transitionEnter={true} transitionLeave={false}> | ||||
|                     <div key={this.state.current}> | ||||
|                       <NgIf cond={!!this.props.loading}> | ||||
|                         <Loader/> | ||||
| @ -204,7 +205,7 @@ class MultiStepForm extends React.Component { | ||||
|                         } | ||||
|                       </NgIf> | ||||
|                     </div> | ||||
|                   </ReactCSSTransitionGroup> | ||||
|                   </CSSTransition> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| import React from "react"; | ||||
| import React, { Suspense } from "react"; | ||||
| import { BrowserRouter, Route, Switch } from "react-router-dom"; | ||||
| import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from "./pages/"; | ||||
| import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT, URL_ADMIN, URL_SHARE } from  "./helpers/"; | ||||
| import { Bundle, ModalPrompt, ModalAlert, ModalConfirm, Notification, UploadQueue } from "./components/"; | ||||
| import { ModalPrompt, ModalAlert, ModalConfirm, Notification, UploadQueue, LoadingPage } from "./components/"; | ||||
|  | ||||
| const AdminPage = (props) => ( | ||||
|     <Bundle loader={import(/* webpackChunkName: "admin" */"./pages/adminpage")} symbol="AdminPage"> | ||||
|       {(Comp) => <Comp {...props}/>} | ||||
|     </Bundle> | ||||
|  | ||||
| const LazyAdminPage = React.lazy(() => import(/* webpackChunkName: "admin" */"./pages/adminpage")); | ||||
| const AdminPage = () => ( | ||||
|     <Suspense fallback={<LoadingPage/>}> | ||||
|         <LazyAdminPage/> | ||||
|     </Suspense> | ||||
| ); | ||||
|  | ||||
| export default class AppRouter extends React.Component { | ||||
| @ -26,8 +28,12 @@ export default class AppRouter extends React.Component { | ||||
|                   <Route component={NotFoundPage} /> | ||||
|                 </Switch> | ||||
|               </BrowserRouter> | ||||
|                 { | ||||
|                     /* | ||||
|               <ModalPrompt /> <ModalAlert /> <ModalConfirm /> | ||||
|               <Notification /> <UploadQueue/> | ||||
|                      */ | ||||
|                 } | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Mickael Kerjean
					Mickael Kerjean