diff --git a/admin/.eslintrc.cjs b/admin/.eslintrc.cjs new file mode 100644 index 000000000..d6c953795 --- /dev/null +++ b/admin/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/admin/README.md b/admin/README.md new file mode 100644 index 000000000..0d6babedd --- /dev/null +++ b/admin/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 000000000..daff73e73 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,14 @@ + + + + + + + Vite + React + TS + + +
+
+ + + diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 000000000..ceb203f84 --- /dev/null +++ b/admin/package.json @@ -0,0 +1,33 @@ +{ + "name": "admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.0.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@types/react": "^18.2.56", + "@types/react-dom": "^18.2.19", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "socket.io-client": "^4.7.4", + "typescript": "^5.2.2", + "vite": "^5.1.4", + "vite-plugin-svgr": "^4.2.0" + } +} diff --git a/admin/public/fond.jpg b/admin/public/fond.jpg new file mode 100644 index 000000000..81357c7bb Binary files /dev/null and b/admin/public/fond.jpg differ diff --git a/admin/src/App.css b/admin/src/App.css new file mode 100644 index 000000000..e69de29bb diff --git a/admin/src/App.tsx b/admin/src/App.tsx new file mode 100644 index 000000000..89ab3c389 --- /dev/null +++ b/admin/src/App.tsx @@ -0,0 +1,80 @@ +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 {useStore} from "./store/store.ts"; +import {LoadingScreen} from "./utils/LoadingScreen.tsx"; + +export const App = ()=> { + const setSettings = useStore(state => state.setSettings); + + useEffect(() => { + useStore.getState().setShowLoading(true); + const settingSocket = connect('http://localhost:9001/settings', { + transports: ['websocket'], + }); + + const pluginsSocket = connect('http://localhost:9001/pluginfw/installer', { + transports: ['websocket'], + }) + + pluginsSocket.on('connect', () => { + useStore.getState().setPluginsSocket(pluginsSocket); + }); + + + settingSocket.on('connect', () => { + useStore.getState().setSettingsSocket(settingSocket); + settingSocket.emit('load'); + console.log('connected'); + }); + settingSocket.on('disconnect', (reason) => { + // The settingSocket.io client will automatically try to reconnect for all reasons other than "io + // server disconnect". + if (reason === 'io server disconnect') settingSocket.connect(); + }); + + settingSocket.on('settings', (settings) => { + /* Check whether the settings.json is authorized to be viewed */ + if (settings.results === 'NOT_ALLOWED') { + console.log('Not allowed to view settings.json') + return; + } + + /* Check to make sure the JSON is clean before proceeding */ + if (isJSONClean(settings.results)) { + setSettings(settings.results); + } else { + alert('Invalid JSON'); + } + useStore.getState().setShowLoading(false); + }); + + settingSocket.on('saveprogress', (status)=>{ + console.log(status) + }) + + return () => { + settingSocket.disconnect(); + pluginsSocket.disconnect() + } + }, []); + + return
+ +
+

Etherpad

+
    +
  • Home
  • +
  • Einstellungen
  • +
  • Hilfe
  • +
+
+
+ +
+
+} + +export default App diff --git a/admin/src/assets/react.svg b/admin/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/admin/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/src/index.css b/admin/src/index.css new file mode 100644 index 000000000..c084f1405 --- /dev/null +++ b/admin/src/index.css @@ -0,0 +1,359 @@ +:root { + --etherpad-color: #0f775b; +} + + + +html, body, #root { + box-sizing: border-box; + height: 100%; + +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body { + overflow: hidden; +} + +body { + margin: 0; + color: #333; + font: 14px helvetica, sans-serif; + background: #eee; +} + +div.menu { + height: 100%; + padding: 15px; + width: 220px; + border-right: 1px solid #ccc; + position: fixed; +} + +div.menu ul { + padding: 0; +} + +div.menu li { + 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.innerwrapper { + padding: 15px; + padding-left: 265px; +} + +div.innerwrapper-err { + padding: 15px; + padding-left: 265px; + display: none; +} + +#wrapper { + 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: 101%;/*always display a scrollbar*/ + +} + +h1 { + font-size: 29px; +} + +h2 { + font-size: 24px; +} + +.separator { + margin: 10px 0; + height: 1px; + background: #aaa; + background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); + background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff); +} + +form { + margin-bottom: 0; +} + +#inner { + width: 300px; + margin: 0 auto; +} + +input { + font-weight: bold; + font-size: 15px; +} + + +.sort { + cursor: pointer; +} +.sort:after { + content: '▲▼' +} +.sort.up:after { + content:'▲' +} +.sort.down:after { + content:'▼' +} + +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; +} + +td, th { + padding: 5px; +} + +.template { + display: none; +} + +#installed-plugins td>div { + position: relative;/* Allows us to position the loading indicator relative to this row */ + display: inline-block; /*make this fill the whole cell*/ + width:100%; +} + +.messages { + height: 5em; +} +.messages * { + display: none; + text-align: center; +} +.messages .fetching { + display: block; +} + +.progress { + position: absolute; + top: 0; left: 0; bottom:0; right:0; + padding: auto; + + background: rgb(255,255,255); + display: none; +} + +#search-progress.progress { + padding-top: 20%; + background: rgba(255,255,255,0.3); +} + +.progress * { + display: block; + margin: 0 auto; + text-align: center; + color: #666; +} + +.settings { + outline: none; + width: 100%; + min-height: 80vh; + resize: none; +} + +#response { + display: inline; +} + +a:link, a:visited, a:hover, a:focus { + color: #333333; + text-decoration: none; +} + +a:focus, a:hover { + text-decoration: underline; +} + +.installed-results a:link, +.search-results a:link, +.installed-results a:visited, +.search-results a:visited, +.installed-results a:hover, +.search-results a:hover, +.installed-results a:focus, +.search-results a:focus { + text-decoration: underline; +} + +.installed-results a:focus, +.search-results a:focus, +.installed-results a:hover, +.search-results a:hover { + text-decoration: none; +} + +pre { + white-space: pre-wrap; + word-wrap: break-word; +} + +@media (max-width: 800px) { + div.innerwrapper { + padding: 0 15px 15px 15px; + } + + div.menu { + padding: 1px 15px 0 15px; + position: static; + height: auto; + border-right: none; + width: auto; + } + + table { + border: none; + } + + table, thead, tbody, td, tr { + display: block; + } + + thead tr { + display: none; + } + + tr { + border: 1px solid #ccc; + margin-bottom: 5px; + border-radius: 3px; + } + + td { + border: none; + border-bottom: 1px solid #eee; + position: relative; + padding-left: 50%; + white-space: normal; + text-align: left; + } + + td.name { + word-wrap: break-word; + } + + td:before { + position: absolute; + top: 6px; + left: 6px; + text-align: left; + padding-right: 10px; + white-space: nowrap; + font-weight: bold; + content: attr(data-label); + } + + td:last-child { + border-bottom: none; + } + + table input[type="button"] { + float: none; + } +} + + +.settings-button-bar { + margin-top: 10px; + display: flex; + gap: 10px; +} + +.login-background { + background-image: url("/fond.jpg"); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f0f0f0; +} + + +.login-textinput { + width: 100%; + padding: 10px; + background-color: #fffacc; + border-radius: 5px; + border: 1px solid #ccc; + margin-bottom: 10px; +} + +.login-box { + width: 20%; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + background-color: #fff; +} + +.login-inner-box{ + position: relative; + padding: 20px; +} + +.login-title { + color: var(--etherpad-color); + font-size: 2em; +} + +.login-button { + padding: 10px; + background-color: var(--etherpad-color); + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + width: 100%; + height: 40px; +} + +.dialog-overlay { + position: fixed; + inset: 0; + background-color: white; + z-index: 100; +} + + +.dialog-content { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px; + z-index: 101; +} + +.dialog-title { + color: var(--etherpad-color); + font-size: 2em; + margin-bottom: 20px; +} diff --git a/admin/src/main.tsx b/admin/src/main.tsx new file mode 100644 index 000000000..9a9dc25a0 --- /dev/null +++ b/admin/src/main.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' +import {createBrowserRouter, createRoutesFromElements, Route, RouterProvider} from "react-router-dom"; +import {HomePage} from "./pages/HomePage.tsx"; +import {SettingsPage} from "./pages/SettingsPage.tsx"; +import {LoginScreen} from "./pages/LoginScreen.tsx"; +import {HelpPage} from "./pages/HelpPage.tsx"; + +const router = createBrowserRouter(createRoutesFromElements( + <>}> + }/> + }/> + }/> + }/> + + }/> + +)) + + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + , +) diff --git a/admin/src/pages/HelpPage.tsx b/admin/src/pages/HelpPage.tsx new file mode 100644 index 000000000..10e21277d --- /dev/null +++ b/admin/src/pages/HelpPage.tsx @@ -0,0 +1,5 @@ +export const HelpPage = () => { + return
+

Help Page

+
+} diff --git a/admin/src/pages/HomePage.tsx b/admin/src/pages/HomePage.tsx new file mode 100644 index 000000000..a8de85d77 --- /dev/null +++ b/admin/src/pages/HomePage.tsx @@ -0,0 +1,48 @@ +import {useStore} from "../store/store.ts"; +import {useEffect, useState} from "react"; +import {PluginDef} from "./Plugin.ts"; + +export const HomePage = () => { + const pluginsSocket = useStore(state=>state.pluginsSocket) + const [limit, setLimit] = useState(20) + const [offset, setOffset] = useState(0) + const [plugins,setPlugins] = useState([]) + + useEffect(() => { + pluginsSocket?.emit('search', { + searchTerm: '', + offset: offset, + limit: limit, + sortBy: 'name', + sortDir: 'asc' + }) + setOffset(offset+limit) + + pluginsSocket!.on('results:search', (data) => { + setPlugins(data.results) + }) + }, []); + + return
+

Home Page

+ + + + + + + + + + {plugins.map((plugin, index) => { + return + + + + + })} + +
NameDescriptionAction
{plugin.name}{plugin.description}test
+ +
+} diff --git a/admin/src/pages/LoginScreen.tsx b/admin/src/pages/LoginScreen.tsx new file mode 100644 index 000000000..2c7dc62b3 --- /dev/null +++ b/admin/src/pages/LoginScreen.tsx @@ -0,0 +1,33 @@ +import {useState} from "react"; + +export const LoginScreen = ()=>{ + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + + const login = ()=>{ + fetch('/api/auth', { + method: 'GET', + headers:{ + Authorization: `Basic ${btoa(`${username}:${password}`)}` + } + }).then(r=>{ + console.log(r.status) + }).catch(e=>{ + console.error(e) + }) + } + + return
+
+

Login Etherpad

+
+
Username
+ setUsername(v.target.value)} placeholder="Username"/> +
Passwort
+ setPassword(v.target.value)} placeholder="Password"/> + +
+
+
+} diff --git a/admin/src/pages/Plugin.ts b/admin/src/pages/Plugin.ts new file mode 100644 index 000000000..7cdb4a096 --- /dev/null +++ b/admin/src/pages/Plugin.ts @@ -0,0 +1,7 @@ +export type PluginDef = { + name: string, + description: string, + version: string, + time: string, + official: boolean, +} diff --git a/admin/src/pages/SettingsPage.tsx b/admin/src/pages/SettingsPage.tsx new file mode 100644 index 000000000..00da85079 --- /dev/null +++ b/admin/src/pages/SettingsPage.tsx @@ -0,0 +1,27 @@ +import {useStore} from "../store/store.ts"; +import {isJSONClean} from "../utils/utils.ts"; + +export const SettingsPage = ()=>{ + const settingsSocket = useStore(state=>state.settingsSocket) + + const settings = useStore(state=>state.settings) + return
+

Derzeitige Konfiguration

+