Fixed docker build.

This commit is contained in:
SamTV12345 2024-03-09 22:32:04 +01:00
parent 4d97c3c48f
commit fa5aed489f
36 changed files with 243 additions and 967 deletions

View file

@ -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>

View file

View 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>

View file

@ -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;
}

View file

@ -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 */
},
};

View file

@ -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>,

View file

@ -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>)}

View file

@ -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)
}}/>

View file

@ -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)
})

View file

@ -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>

View file

@ -12,7 +12,7 @@ export type InstalledPlugin = {
path: string,
realPath: string,
version:string,
updatable: boolean
updatable?: boolean
}

View file

@ -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,

View file

@ -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/')