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

@ -14,6 +14,7 @@
"@radix-ui/react-toast": "^1.1.5",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0",
"lucide-react": "^0.356.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",

BIN
admin/public/Karla-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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>

View file

@ -27,6 +27,9 @@
"dependencies": {
"ep_etherpad-lite": "workspace:./src"
},
"devDependencies": {
"admin": "workspace:./admin"
},
"engines": {
"node": ">=18.18.2",
"npm": ">=6.14.0",

View file

@ -1,2 +1,3 @@
packages:
- src
- admin

View file

@ -1,5 +1,6 @@
import {expect, test} from "@playwright/test";
import {loginToAdmin, restartEtherpad, saveSettings} from "../helper/adminhelper";
import exp from "node:constants";
test.beforeEach(async ({ page })=>{
await loginToAdmin(page, 'admin', 'changeme1');