mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-05 22:57:11 -04:00
Fixed docker build.
This commit is contained in:
parent
4d97c3c48f
commit
fa5aed489f
36 changed files with 243 additions and 967 deletions
|
@ -2,9 +2,9 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Etherpad Admin Dashboard</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
0
admin/pnpm-workspace.yaml
Normal file
0
admin/pnpm-workspace.yaml
Normal file
|
@ -2,26 +2,38 @@ import {useEffect} from 'react'
|
|||
import './App.css'
|
||||
import {connect} from 'socket.io-client'
|
||||
import {isJSONClean} from './utils/utils.ts'
|
||||
import {NavLink, Outlet} from "react-router-dom";
|
||||
import {NavLink, Outlet, useNavigate} from "react-router-dom";
|
||||
import {useStore} from "./store/store.ts";
|
||||
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
||||
import {ToastDialog} from "./utils/Toast.tsx";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
|
||||
export const App = ()=> {
|
||||
const setSettings = useStore(state => state.setSettings);
|
||||
const {t} = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/admin-auth/', {
|
||||
method: 'POST'
|
||||
}).then((value)=>{
|
||||
if(!value.ok){
|
||||
navigate('/login')
|
||||
}
|
||||
}).catch(()=>{
|
||||
navigate('/login')
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t('admin.page-title')
|
||||
|
||||
useStore.getState().setShowLoading(true);
|
||||
const settingSocket = connect('http://localhost:9001/settings', {
|
||||
const settingSocket = connect(`${WS_URL}/settings`, {
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
const pluginsSocket = connect('http://localhost:9001/pluginfw/installer', {
|
||||
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
|
||||
transports: ['websocket'],
|
||||
})
|
||||
|
||||
|
@ -74,7 +86,6 @@ export const App = ()=> {
|
|||
|
||||
return <div id="wrapper">
|
||||
<LoadingScreen/>
|
||||
<ToastDialog/>
|
||||
<div className="menu">
|
||||
<h1>Etherpad</h1>
|
||||
<ul>
|
||||
|
|
|
@ -339,6 +339,25 @@ pre {
|
|||
}
|
||||
|
||||
|
||||
.dialog-confirm-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
.dialog-confirm-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background-color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
|
||||
.dialog-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
|
@ -463,3 +482,8 @@ pre {
|
|||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px
|
||||
}
|
||||
|
||||
.search-field {
|
||||
width: 50%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
|
|
@ -28,17 +28,17 @@ const LazyImportPlugin: BackendModule = {
|
|||
try {
|
||||
json = JSON.parse(await localeJSON.text())
|
||||
} catch(e) {
|
||||
callback(true, null);
|
||||
callback(new Error("Error loading"), null);
|
||||
}
|
||||
|
||||
|
||||
callback(null, json);
|
||||
},
|
||||
|
||||
save: function (language, namespace, data) {
|
||||
save: function () {
|
||||
},
|
||||
|
||||
create: function (languages, namespace, key, fallbackValue) {
|
||||
create: function () {
|
||||
/* save the missing translation */
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import * as Toast from '@radix-ui/react-toast'
|
|||
import {I18nextProvider} from "react-i18next";
|
||||
import i18n from "./localization/i18n.ts";
|
||||
import {PadPage} from "./pages/PadPage.tsx";
|
||||
import {ToastDialog} from "./utils/Toast.tsx";
|
||||
|
||||
const router = createBrowserRouter(createRoutesFromElements(
|
||||
<><Route element={<App/>}>
|
||||
|
@ -31,7 +32,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||
<React.StrictMode>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Toast.Provider>
|
||||
<RouterProvider router={router}/>
|
||||
<ToastDialog/>
|
||||
<RouterProvider router={router}/>
|
||||
</Toast.Provider>
|
||||
</I18nextProvider>
|
||||
</React.StrictMode>,
|
||||
|
|
|
@ -10,7 +10,6 @@ export const HelpPage = () => {
|
|||
useEffect(() => {
|
||||
if(!settingsSocket) return;
|
||||
settingsSocket?.on('reply:help', (data) => {
|
||||
console.log(data)
|
||||
setHelpData(data)
|
||||
});
|
||||
|
||||
|
@ -19,11 +18,11 @@ export const HelpPage = () => {
|
|||
|
||||
const renderHooks = (hooks:Record<string, Record<string, string>>) => {
|
||||
return Object.keys(hooks).map((hookName, i) => {
|
||||
return <div key={i}>
|
||||
return <div key={hookName+i}>
|
||||
<h3>{hookName}</h3>
|
||||
<ul>
|
||||
{Object.keys(hooks[hookName]).map((hook, i) => <li>{hook}
|
||||
<ul key={i}>
|
||||
<ul key={hookName+hook+i}>
|
||||
{Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)}
|
||||
</ul>
|
||||
</li>)}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {useStore} from "../store/store.ts";
|
|||
import {useEffect, useState} from "react";
|
||||
import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts";
|
||||
import {useDebounce} from "../utils/useDebounce.ts";
|
||||
import {Trans} from "react-i18next";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
export const HomePage = () => {
|
||||
|
@ -17,6 +17,7 @@ export const HomePage = () => {
|
|||
searchTerm: ''
|
||||
})
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const {t} = useTranslation()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -30,8 +31,18 @@ export const HomePage = () => {
|
|||
setInstalledPlugins(data.installed)
|
||||
})
|
||||
|
||||
pluginsSocket.on('results:updatable', () => {
|
||||
console.log("Finished install")
|
||||
pluginsSocket.on('results:updatable', (data) => {
|
||||
data.updatable.forEach((pluginName: string) => {
|
||||
setInstalledPlugins(installedPlugins.map(plugin => {
|
||||
if (plugin.name === pluginName) {
|
||||
return {
|
||||
...plugin,
|
||||
updatable: true
|
||||
}
|
||||
}
|
||||
return plugin
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
pluginsSocket.on('finished:install', () => {
|
||||
|
@ -118,19 +129,25 @@ export const HomePage = () => {
|
|||
return <tr key={index}>
|
||||
<td>{plugin.name}</td>
|
||||
<td>{plugin.version}</td>
|
||||
<td onClick={() => {
|
||||
}}>
|
||||
<button disabled={plugin.name == "ep_etherpad-lite"} onClick={() => uninstallPlugin(plugin.name)}><Trans i18nKey="admin_plugins.installed_uninstall.value"/></button>
|
||||
<td>
|
||||
{
|
||||
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>
|
||||
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins.available"/></h2>
|
||||
<h2><Trans i18nKey="admin_plugins.available"/></h2>
|
||||
|
||||
<input type="text" value={searchTerm} onChange={v=>{
|
||||
<input className="search-field" placeholder={t('admin_plugins.available_search.placeholder')} type="text" value={searchTerm} onChange={v=>{
|
||||
setSearchTerm(v.target.value)
|
||||
}}/>
|
||||
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
import {useState} from "react";
|
||||
import {useStore} from "../store/store.ts";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export const LoginScreen = ()=>{
|
||||
const navigate = useNavigate()
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const login = ()=>{
|
||||
fetch('/api/auth', {
|
||||
method: 'GET',
|
||||
fetch('/admin-auth/', {
|
||||
method: 'POST',
|
||||
headers:{
|
||||
Authorization: `Basic ${btoa(`${username}:${password}`)}`
|
||||
}
|
||||
}).then(r=>{
|
||||
console.log(r.status)
|
||||
if(!r.ok) {
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Login failed",
|
||||
success: false
|
||||
})
|
||||
} else {
|
||||
navigate('/')
|
||||
}
|
||||
}).catch(e=>{
|
||||
console.error(e)
|
||||
})
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import {Trans} from "react-i18next";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useStore} from "../store/store.ts";
|
||||
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";
|
||||
|
||||
export const PadPage = ()=>{
|
||||
const settingsSocket = useStore(state=>state.settingsSocket)
|
||||
|
@ -14,7 +15,7 @@ export const PadPage = ()=>{
|
|||
sortBy: 'padName',
|
||||
ascending: true
|
||||
})
|
||||
|
||||
const {t} = useTranslation()
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const pads = useStore(state=>state.pads)
|
||||
const pages = useMemo(()=>{
|
||||
|
@ -24,7 +25,9 @@ export const PadPage = ()=>{
|
|||
|
||||
const totalPages = Math.ceil(pads!.total / searchParams.limit)
|
||||
return Array.from({length: totalPages}, (_, i) => i+1)
|
||||
},[pads])
|
||||
},[pads, searchParams.limit])
|
||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
|
||||
const [padToDelete, setPadToDelete] = useState<string>('')
|
||||
|
||||
useDebounce(()=>{
|
||||
setSearchParams({
|
||||
|
@ -64,11 +67,39 @@ export const PadPage = ()=>{
|
|||
})
|
||||
}, [settingsSocket, pads]);
|
||||
|
||||
const deletePad = (padID: string)=>{
|
||||
settingsSocket?.emit('deletePad', padID)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return <div>
|
||||
<Dialog.Root open={deleteDialog}><Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay" />
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<div className="">
|
||||
<div className=""></div>
|
||||
<div className="">
|
||||
{t("ep_admin_pads:ep_adminpads2_confirm", {
|
||||
padID: padToDelete,
|
||||
})}
|
||||
</div>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={()=>{
|
||||
setDeleteDialog(false)
|
||||
}}>Cancel</button>
|
||||
<button onClick={()=>{
|
||||
deletePad(padToDelete)
|
||||
setDeleteDialog(false)
|
||||
}}>Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</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="Pads suchen"/>
|
||||
<input type="text" value={searchTerm} onChange={v=>setSearchTerm(v.target.value)}
|
||||
placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -78,21 +109,21 @@ export const PadPage = ()=>{
|
|||
sortBy: 'padName',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}>PadId</th>
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'lastEdited')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'lastEdited',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}>Users</th>
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_pad-user-count"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'userCount')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'userCount',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}>Last Edited</th>
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_last-edited"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'revisionNumber')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
|
@ -100,7 +131,7 @@ export const PadPage = ()=>{
|
|||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}>Revision number</th>
|
||||
<th>Actions</th>
|
||||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -114,8 +145,9 @@ export const PadPage = ()=>{
|
|||
<td>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={()=>{
|
||||
settingsSocket?.emit('deletePad', pad.padName)
|
||||
}}>delete</button>
|
||||
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>
|
||||
|
|
|
@ -12,7 +12,7 @@ export type InstalledPlugin = {
|
|||
path: string,
|
||||
realPath: string,
|
||||
version:string,
|
||||
updatable: boolean
|
||||
updatable?: boolean
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {create} from "zustand";
|
||||
import {Socket} from "socket.io-client";
|
||||
import {PadSearchResult, PadType} from "../utils/PadSearch.ts";
|
||||
import {PadSearchResult} from "../utils/PadSearch.ts";
|
||||
|
||||
type ToastState = {
|
||||
description?:string,
|
||||
|
|
|
@ -14,14 +14,17 @@ export default defineConfig({
|
|||
]
|
||||
})],
|
||||
base: '/admin',
|
||||
build:{
|
||||
outDir: '../src/templates/admin'
|
||||
},
|
||||
server:{
|
||||
proxy: {
|
||||
'/socket.io/': {
|
||||
'/socket.io/*': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/api/auth': {
|
||||
'/admin-auth/': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue