Begin redesigning admin panel. (#6219)

* Begin redesigning admin panel.

* Added monaco editor.

* Fixed tests
This commit is contained in:
SamTV12345 2024-03-13 15:47:02 +01:00 committed by GitHub
parent 4add6eb313
commit 73dff0bfe7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 252 additions and 56 deletions

View file

@ -6,6 +6,7 @@ import {NavLink, Outlet, useNavigate} from "react-router-dom";
import {useStore} from "./store/store.ts";
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
import {Trans, useTranslation} from "react-i18next";
import {Cable, Construction, Crown, NotepadText, Wrench} from "lucide-react";
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
export const App = ()=> {
@ -86,14 +87,19 @@ export const App = ()=> {
return <div id="wrapper">
<LoadingScreen/>
<div className="menu">
<h1>Etherpad</h1>
<ul>
<li><NavLink to="/plugins"><Trans i18nKey="admin_plugins"/></NavLink></li>
<li><NavLink to={"/settings"}><Trans i18nKey="admin_settings"/></NavLink></li>
<li> <NavLink to={"/help"}><Trans i18nKey="admin_plugins_info"/></NavLink></li>
<li><NavLink to={"/pads"}><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
</ul>
<div className="menu">
<div className="inner-menu">
<span>
<Crown width={40} height={40}/>
<h1>Etherpad</h1>
</span>
<ul>
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
<li> <NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
<li><NavLink to={"/pads"}><NotepadText/><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
</ul>
</div>
</div>
<div className="innerwrapper">
<Outlet/>

View file

@ -0,0 +1,16 @@
import {FC, ReactElement} from "react";
export type IconButtonProps = {
icon: JSX.Element,
title: string|ReactElement,
onClick: ()=>void,
className?: string,
disabled?: boolean
}
export const IconButton:FC<IconButtonProps> = ({icon,className,onClick,title, disabled})=>{
return <button onClick={onClick} className={"icon-button "+ className} disabled={disabled}>
{icon}
<span>{title}</span>
</button>
}

View file

@ -0,0 +1,14 @@
import {ChangeEventHandler, FC} from "react";
import {Search} from 'lucide-react'
export type SearchFieldProps = {
value: string,
onChange: ChangeEventHandler<HTMLInputElement>,
placeholder?: string
}
export const SearchField:FC<SearchFieldProps> = ({onChange,value, placeholder})=>{
return <span className="search-field">
<input value={value} onChange={onChange} placeholder={placeholder}/>
<Search/>
</span>
}

View file

@ -1,17 +1,23 @@
:root {
--etherpad-color: #0f775b;
--etherpad-comp: #9C8840;
--etherpad-light: #99FF99;
}
@font-face {
font-family: Karla;
src: url(/Karla-Regular.ttf);
}
html, body, #root {
box-sizing: border-box;
height: 100%;
font-family: "Karla", sans-serif;
}
*, *:before, *:after {
box-sizing: inherit;
font-size: 16px;
}
body {
@ -22,44 +28,111 @@ body {
}
div.menu {
height: 100%;
padding: 15px;
width: 220px;
border-right: 1px solid #ccc;
position: fixed;
height: 100vh;
font-size: 16px;
font-weight: bolder;
display: flex;
align-items: center;
justify-content: center;
max-width: 20%;
min-width: 20%;
}
.icon-button{
display: flex;
gap: 10px;
background-color: var(--etherpad-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
.icon-button svg {
align-self: center;
}
.icon-button span {
align-self: center;
}
div.menu span:first-child {
display: flex;
justify-content: center;
}
div.menu span:first-child svg {
margin-right: 10px;
align-self: center;
}
div.menu h1 {
font-size: 50px;
text-align: center;
}
.inner-menu {
border-radius: 0 20px 20px 0;
padding: 10px;
flex-grow: 100;
background-color: var(--etherpad-comp);
color: white;
height: 100vh;
}
div.menu ul {
color: white;
padding: 0;
}
div.menu li a {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
div.menu svg {
align-self: center;
}
div.menu li {
padding: 10px;
color: white;
list-style: none;
margin-left: 3px;
line-height: 3;
border-top: 1px solid #ccc;
}
div.menu li:last-child {
border-bottom: 1px solid #ccc;
div.menu li:has(.active) {
background-color: #9C885C ;
}
div.menu li a {
color: lightgray;
}
div.innerwrapper {
padding: 15px;
padding-left: 265px;
background-color: #F0F0F0;
overflow: auto;
height: 100vh;
flex-grow: 100;
padding: 20px;
}
div.innerwrapper-err {
padding: 15px;
padding-left: 265px;
display: none;
}
#wrapper {
display: flex;
background: none repeat scroll 0px 0px #FFFFFF;
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2);
margin: auto;
max-width: 1150px;
min-height: 100%;/*always display a scrollbar*/
}
@ -110,17 +183,25 @@ input {
content:'▼'
}
#installed-plugins thead tr th:nth-child(3) {
width: 15%;
}
table {
border: 1px solid #ddd;
border-radius: 3px;
border-spacing: 0;
width: 100%;
margin: 20px 0;
position:relative; /* Allows us to position the loading indicator relative to the table */
}
table thead tr {
background: #eee;
#available-plugins th:first-child, #available-plugins th:nth-child(2){
text-align: center;
}
td, th {
@ -223,6 +304,7 @@ pre {
height: auto;
border-right: none;
width: auto;
float: left;
}
table {
@ -484,6 +566,76 @@ pre {
}
.search-field {
width: 50%;
padding: 5px;
position: relative;
}
.search-field input {
border-color: transparent;
border-radius: 20px;
height: 2.5rem;
width: 100vh;
padding: 5px 5px 5px 30px;
}
.search-field input:focus {
outline: none;
}
.search-field svg {
position: absolute;
left: 3px;
bottom: -3px;
}
.search-field svg {
color: gray
}
table {
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
th:first-child {
border-top-left-radius: 10px;
}
th:last-child {
border-top-right-radius: 10px;
}
table thead tr {
font-size: 25px;
background-color: var(--etherpad-color);
color: #ffffff;
text-align: left;
}
table tbody tr {
border-bottom: 1px solid #dddddd;
}
table tr:nth-child(even) td{
background-color: lightgray;
}
table tr td {
padding: 12px 15px;
}
table tbody tr:nth-of-type(even) {
background-color: #f3f3f3;
}
table tbody tr:last-of-type {
border-bottom: 2px solid #009879;
}
table tbody tr.active-row {
font-weight: bold;
color: #009879;
}

View file

@ -3,6 +3,9 @@ import {useEffect, useMemo, useState} from "react";
import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts";
import {useDebounce} from "../utils/useDebounce.ts";
import {Trans, useTranslation} from "react-i18next";
import {SearchField} from "../components/SearchField.tsx";
import {Download, Trash} from "lucide-react";
import {IconButton} from "../components/IconButton.tsx";
export const HomePage = () => {
@ -128,12 +131,12 @@ export const HomePage = () => {
<h2><Trans i18nKey="admin_plugins.installed"/></h2>
<table>
<table id="installed-plugins">
<thead>
<tr>
<th><Trans i18nKey="admin_plugins.name"/></th>
<th><Trans i18nKey="admin_plugins.version"/></th>
<th></th>
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
</tr>
</thead>
<tbody style={{overflow: 'auto'}}>
@ -145,10 +148,7 @@ export const HomePage = () => {
{
plugin.updatable ?
<button onClick={() => installPlugin(plugin.name)}>Update</button>
: <button disabled={plugin.name == "ep_etherpad-lite"}
onClick={() => uninstallPlugin(plugin.name)}><Trans
i18nKey="admin_plugins.installed_uninstall.value"/></button>
: <IconButton disabled={plugin.name == "ep_etherpad-lite"} icon={<Trash/>} title={<Trans i18nKey="admin_plugins.installed_uninstall.value"/>} onClick={() => uninstallPlugin(plugin.name)}/>
}
</td>
</tr>
@ -158,19 +158,16 @@ export const HomePage = () => {
<h2><Trans i18nKey="admin_plugins.available"/></h2>
<SearchField onChange={v=>{setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/>
<input className="search-field" placeholder={t('admin_plugins.available_search.placeholder')} type="text" value={searchTerm} onChange={v=>{
setSearchTerm(v.target.value)
}}/>
<table>
<table id="available-plugins">
<thead>
<tr>
<th><Trans i18nKey="admin_plugins.name"/></th>
<th style={{width: '30%'}}><Trans i18nKey="admin_plugins.description"/></th>
<th><Trans i18nKey="admin_plugins.version"/></th>
<th><Trans i18nKey="admin_plugins.last-update"/></th>
<th></th>
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
</tr>
</thead>
<tbody style={{overflow: 'auto'}}>
@ -181,7 +178,7 @@ export const HomePage = () => {
<td>{plugin.version}</td>
<td>{plugin.time}</td>
<td>
<button onClick={() => installPlugin(plugin.name)}><Trans i18nKey="admin_plugins.available_install.value"/></button>
<IconButton icon={<Download/>} onClick={() => installPlugin(plugin.name)} title={<Trans i18nKey="admin_plugins.available_install.value"/>}/>
</td>
</tr>
})}

View file

@ -5,6 +5,9 @@ import {PadSearchQuery, PadSearchResult} from "../utils/PadSearch.ts";
import {useDebounce} from "../utils/useDebounce.ts";
import {determineSorting} from "../utils/sorting.ts";
import * as Dialog from "@radix-ui/react-dialog";
import {IconButton} from "../components/IconButton.tsx";
import {Trash2} from "lucide-react";
import {SearchField} from "../components/SearchField.tsx";
export const PadPage = ()=>{
const settingsSocket = useStore(state=>state.settingsSocket)
@ -98,8 +101,7 @@ export const PadPage = ()=>{
</Dialog.Portal>
</Dialog.Root>
<h1><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></h1>
<input type="text" value={searchTerm} onChange={v=>setSearchTerm(v.target.value)}
placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
<table>
<thead>
<tr>
@ -144,13 +146,11 @@ export const PadPage = ()=>{
<td style={{textAlign: 'center'}}>{pad.revisionNumber}</td>
<td>
<div className="settings-button-bar">
<button onClick={()=>{
<IconButton icon={<Trash2/>} title={<Trans i18nKey="ep_admin_pads:ep_adminpads2_delete.value"/>} onClick={()=>{
setPadToDelete(pad.padName)
setDeleteDialog(true)
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_delete.value"/></button>
<button onClick={()=>{
window.open(`/p/${pad.padName}`, '_blank')
}}>view</button>
}}/>
<IconButton icon={<Trash2/>} title="view" onClick={()=>window.open(`/p/${pad.padName}`, '_blank')}/>
</div>
</td>
</tr>

View file

@ -1,10 +1,11 @@
import {useStore} from "../store/store.ts";
import {isJSONClean} from "../utils/utils.ts";
import {Trans} from "react-i18next";
import {IconButton} from "../components/IconButton.tsx";
import {RotateCw, Save} from "lucide-react";
export const SettingsPage = ()=>{
const settingsSocket = useStore(state=>state.settingsSocket)
const settings = useStore(state=>state.settings)
return <div>
@ -13,7 +14,8 @@ export const SettingsPage = ()=>{
useStore.getState().setSettings(v.target.value)
}}/>
<div className="settings-button-bar">
<button className="settingsButton" onClick={() => {
<IconButton className="settingsButton" icon={<Save/>}
title={<Trans i18nKey="admin_settings.current_save.value"/>} onClick={() => {
if (isJSONClean(settings!)) {
// JSON is clean so emit it to the server
settingsSocket!.emit('saveSettings', settings!);
@ -29,16 +31,19 @@ export const SettingsPage = ()=>{
success: false
})
}
}}><Trans i18nKey="admin_settings.current_save.value"/></button>
<button className="settingsButton" onClick={() => {
}}/>
<IconButton className="settingsButton" icon={<RotateCw/>}
title={<Trans i18nKey="admin_settings.current_restart.value"/>} onClick={() => {
settingsSocket!.emit('restartServer');
}}><Trans i18nKey="admin_settings.current_restart.value"/></button>
}}/>
</div>
<div className="separator"/>
<div className="settings-button-bar">
<a rel="noopener noreferrer" target="_blank" href="https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON"><Trans
<a rel="noopener noreferrer" target="_blank"
href="https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON"><Trans
i18nKey="admin_settings.current_example-prod"/></a>
<a rel="noopener noreferrer" target="_blank" href="https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON"><Trans
<a rel="noopener noreferrer" target="_blank"
href="https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON"><Trans
i18nKey="admin_settings.current_example-devel"/></a>
</div>
</div>