diff --git a/.dockerignore b/.dockerignore index f7accabfd..e05914176 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,3 +25,5 @@ Dockerfile settings.json src/node_modules admin/node_modules +ui/node_modules +node_modules diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index baa786f9a..3c6e63bb4 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 21] + node: [20, 22, 23] steps: - name: Checkout repository @@ -53,7 +53,7 @@ jobs: run: pnpm config set auto-install-peers false - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 with: packages: libreoffice libreoffice-pdfimport version: 1.0 @@ -69,6 +69,9 @@ jobs: - name: Run the backend tests run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withpluginsLinux: # run on pushes to any branch @@ -81,7 +84,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 21] + node: [20, 22, 23] steps: - name: Checkout repository @@ -110,7 +113,7 @@ jobs: run: pnpm config set auto-install-peers false - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 with: packages: libreoffice libreoffice-pdfimport version: 1.0 @@ -142,6 +145,9 @@ jobs: - name: Run the backend tests run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withoutpluginsWindows: # run on pushes to any branch @@ -193,7 +199,11 @@ jobs: powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - name: Run the backend tests - run: cd src && pnpm test + working-directory: src + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest withpluginsWindows: # run on pushes to any branch @@ -211,7 +221,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 22 - uses: pnpm/action-setup@v4 name: Install pnpm with: @@ -273,4 +283,8 @@ jobs: powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - name: Run the backend tests - run: cd src && pnpm test + working-directory: src + run: pnpm test + - name: Run the new vitest tests + working-directory: src + run: pnpm run test:vitest diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d3e7d5621..313d48fc6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,6 +22,9 @@ jobs: - name: Check out uses: actions/checkout@v4 + with: + path: etherpad + - name: Set up QEMU if: github.event_name == 'push' @@ -31,9 +34,9 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build and export to Docker - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: - context: . + context: ./etherpad target: production load: true tags: ${{ env.TEST_TAG }} @@ -62,6 +65,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Test + working-directory: etherpad run: | docker run --rm -d -p 9001:9001 --name test ${{ env.TEST_TAG }} ./bin/installDeps.sh @@ -98,10 +102,11 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push + id: build-docker if: github.event_name == 'push' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: - context: . + context: ./etherpad target: production platforms: linux/amd64,linux/arm64 push: true @@ -111,7 +116,29 @@ jobs: uses: peter-evans/dockerhub-description@v4 if: github.ref == 'refs/heads/master' with: + readme-filepath: ./etherpad/README.md username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: etherpad/etherpad enable-url-completion: true + - name: Check out + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + uses: actions/checkout@v4 + with: + path: ether-charts + repository: ether/ether-charts + token: ${{ secrets.ETHER_CHART_TOKEN }} + - name: Update tag in values-dev.yaml + if: success() && github.ref == 'refs/heads/develop' + working-directory: ether-charts + run: | + sed -i 's/tag: ".*"/tag: "${{ steps.build-docker.outputs.digest }}"/' values-dev.yaml + - name: Commit and push changes + working-directory: ether-charts + if: success() && github.ref == 'refs/heads/develop' + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add values-dev.yaml + git commit -m 'Update develop image tag' + git push diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index c422b6450..4963fac24 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -11,14 +11,13 @@ permissions: jobs: withplugins: - if: ${{ github.actor != 'dependabot[bot]' }} name: with plugins runs-on: ubuntu-latest strategy: fail-fast: false matrix: - node: [20, 21] + node: [20, 22, 23] steps: - @@ -50,6 +49,13 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} - name: Only install direct dependencies run: pnpm config set auto-install-peers false #- @@ -69,7 +75,7 @@ jobs: # rules. - name: Install all dependencies and symlink for ep_etherpad-lite - run: bin/installDeps.sh + run: pnpm i #- # name: Install etherpad plugins # run: rm -Rf node_modules/ep_align/static/tests/* @@ -93,7 +99,6 @@ jobs: - name: Build admin frontend working-directory: admin run: | - pnpm install pnpm run build # name: Run the frontend admin tests # shell: bash @@ -125,7 +130,7 @@ jobs: - name: Run the frontend admin tests shell: bash run: | - pnpm run dev & + pnpm run prod & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index c1becbd3d..c11058b24 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 22 - uses: pnpm/action-setup@v4 name: Install pnpm with: @@ -56,10 +56,17 @@ jobs: - name: Create settings.json run: cp ./src/tests/settings.json settings.json + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} - name: Run the frontend tests shell: bash run: | - pnpm run dev & + pnpm run prod & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -92,7 +99,7 @@ jobs: uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 22 - uses: pnpm/action-setup@v4 name: Install pnpm with: @@ -119,10 +126,17 @@ jobs: run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" - name: Create settings.json run: cp ./src/tests/settings.json settings.json + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} - name: Run the frontend tests shell: bash run: | - pnpm run dev & + pnpm run prod & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -159,7 +173,14 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 22 + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} - uses: pnpm/action-setup@v4 name: Install pnpm with: @@ -192,7 +213,7 @@ jobs: - name: Run the frontend tests shell: bash run: | - pnpm run dev & + pnpm run prod & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml index 006399694..003a10000 100644 --- a/.github/workflows/rate-limit.yml +++ b/.github/workflows/rate-limit.yml @@ -48,7 +48,7 @@ jobs: - name: docker network - run: docker network create --subnet=172.23.42.0/16 ep_net + run: docker network create --subnet=172.23.0.0/16 ep_net - name: build docker image run: | diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index 96f289bd3..08421e7ec 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -24,13 +24,13 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 21] + node: [20, 22, 23] steps: - name: Check out latest release uses: actions/checkout@v4 with: - ref: master + ref: develop #FIXME change to master when doing release - uses: actions/setup-node@v4 with: @@ -43,7 +43,7 @@ jobs: - name: Only install direct dependencies run: pnpm config set auto-install-peers false - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 with: packages: libreoffice libreoffice-pdfimport version: 1.0 @@ -62,7 +62,7 @@ jobs: run: pnpm config set auto-install-peers false - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.4.2 + uses: awalsh128/cache-apt-pkgs-action@v1.5.0 with: packages: libreoffice libreoffice-pdfimport version: 1.0 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 57314545e..c0ce8b8bd 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -14,6 +14,7 @@ permissions: jobs: build-zip: + permissions: write-all # run on pushes to any branch # run on PRs from external forks if: | @@ -34,7 +35,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 22 - uses: pnpm/action-setup@v4 name: Install pnpm with: @@ -60,112 +61,26 @@ jobs: - name: Run the backend tests shell: msys2 {0} - run: cd src && pnpm test - - - name: Build the .zip - shell: msys2 {0} - run: bin/buildForWindows.sh - - - name: Archive production artifacts - uses: actions/upload-artifact@v4 - with: - name: etherpad-win.zip - path: etherpad-win.zip - - build-exe: - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - name: Build .exe - needs: build-zip - runs-on: windows-latest - steps: - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download .zip - uses: actions/download-artifact@v4 - with: - name: etherpad-win.zip - path: .. - - - name: Extract .zip - working-directory: .. - run: 7z x etherpad-win.zip -oetherpad-zip - - - name: Create installer - uses: joncloud/makensis-action@v4.1 - with: - script-file: 'bin/nsis/etherpad.nsi' - - - name: Archive production artifacts - uses: actions/upload-artifact@v4 - with: - name: etherpad-win.exe - path: etherpad-win.exe - - deploy-zip: - # run on pushes to any branch - # run on PRs from external forks - permissions: - contents: write - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - name: Deploy - needs: build-zip - runs-on: windows-latest - steps: - - - name: Download zip - uses: actions/download-artifact@v4 - with: - name: etherpad-win.zip - - - name: Extract Etherpad - run: 7z x etherpad-win.zip -oetherpad - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 9.0.4 - run_install: false - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - name: Only install direct dependencies - run: pnpm config set auto-install-peers false - - name: Install all dependencies and symlink for ep_etherpad-lite - run: .\bin\installOnWindows.bat - working-directory: etherpad + working-directory: src + run: pnpm test - name: Run Etherpad - working-directory: etherpad/src + working-directory: src run: | - pnpm install cypress - .\node_modules\.bin\cypress.cmd install --force + pnpm i + pnpm exec playwright install --with-deps pnpm run prod & curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test - pnpm exec cypress run --config-file ./tests/frontend/cypress/cypress.config.js - # On release, upload windows zip to GitHub release tab - - - name: Rename to etherpad-lite-win.zip - shell: powershell - run: mv etherpad-win.zip etherpad-lite-win.zip - - name: upload binaries to release + pnpm exec playwright install chromium --with-deps + pnpm run test-ui --project=chromium + # On release, create release + - name: Generate Changelog + if: ${{startsWith(github.ref, 'refs/tags/v') }} + working-directory: bin + run: pnpm run generateChangelog ${{ github.ref }} > ${{ github.workspace }}-CHANGELOG.txt + - name: Release uses: softprops/action-gh-release@v2 if: ${{startsWith(github.ref, 'refs/tags/v') }} with: - files: etherpad-lite-win.zip + body_path: ${{ github.workspace }}-CHANGELOG.txt + make_latest: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e7b6b565..e0a70e7dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,85 @@ +# 2.3.0 + +### Notable enhancements and fixes + +- Added possibility to cluster Etherpads behind reverse proxy. There is now a new reverse proxy designed for Etherpads that handles multiple Etherpads and the created pads in them. It will assign the pad assignement to an Etherpad at random but once the choice was made it will always reverse proxy the same backend. This allows to host multiple concurrent Etherpads and benefit from multi core systems even though one Etherpad is singlethreaded. +- Added reverse proxy configuration for replacing Nginx. In the past there were some issues with nginx and its configuration. This reverse proxy allows you to handle your configuration with ease. + +If you want to find out more about the reverse proxy method check out the repository https://github.com/ether/etherpad-proxy . It also contains a sample docker-compose file with three Etherpads and one etherpad-proxy. Of course you need to adapt the settings.json.template to your liking and map it into the reverse proxy image before you are ready :). + + +- Added client authorization to work with Etherpad. Before it would get blocked because it doesn't have the required claim. As this is now fixed etherpad-proxy can also work with your new OAuth2 configuration and retrieve a token via client credentials flow. + + + + +# 2.2.7 + + +### Notable enhancements and fixes + +- We migrated all important pages to React 19 and React Router v7 + +Besides that only dependency updates. + + + -> Have a merry Christmas and a happy new year. 🎄 🎁 + + +# 2.2.6 + +### Notable enhancements and fixes + +- Added option to delete a pad by the creator. This option can be found in the settings menu. When you click on it you get a confirm dialog and after that you have the chance to completely erase the pad. + + +# 2.2.5 + +### Notable enhancements and fixes + +- Fixed timeslider not scrolling when the revision count is a multiple of 100 +- Added new Restful API for version 2 of Etherpad. It is available at /api-docs + + +# 2.2.4 + +### Notable enhancements and fixes + +- Switched to new SQLite backend +- Fixed rusty-store-kv module not found + + +# 2.2.3 + +### Notable enhancements and fixes + +- Introduced a new in process database `rustydb` that represents a fast key value store written in Rust. +- Readded window._ as a shortcut for getting text +- Added support for migrating any ueberdb database to another. You can now switch as you please. See here: https://docs.etherpad.org/cli.html +- Further Typescript movements +- A lot of security issues fixed and reviewed in this release. Please update. + + +# 2.2.2 + +### Notable enhancements and fixes + +- Removal of Etherpad require kernel: We finally managed to include esbuild to bundle our frontend code together. So no matter how many plugins your server has it is always one JavaScript file. This boosts performance dramatically. +- Added log layoutType: This lets you print the log in either colored or basic (black and white text) +- Introduced esbuild for bundling CSS files +- Cache all files to be bundled in memory for faster load speed + + +# 2.1.1 + + +### Notable enhancements and fixes + +- Fixed failing Docker build when checked out as git submodule. Thanks to @neurolabs +- Fixed: Fallback to websocket and polling when unknown(old) config is present for socket io +- Fixed: Next page disabled if zero page by @samyakj023 +- On CTRL+CLICK bring the window back to focus by Helder Sepulveda + # 2.1.0 ### Notable enhancements and fixes diff --git a/Dockerfile b/Dockerfile index 2efca055b..eccecab90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,16 +3,17 @@ # https://github.com/ether/etherpad-lite # # Author: muxator +ARG BUILD_ENV=git -FROM node:alpine as adminBuild - +FROM node:alpine AS adminbuild +RUN npm install -g pnpm@latest WORKDIR /opt/etherpad-lite -COPY ./ ./ -RUN cd ./admin && npm install -g pnpm@9.0.4 && pnpm install && pnpm run build --outDir ./dist -RUN cd ./ui && pnpm install && pnpm run build --outDir ./dist +COPY . . +RUN pnpm install +RUN pnpm run build:ui -FROM node:alpine as build +FROM node:alpine AS build LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" # Set these arguments when building the image from behind a proxy @@ -49,6 +50,14 @@ ARG ETHERPAD_PLUGINS= # ETHERPAD_LOCAL_PLUGINS="../ep_my_plugin ../ep_another_plugin" ARG ETHERPAD_LOCAL_PLUGINS= +# github plugins to install while building the container. By default no plugins are +# installed. +# If given a value, it has to be a space-separated, quoted list of plugin names. +# +# EXAMPLE: +# ETHERPAD_GITHUB_PLUGINS="ether/ep_plugin" +ARG ETHERPAD_GITHUB_PLUGINS= + # Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats. # By default, it is not installed. # If given any value, abiword will be installed. @@ -91,7 +100,7 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}" # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199 RUN \ mkdir -p /usr/share/man/man1 && \ - npm install pnpm@9.0.4 -g && \ + npm install pnpm@latest -g && \ apk update && apk upgrade && \ apk add --no-cache \ ca-certificates \ @@ -105,39 +114,65 @@ USER etherpad WORKDIR "${EP_DIR}" # etherpads version feature requires this. Only copy what is really needed -COPY --chown=etherpad:etherpad ./.git/HEAD ./.git/HEAD -COPY --chown=etherpad:etherpad ./.git/refs ./.git/refs COPY --chown=etherpad:etherpad ${SETTINGS} ./settings.json COPY --chown=etherpad:etherpad ./var ./var COPY --chown=etherpad:etherpad ./bin ./bin COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./ -FROM build as development -COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/ -COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin -COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/ui/dist ./src/static/oidc + +FROM build AS build_git +ONBUILD COPY --chown=etherpad:etherpad ./.git/HEA[D] ./.git/HEAD +ONBUILD COPY --chown=etherpad:etherpad ./.git/ref[s] ./.git/refs + +FROM build AS build_copy + + + + +FROM build_${BUILD_ENV} AS development + +ARG ETHERPAD_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS_ENV= +ARG ETHERPAD_GITHUB_PLUGINS= + +COPY --chown=etherpad:etherpad ./src/ ./src/ +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/ templates/admin./src/templates/admin +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc + +COPY --chown=etherpad:etherpad ./local_plugin[s] ./local_plugins/ + +RUN bash -c ./bin/installLocalPlugins.sh RUN bin/installDeps.sh && \ - if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \ - pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \ - fi + if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \ + pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \ + fi -FROM build as production +FROM build_${BUILD_ENV} AS production + +ARG ETHERPAD_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS= +ARG ETHERPAD_LOCAL_PLUGINS_ENV= +ARG ETHERPAD_GITHUB_PLUGINS= ENV NODE_ENV=production ENV ETHERPAD_PRODUCTION=true COPY --chown=etherpad:etherpad ./src ./src -COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin -COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/ui/dist ./src/static/oidc +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/templates/admin ./src/templates/admin +COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc -RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \ - if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \ - pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \ - fi +COPY --chown=etherpad:etherpad ./local_plugin[s] ./local_plugins/ +RUN bash -c ./bin/installLocalPlugins.sh + +RUN bin/installDeps.sh && \ + if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \ + pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \ + fi # Copy the configuration file. COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json diff --git a/README.md b/README.md index da993b92d..32e183081 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ services: # ports: # - "5432:5432" volumes: - - postgres_data:/var/lib/postgresql/data/pgdata + - postgres_data:/var/lib/postgresql/data volumes: postgres_data: @@ -98,76 +98,19 @@ volumes: etherpad-var: ``` - - ### Requirements [Node.js](https://nodejs.org/) >= **18.18.2**. -### GNU/Linux and other UNIX-like systems +### Windows, macOS, Linux -#### Quick install on Debian/Ubuntu - -Install the latest Node.js LTS per [official install instructions](https://github.com/nodesource/distributions#installation-instructions), then: -```sh -git clone --branch master https://github.com/ether/etherpad-lite.git && -cd etherpad-lite && -bin/run.sh -``` - -#### Manual install - -You'll need Git and [Node.js](https://nodejs.org/) installed. - -**As any user (we recommend creating a separate user called etherpad):** - - 1. Move to a folder where you want to install Etherpad. - 2. Clone the Git repository: `git clone --branch master - https://github.com/ether/etherpad-lite.git` - 3. Change into the new directory containing the cloned source code: `cd - etherpad-lite` - 4. Run `bin/run.sh` and open http://127.0.0.1:9001 in your browser. - -To update to the latest released version, execute `git pull origin`. The next -start with `bin/run.sh` will update the dependencies. - -### Windows - -#### Prebuilt Windows package - -This package runs on any Windows machine. You can perform a manual installation -via git for development purposes, but as this uses symlinks which performs -unreliably on Windows, please stick to the prebuilt package if possible. - - 1. [Download the latest Windows package](https://etherpad.org/#download) - 2. Extract the folder - -Run `start.bat` and open in your browser. - -#### Manually install on Windows - -You'll need [Node.js](https://nodejs.org) and (optionally, though recommended) -git. - - 1. Grab the source, either: - * download - * or `git clone --branch master - https://github.com/ether/etherpad-lite.git` - 2. With a "Run as administrator" command prompt execute - `bin\installOnWindows.bat` - -Now, run `start.bat` and open http://localhost:9001 in your browser. - -Update to the latest version with `git pull origin`, then run -`bin\installOnWindows.bat`, again. - -If cloning to a subdirectory within another project, you may need to do the -following: - - 1. Start the server manually (e.g. `node src/node/server.ts`) - 2. Edit the db `filename` in `settings.json` to the relative directory with - the file (e.g. `application/lib/etherpad-lite/var/dirty.db`) - 3. Add auto-generated files to the main project `.gitignore` +1. Download the latest Node.js runtime from [nodejs.org](https://nodejs.org/). +2. Install pnpm: `npm install -g pnpm` (Administrator privileges may be required). +3. Clone the repository: `git clone -b master` +4. Run `pnpm i` +5. Run `pnpm run build:etherpad` +6. Run `pnpm run prod` +7. Visit `http://localhost:9001` in your browser. ### Docker container @@ -231,6 +174,31 @@ following plugins: that each user's chosen color, display name, comment ownership, etc. is strongly linked to their account. +### Upgrade Etherpad + +Run the following command in your Etherpad folder to upgrade + +1. Stop any running Etherpad (manual, systemd ...) +2. Get present version +```sh +git -P tag --contains +``` +3. List versions available +```sh +git -P tag --list "v*" --merged +``` +4. Select the version +```sh +git checkout v2.2.5 +git switch -c v2.2.5 +``` +5. Upgrade Etherpad +```sh +./bin/run.sh +``` +6. Stop with [CTRL-C] +7. Restart your Etherpad service + ## Next Steps ### Tweak the settings diff --git a/admin/package.json b/admin/package.json index dc22daff0..40d75e251 100644 --- a/admin/package.json +++ b/admin/package.json @@ -1,7 +1,7 @@ { "name": "admin", "private": true, - "version": "2.1.0", + "version": "2.3.0", "type": "module", "scripts": { "dev": "vite", @@ -11,32 +11,32 @@ "preview": "vite preview" }, "dependencies": { - "@radix-ui/react-switch": "^1.0.3" + "@radix-ui/react-switch": "^1.2.5" }, "devDependencies": { - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-toast": "^1.1.5", - "@types/react": "^18.3.2", - "@types/react-dom": "^18.2.25", - "@typescript-eslint/eslint-plugin": "^7.11.0", - "@typescript-eslint/parser": "^7.11.0", - "@vitejs/plugin-react-swc": "^3.5.0", - "eslint": "^9.2.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.7", - "i18next": "^23.11.5", - "i18next-browser-languagedetector": "^8.0.0", - "lucide-react": "^0.381.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hook-form": "^7.51.5", - "react-i18next": "^14.1.0", - "react-router-dom": "^6.23.1", - "socket.io-client": "^4.7.5", - "typescript": "^5.4.5", - "vite": "^5.2.12", - "vite-plugin-static-copy": "^1.0.3", - "vite-plugin-svgr": "^4.2.0", - "zustand": "^4.5.2" + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-toast": "^1.2.14", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@typescript-eslint/eslint-plugin": "^8.34.0", + "@typescript-eslint/parser": "^8.34.0", + "@vitejs/plugin-react-swc": "^3.10.2", + "eslint": "^9.28.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "i18next": "^25.2.1", + "i18next-browser-languagedetector": "^8.2.0", + "lucide-react": "^0.515.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-hook-form": "^7.57.0", + "react-i18next": "^15.5.3", + "react-router-dom": "^7.6.2", + "socket.io-client": "^4.8.1", + "typescript": "^5.8.2", + "vite": "^6.3.5", + "vite-plugin-static-copy": "^3.0.0", + "vite-plugin-svgr": "^4.3.0", + "zustand": "^5.0.5" } } diff --git a/admin/public/ep_admin_pads/de.json b/admin/public/ep_admin_pads/de.json index afb553caf..67dd73ddf 100644 --- a/admin/public/ep_admin_pads/de.json +++ b/admin/public/ep_admin_pads/de.json @@ -14,6 +14,7 @@ "ep_adminpads2_autoupdate.title": "Aktiviert oder deaktiviert automatische Aktualisierungen für die aktuelle Abfrage.", "ep_adminpads2_confirm": "Willst du das Pad {{padID}} wirklich löschen?", "ep_adminpads2_delete.value": "Löschen", + "ep_adminpads2_cleanup": "Historie aufräumen", "ep_adminpads2_last-edited": "Zuletzt bearbeitet", "ep_adminpads2_loading": "Lädt...", "ep_adminpads2_manage-pads": "Pads verwalten", diff --git a/admin/public/ep_admin_pads/en.json b/admin/public/ep_admin_pads/en.json index 8a9044b1b..76354c640 100644 --- a/admin/public/ep_admin_pads/en.json +++ b/admin/public/ep_admin_pads/en.json @@ -4,6 +4,7 @@ "ep_adminpads2_autoupdate.title": "Enables or disables automatic updates for the current query.", "ep_adminpads2_confirm": "Do you really want to delete the pad {{padID}}?", "ep_adminpads2_delete.value": "Delete", + "ep_adminpads2_cleanup": "Cleanup revisions", "ep_adminpads2_last-edited": "Last edited", "ep_adminpads2_loading": "Loading…", "ep_adminpads2_manage-pads": "Manage pads", diff --git a/admin/src/App.tsx b/admin/src/App.tsx index b3238ef9a..ae23ab3d3 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -1,4 +1,4 @@ -import {useEffect} from 'react' +import {useEffect, useState} from 'react' import './App.css' import {connect} from 'socket.io-client' import {isJSONClean} from './utils/utils.ts' @@ -6,107 +6,115 @@ 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, PhoneCall} from "lucide-react"; +import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall, LucideMenu} from "lucide-react"; -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() +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() + const [sidebarOpen, setSidebarOpen] = useState(true) - useEffect(() => { - fetch('/admin-auth/', { - method: 'POST' - }).then((value)=>{ - if(!value.ok){ - navigate('/login') - } - }).catch(()=>{ - navigate('/login') - }) - }, []); + useEffect(() => { + fetch('/admin-auth/', { + method: 'POST' + }).then((value) => { + if (!value.ok) { + navigate('/login') + } + }).catch(() => { + navigate('/login') + }) + }, []); - useEffect(() => { - document.title = t('admin.page-title') + useEffect(() => { + document.title = t('admin.page-title') - useStore.getState().setShowLoading(true); - const settingSocket = connect(`${WS_URL}/settings`, { - transports: ['websocket'], - }); + useStore.getState().setShowLoading(true); + const settingSocket = connect(`${WS_URL}/settings`, { + transports: ['websocket'], + }); - const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, { - transports: ['websocket'], - }) + const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, { + transports: ['websocket'], + }) - pluginsSocket.on('connect', () => { - useStore.getState().setPluginsSocket(pluginsSocket); - }); + pluginsSocket.on('connect', () => { + useStore.getState().setPluginsSocket(pluginsSocket); + }); - settingSocket.on('connect', () => { - useStore.getState().setSettingsSocket(settingSocket); - useStore.getState().setShowLoading(false) - settingSocket.emit('load'); - console.log('connected'); - }); + settingSocket.on('connect', () => { + useStore.getState().setSettingsSocket(settingSocket); + useStore.getState().setShowLoading(false) + 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". - useStore.getState().setShowLoading(true) - if (reason === 'io server disconnect') { - settingSocket.connect(); - } - }); + settingSocket.on('disconnect', (reason) => { + // The settingSocket.io client will automatically try to reconnect for all reasons other than "io + // server disconnect". + useStore.getState().setShowLoading(true) + 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; - } + 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); - }); + /* 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) - }) + settingSocket.on('saveprogress', (status) => { + console.log(status) + }) - return () => { - settingSocket.disconnect(); - pluginsSocket.disconnect() - } - }, []); + return () => { + settingSocket.disconnect(); + pluginsSocket.disconnect() + } + }, []); - return
- -
-
- + return
+ +
+
+

Etherpad

-
    -
  • -
  • -
  • -
  • -
  • Communication
  • -
-
-
-
- -
+
    { + if (window.innerWidth < 768) { + setSidebarOpen(false) + } + }}> +
  • +
  • +
  • +
  • +
  • Communication
  • +
+
+ +
+ +
+
} export default App diff --git a/admin/src/components/IconButton.tsx b/admin/src/components/IconButton.tsx index e91f3e914..876a55779 100644 --- a/admin/src/components/IconButton.tsx +++ b/admin/src/components/IconButton.tsx @@ -1,4 +1,4 @@ -import {FC, ReactElement} from "react"; +import {FC, JSX, ReactElement} from "react"; export type IconButtonProps = { icon: JSX.Element, diff --git a/admin/src/index.css b/admin/src/index.css index a7330448e..acc0d2e97 100644 --- a/admin/src/index.css +++ b/admin/src/index.css @@ -1,281 +1,298 @@ :root { - --etherpad-color: #0f775b; - --etherpad-comp: #9C8840; - --etherpad-light: #99FF99; + --etherpad-color: #0f775b; + --etherpad-comp: #9C8840; + --etherpad-light: #99FF99; + --sidebar-width: 20em; } @font-face { - font-family: Karla; - src: url(/Karla-Regular.ttf); + font-family: Karla; + src: url(/Karla-Regular.ttf); } html, body, #root { - box-sizing: border-box; - height: 100%; - font-family: "Karla", sans-serif; + box-sizing: border-box; + height: 100%; + font-family: "Karla", sans-serif; } *, *:before, *:after { - box-sizing: inherit; - font-size: 16px; + box-sizing: inherit; + font-size: 16px; } body { - margin: 0; - color: #333; - font: 14px helvetica, sans-serif; - background: #eee; + margin: 0; + color: #333; + font: 14px helvetica, sans-serif; + background: #eee; } div.menu { - height: 100vh; - font-size: 16px; - font-weight: bolder; - display: flex; - align-items: center; - justify-content: center; - max-width: 20%; - min-width: 20%; + left: 0; + transition: left .3s; + height: 100vh; + font-size: 16px; + font-weight: bolder; + display: flex; + align-items: center; + justify-content: center; + width: var(--sidebar-width); + z-index: 99; + position: fixed; } -.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 { + 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; + align-self: center; } .icon-button span { - align-self: center; + align-self: center; } div.menu span:first-child { - display: flex; - justify-content: center; + display: flex; + justify-content: center; } div.menu span:first-child svg { - margin-right: 10px; - align-self: center; + margin-right: 10px; + align-self: center; } div.menu h1 { - font-size: 50px; - text-align: center; + 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; + 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; + color: white; + padding: 0; } div.menu li a { - display: flex; - gap: 10px; - margin-bottom: 20px; + display: flex; + gap: 10px; + margin-bottom: 20px; } div.menu svg { - align-self: center; + align-self: center; } div.menu li { - padding: 10px; - color: white; - list-style: none; - margin-left: 3px; - line-height: 3; + padding: 10px; + color: white; + list-style: none; + margin-left: 3px; + line-height: 3; } div.menu li:has(.active) { - background-color: #9C885C ; + background-color: #9C885C; } div.menu li a { - color: lightgray; + color: lightgray; } - div.innerwrapper { - background-color: #F0F0F0; - overflow: auto; - height: 100vh; - flex-grow: 100; - padding: 20px; + transition: margin-left .3s; + isolation: isolate; + background-color: #F0F0F0; + overflow: auto; + height: 100vh; + flex-grow: 100; + margin-left: var(--sidebar-width); + padding: 20px 20px 20px; } div.innerwrapper-err { - display: none; + display: none; } #wrapper { - display: flex; - background: none repeat scroll 0px 0px #FFFFFF; - box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2); - min-height: 100%;/*always display a scrollbar*/ - + background: none repeat scroll 0px 0px #FFFFFF; + box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2); + min-height: 100%; /*always display a scrollbar*/ } h1 { - font-size: 29px; + font-size: 29px; } h2 { - font-size: 24px; + 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); + 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; + margin-bottom: 0; } #inner { - width: 300px; - margin: 0 auto; + width: 300px; + margin: 0 auto; } input { - font-weight: bold; - font-size: 15px; + font-weight: bold; + font-size: 15px; } .sort { - cursor: pointer; + cursor: pointer; } + .sort:after { - content: '▲▼' + content: '▲▼' } + .sort.up:after { - content:'▲' + content: '▲' } + .sort.down:after { - content:'▼' + content: '▼' } #installed-plugins thead tr th:nth-child(3) { - width: 15%; + width: 15%; } table { - border: 1px solid #ddd; - border-radius: 3px; - border-spacing: 0; - width: 100%; - margin: 20px 0; + border: 1px solid #ddd; + border-radius: 3px; + border-spacing: 0; + width: 100%; + margin: 20px 0; +} + +.table-container { + width: 100%; + overflow: auto; + max-height: 90vh; } - - - -#available-plugins th:first-child, #available-plugins th:nth-child(2){ - text-align: center; +#available-plugins th:first-child, #available-plugins th:nth-child(2) { + text-align: center; } td, th { - padding: 5px; + padding: 5px; } .template { - display: none; + 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%; +#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; + height: 5em; } + .messages * { - display: none; - text-align: center; + display: none; + text-align: center; } + .messages .fetching { - display: block; + display: block; } .progress { - position: absolute; - top: 0; left: 0; bottom:0; right:0; - padding: auto; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + padding: auto; - background: rgb(255,255,255); - display: none; + background: rgb(255, 255, 255); + display: none; } #search-progress.progress { - padding-top: 20%; - background: rgba(255,255,255,0.3); + padding-top: 20%; + background: rgba(255, 255, 255, 0.3); } .progress * { - display: block; - margin: 0 auto; - text-align: center; - color: #666; + display: block; + margin: 0 auto; + text-align: center; + color: #666; } .settings-page { - display: flex; - flex-direction: column; - gap: 20px; - height: 100%; + display: flex; + flex-direction: column; + gap: 20px; + height: 100%; } .settings { - flex-grow: max(1, 1); - outline: none; - width: 100%; - resize: none; + flex-grow: max(1, 1); + outline: none; + width: 100%; + resize: none; + font-family: monospace; } #response { - display: inline; + display: inline; } a:link, a:visited, a:hover, a:focus { - color: #333333; - text-decoration: none; + color: #333333; + text-decoration: none; } a:focus, a:hover { - text-decoration: underline; + text-decoration: underline; } .installed-results a:link, @@ -285,519 +302,569 @@ a:focus, a:hover { .installed-results a:hover, .search-results a:hover, .installed-results a:focus, -.search-results a:focus { - text-decoration: underline; +.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; + text-decoration: none; } pre { - white-space: pre-wrap; - word-wrap: break-word; + white-space: pre-wrap; + word-wrap: break-word; +} + + +#icon-button { + color: var(--etherpad-color); + top: 10px; + background-color: transparent; + border: none; + z-index: 99; + position: absolute; + left: 10px; +} + + +.inner-menu span:nth-child(2) { + display: flex; + margin-top: 30px; +} + +#wrapper.closed .menu { + left: calc(-1 * var(--sidebar-width)); +} + +#wrapper.closed .innerwrapper { + margin-left: 0; } @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; - float: left; - } + div.innerwrapper { + margin-left: 0; + } - table { - border: none; - } + .inner-menu { + border-radius: 0; + } - table, thead, tbody, td, tr { - display: block; - } + div.menu { + height: auto; + border-right: none; + --sidebar-width: 100%; + float: left; + } - thead tr { - display: none; - } + table { + border: none; + } - tr { - border: 1px solid #ccc; - margin-bottom: 5px; - border-radius: 3px; - } + table, thead, tbody, td, tr { + display: block; + } - td { - border: none; - border-bottom: 1px solid #eee; - position: relative; - padding-left: 50%; - white-space: normal; - text-align: left; - } + thead tr { + display: none; + } - td.name { - word-wrap: break-word; - } + tr { + border: 1px solid #ccc; + margin-bottom: 5px; + border-radius: 3px; + } - 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 { + border: none; + border-bottom: 1px solid #eee; + position: relative; + padding-left: 50%; + white-space: normal; + text-align: left; + } - td:last-child { - border-bottom: none; - } + td.name { + word-wrap: break-word; + } - table input[type="button"] { - float: none; - } + 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; + margin-top: 10px; + display: flex; + gap: 10px; } .login-background { - background-image: url("/fond.jpg"); - background-repeat: no-repeat; - background-size: cover; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - background-color: #f0f0f0; + background-image: url("/fond.jpg"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f0f0f0; } .login-inner-box div { - margin-top: 1rem; + margin-top: 1rem; } -.login-inner-box [type=submit]{ - margin-top: 2rem; +.login-inner-box [type=submit] { + margin-top: 2rem; } - .login-textinput { - width: 100%; - padding: 10px; - background-color: #fffacc; - border-radius: 5px; - border: 1px solid #ccc; - margin-bottom: 10px; + 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: 40px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - background-color: #fff; + padding: 20px; + border-radius: 40px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + background-color: #fff; } -.login-inner-box{ - position: relative; - padding: 20px; + +@media (max-width: 900px) { + .login-box { + width: 90% + } +} + +.login-inner-box { + position: relative; + padding: 20px; } .login-title { - padding: 0; - margin: 0; - text-align: center; - color: var(--etherpad-color); - font-size: 4rem; - font-weight: 1000; + padding: 0; + margin: 0; + text-align: center; + color: var(--etherpad-color); + font-size: 4rem; + font-weight: 1000; } .login-button { - padding: 10px; - background-color: var(--etherpad-color); - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - width: 100%; - height: 40px; + 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; + position: fixed; + inset: 0; + background-color: white; + z-index: 100; } .dialog-confirm-overlay { - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 100; + 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; + position: fixed; + top: 50%; + left: 50%; + background-color: white; + transform: translate(-50%, -50%); + padding: 20px; + z-index: 101; } .dialog-content { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - padding: 20px; - z-index: 101; + 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; + color: var(--etherpad-color); + font-size: 2em; + margin-bottom: 20px; } - .ToastViewport { - position: fixed; - top: 10px; - right: 20px; - display: flex; - flex-direction: column; - gap: 10px; - width: 390px; - max-width: 100vw; - margin: 0; - list-style: none; - z-index: 2147483647; - outline: none; + position: fixed; + top: 10px; + right: 20px; + display: flex; + flex-direction: column; + gap: 10px; + width: 390px; + max-width: 100vw; + margin: 0; + list-style: none; + z-index: 2147483647; + outline: none; } .ToastRootSuccess { - background-color: lawngreen; + background-color: lawngreen; } .ToastRootFailure { - background-color: red; + background-color: red; } .ToastRootFailure > .ToastTitle { - color: white; + color: white; } .ToastRoot { - border-radius: 20px; - box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px; - padding: 15px; - display: grid; - grid-template-areas: 'title action' 'description action'; - grid-template-columns: auto max-content; - column-gap: 15px; - align-items: center; + border-radius: 20px; + box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px; + padding: 15px; + display: grid; + grid-template-areas: 'title action' 'description action'; + grid-template-columns: auto max-content; + column-gap: 15px; + align-items: center; } + .ToastRoot[data-state='open'] { - animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1); + animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1); } + .ToastRoot[data-state='closed'] { - animation: hide 100ms ease-in; + animation: hide 100ms ease-in; } + .ToastRoot[data-swipe='move'] { - transform: translateX(var(--radix-toast-swipe-move-x)); + transform: translateX(var(--radix-toast-swipe-move-x)); } + .ToastRoot[data-swipe='cancel'] { - transform: translateX(0); - transition: transform 200ms ease-out; + transform: translateX(0); + transition: transform 200ms ease-out; } + .ToastRoot[data-swipe='end'] { - animation: swipeOut 100ms ease-out; + animation: swipeOut 100ms ease-out; } @keyframes hide { - from { - opacity: 1; - } - to { - opacity: 0; - } + from { + opacity: 1; + } + to { + opacity: 0; + } } @keyframes slideIn { - from { - transform: translateX(calc(100% + var(--viewport-padding))); - } - to { - transform: translateX(0); - } + from { + transform: translateX(calc(100% + var(--viewport-padding))); + } + to { + transform: translateX(0); + } } @keyframes swipeOut { - from { - transform: translateX(var(--radix-toast-swipe-end-x)); - } - to { - transform: translateX(calc(100% + var(--viewport-padding))); - } + from { + transform: translateX(var(--radix-toast-swipe-end-x)); + } + to { + transform: translateX(calc(100% + var(--viewport-padding))); + } } .ToastTitle { - grid-area: title; - margin-bottom: 5px; - font-weight: 500; - color: var(--slate-12); - padding: 10px; - font-size: 15px; + grid-area: title; + margin-bottom: 5px; + font-weight: 500; + color: var(--slate-12); + padding: 10px; + font-size: 15px; } .ToastDescription { - grid-area: description; - margin: 0; - color: var(--slate-11); - font-size: 13px; - line-height: 1.3; + grid-area: description; + margin: 0; + color: var(--slate-11); + font-size: 13px; + line-height: 1.3; } .ToastAction { - grid-area: action; + grid-area: action; } .help-block { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 20px + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px } .search-field { - position: relative; + position: relative; } .search-field input { - border-color: transparent; - border-radius: 20px; - height: 2.5rem; - width: 100vh; - padding: 5px 5px 5px 30px; + border-color: transparent; + border-radius: 20px; + height: 2.5rem; + width: 100%; + padding: 5px 5px 5px 30px; } .search-field input:focus { - outline: none; + outline: none; } .send-message { - position: relative; + position: relative; } .send-message input { - width: auto; + width: auto; } .send-message { } .send-message svg { - position: absolute; - right: 3px; - bottom: -3px; - left: auto !important; + position: absolute; + right: 3px; + bottom: -3px; + left: auto !important; } .search-field svg { - position: absolute; - left: 3px; - bottom: -3px; + position: absolute; + left: 3px; + bottom: -3px; } .search-field svg { - color: gray + 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); + 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; + border-top-left-radius: 10px; } th:last-child { - border-top-right-radius: 10px; + border-top-right-radius: 10px; } table thead tr { - font-size: 25px; - background-color: var(--etherpad-color); - color: #ffffff; - text-align: left; + font-size: 25px; + background-color: var(--etherpad-color); + color: #ffffff; + text-align: left; } table tbody tr { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #dddddd; } table tr:nth-child(even) td { - background-color: lightgray; + background-color: lightgray; } table tr td { - padding: 12px 15px; + padding: 12px 15px; } table tbody tr:nth-of-type(even) { - background-color: #f3f3f3; + background-color: #f3f3f3; } table tbody tr:last-of-type { - border-bottom: 2px solid #009879; + border-bottom: 2px solid #009879; } table tbody tr.active-row { - font-weight: bold; - color: #009879; + font-weight: bold; + color: #009879; } -.pad-pagination{ - display: flex; - justify-content: center; - gap: 10px; - margin-top: 20px; +.pad-pagination { + display: flex; + justify-content: center; + gap: 10px; + margin-top: 20px; } .pad-pagination button { - display: flex; - padding: 10px 20px; - border-radius: 5px; - border: none; - color: black; - cursor: pointer; + display: flex; + padding: 10px 20px; + border-radius: 5px; + border: none; + color: black; + cursor: pointer; } .pad-pagination button:disabled { - background: transparent; - color: lightgrey; - cursor: not-allowed; + background: transparent; + color: lightgrey; + cursor: not-allowed; } .pad-pagination span { - align-self: center; + align-self: center; } -.pad-pagination >span { - font-size: 20px; +.pad-pagination > span { + font-size: 20px; } .login-page .login-form .input-control input[type=text], .login-page .login-form .input-control input[type=email], .login-page .login-form .input-control input[type=password], .login-page .signup-form .input-control input[type=text], .login-page .signup-form .input-control input[type=email], .login-page .signup-form .input-control input[type=password], .login-page .forgot-form .input-control input[type=text], .login-page .forgot-form .input-control input[type=email], .login-page .forgot-form .input-control input[type=password] { - width: 100%; - padding: 12px 20px; - margin: 8px 0; - display: inline-block; - border-bottom: 2px solid #ccc; - border-top: 0; - border-left: 0; - border-right: 0; - -webkit-box-sizing: border-box; - box-sizing: border-box; - border-radius: 5px; - font-size: 14px; - color: #666; - background-color: #f8f8f8; - -webkit-transition: all 0.3s ease-in-out; - transition: all 0.3s ease-in-out; + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: inline-block; + border-bottom: 2px solid #ccc; + border-top: 0; + border-left: 0; + border-right: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; + border-radius: 5px; + font-size: 14px; + color: #666; + background-color: #f8f8f8; + -webkit-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; } input, button, select, optgroup, textarea { - margin: 0; - font-family: inherit; - font-size: inherit; - line-height: inherit; + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; } .icon-input { - position: relative; + position: relative; } .icon-input svg { - position: absolute; - top: 50%; - transform: translateY(-50%); - right: 10px; - color: #666; + position: absolute; + top: 50%; + transform: translateY(-50%); + right: 10px; + color: #666; } .SwitchRoot { - align-self: center; - width: 60px; - height: 30px; - background-color: black; - border-radius: 9999px; - position: relative; - box-shadow: 0 2px 10px var(--black-a7); - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + align-self: center; + width: 60px; + height: 30px; + background-color: black; + border-radius: 9999px; + position: relative; + box-shadow: 0 2px 10px var(--black-a7); + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } + .SwitchRoot:focus { - box-shadow: 0 0 0 2px black; + box-shadow: 0 0 0 2px black; } + .SwitchRoot[data-state='checked'] { - background-color: var(--etherpad-color); + background-color: var(--etherpad-color); } .SwitchThumb { - display: block; - width: 20px; - height: 20px; - background-color: white; - border-radius: 9999px; - box-shadow: 0 2px 2px var(--black-a7); - transition: transform 100ms; - transform: translateX(2px); - will-change: transform; + display: block; + width: 20px; + height: 20px; + background-color: white; + border-radius: 9999px; + box-shadow: 0 2px 2px var(--black-a7); + transition: transform 100ms; + transform: translateX(2px); + will-change: transform; } + .SwitchThumb[data-state='checked'] { - transform: translateX(25px); + transform: translateX(25px); } .Label { - color: white; - font-size: 15px; - line-height: 1; + color: white; + font-size: 15px; + line-height: 1; } .message { - position: relative; - padding: 10px; - border: 1px solid #e0e0e0; - margin: 10px 20px 10px 10px; - border-radius: 10px 0 10px 10px; - background-color: var(--etherpad-color); - color: white + position: relative; + padding: 10px; + border: 1px solid #e0e0e0; + margin: 10px 20px 10px 10px; + border-radius: 10px 0 10px 10px; + background-color: var(--etherpad-color); + color: white +} + +.search-pads { + text-align: center; +} + +.search-pads-body tr td:last-child { + display: flex; + justify-content: center; } diff --git a/admin/src/pages/HomePage.tsx b/admin/src/pages/HomePage.tsx index 889432487..f5ce0a5ab 100644 --- a/admin/src/pages/HomePage.tsx +++ b/admin/src/pages/HomePage.tsx @@ -4,16 +4,54 @@ 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 {ArrowUpFromDot, Download, Trash} from "lucide-react"; import {IconButton} from "../components/IconButton.tsx"; +import {determineSorting} from "../utils/sorting.ts"; export const HomePage = () => { const pluginsSocket = useStore(state=>state.pluginsSocket) const [plugins,setPlugins] = useState([]) - const [installedPlugins, setInstalledPlugins] = useState([]) + const installedPlugins = useStore(state=>state.installedPlugins) + const setInstalledPlugins = useStore(state=>state.setInstalledPlugins) + const [searchParams, setSearchParams] = useState({ + offset: 0, + limit: 99999, + sortBy: 'name', + sortDir: 'asc', + searchTerm: '' + }) + + const filteredInstallablePlugins = useMemo(()=>{ + return plugins.sort((a, b)=>{ + if(searchParams.sortBy === "version"){ + if(searchParams.sortDir === "asc"){ + return a.version.localeCompare(b.version) + } + return b.version.localeCompare(a.version) + } + + if(searchParams.sortBy === "last-updated"){ + if(searchParams.sortDir === "asc"){ + return a.time.localeCompare(b.time) + } + return b.time.localeCompare(a.time) + } + + + if (searchParams.sortBy === "name") { + if(searchParams.sortDir === "asc"){ + return a.name.localeCompare(b.name) + } + return b.name.localeCompare(a.name) + } + return 0 + }) + }, [plugins, searchParams]) + const sortedInstalledPlugins = useMemo(()=>{ - return installedPlugins.sort((a, b)=>{ + return useStore.getState().installedPlugins.sort((a, b)=>{ + if(a.name < b.name){ return -1 } @@ -23,14 +61,8 @@ export const HomePage = () => { return 0 }) - } ,[installedPlugins]) - const [searchParams, setSearchParams] = useState({ - offset: 0, - limit: 99999, - sortBy: 'name', - sortDir: 'asc', - searchTerm: '' - }) + } ,[installedPlugins, searchParams]) + const [searchTerm, setSearchTerm] = useState('') const {t} = useTranslation() @@ -47,17 +79,16 @@ export const HomePage = () => { }) pluginsSocket.on('results:updatable', (data) => { - data.updatable.forEach((pluginName: string) => { - setInstalledPlugins(installedPlugins.map(plugin => { - if (plugin.name === pluginName) { - return { - ...plugin, - updatable: true - } - } - return plugin - })) - }) + const newInstalledPlugins = useStore.getState().installedPlugins.map(plugin => { + if (data.updatable.includes(plugin.name)) { + return { + ...plugin, + updatable: true + } + } + return plugin + }) + setInstalledPlugins(newInstalledPlugins) }) pluginsSocket.on('finished:install', () => { @@ -93,25 +124,20 @@ export const HomePage = () => { if (!pluginsSocket) { return } - pluginsSocket?.emit('search', searchParams) - - pluginsSocket!.on('results:search', (data: { results: PluginDef[] }) => { - if (Array.isArray(data.results) && data.results.length > 0) { - setPlugins(data.results) - } else { - useStore.getState().setToastState({ - open: true, - title: "Error retrieving plugins", - success: false - }) - } + setPlugins(data.results) + }) + pluginsSocket!.on('results:searcherror', (data: {error: string}) => { + console.log(data.error) + useStore.getState().setToastState({ + open: true, + title: "Error retrieving plugins", + success: false + }) }) - - }, [searchParams, pluginsSocket]); const uninstallPlugin = (pluginName: string)=>{ @@ -125,7 +151,6 @@ export const HomePage = () => { setPlugins(plugins.filter(plugin=>plugin.name !== pluginName)) } - useDebounce(()=>{ setSearchParams({ ...searchParams, @@ -134,6 +159,7 @@ export const HomePage = () => { }) }, 500, [searchTerm]) + return

@@ -155,42 +181,67 @@ export const HomePage = () => { { plugin.updatable ? - + installPlugin(plugin.name)} icon={} title="Update"> : } title={} onClick={() => uninstallPlugin(plugin.name)}/> } })} - - + + -

- {setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/> +

+ {setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/> +
- + - - + + - {plugins.map((plugin) => { - return - - - - - - - })} + {(filteredInstallablePlugins.length > 0) ? + filteredInstallablePlugins.map((plugin) => { + return + + + + + + + }) + : + + }
{ + setSearchParams({ + ...searchParams, + sortBy: 'name', + sortDir: searchParams.sortDir === "asc"? "desc": "asc" + }) + }}> + { + setSearchParams({ + ...searchParams, + sortBy: 'version', + sortDir: searchParams.sortDir === "asc"? "desc": "asc" + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'last-updated', + sortDir: searchParams.sortDir === "asc"? "desc": "asc" + }) + }}>
{plugin.name}{plugin.description}{plugin.version}{plugin.time} - } onClick={() => installPlugin(plugin.name)} title={}/> -
{plugin.name}{plugin.description}{plugin.version}{plugin.time} + } onClick={() => installPlugin(plugin.name)} title={}/> +
{searchTerm == '' ? : }
+
} diff --git a/admin/src/pages/PadPage.tsx b/admin/src/pages/PadPage.tsx index 284cba3c1..b5db854f5 100644 --- a/admin/src/pages/PadPage.tsx +++ b/admin/src/pages/PadPage.tsx @@ -6,7 +6,7 @@ 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 {ChevronLeft, ChevronRight, Eye, Trash2} from "lucide-react"; +import {ChevronLeft, ChevronRight, Eye, Trash2, FileStack} from "lucide-react"; import {SearchField} from "../components/SearchField.tsx"; export const PadPage = ()=>{ @@ -23,10 +23,11 @@ export const PadPage = ()=>{ const pads = useStore(state=>state.pads) const [currentPage, setCurrentPage] = useState(0) const [deleteDialog, setDeleteDialog] = useState(false) + const [errorText, setErrorText] = useState(null) const [padToDelete, setPadToDelete] = useState('') const pages = useMemo(()=>{ if(!pads){ - return [0] + return 0; } return Math.ceil(pads!.total / searchParams.limit) @@ -68,12 +69,35 @@ export const PadPage = ()=>{ results: newPads }) }) + + settingsSocket.on('results:cleanupPadRevisions', (data)=>{ + let newPads = useStore.getState().pads?.results ?? [] + + if (data.error) { + setErrorText(data.error) + return + } + + newPads.forEach((pad)=>{ + if (pad.padName === data.padId) { + pad.revisionNumber = data.keepRevisions + } + }) + + useStore.getState().setPads({ + results: newPads, + total: useStore.getState().pads!.total + }) + }) }, [settingsSocket, pads]); const deletePad = (padID: string)=>{ settingsSocket?.emit('deletePad', padID) } + const cleanupPad = (padID: string)=>{ + settingsSocket?.emit('cleanupPadRevisions', padID) + } return
@@ -100,11 +124,26 @@ export const PadPage = ()=>{ + + + + +
+
Error occured: {errorText}
+
+ +
+
+
+
+

setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/> - + - + - + { pads?.results?.map((pad)=>{ return @@ -150,6 +189,9 @@ export const PadPage = ()=>{ setPadToDelete(pad.padName) setDeleteDialog(true) }}/> + } title={} onClick={()=>{ + cleanupPad(pad.padName) + }}/> } title="view" onClick={()=>window.open(`/p/${pad.padName}`, '_blank')}/> @@ -166,7 +208,7 @@ export const PadPage = ()=>{ offset: (Number(currentPage)-1)*searchParams.limit}) }}>Previous Page {currentPage+1} out of {pages} - +
  • +
  • +
  • +
  • +
  • +
  • +
  • + + + + + + + + +
    + + + + + + + +
    + +
    + +
    +

    + You do not have permission to access this pad +

    +
    + + +

    +
    + Loading... +

    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + +
    { setSearchParams({ ...searchParams, @@ -112,19 +151,19 @@ export const PadPage = ()=>{ ascending: !searchParams.ascending }) }}>{ - setSearchParams({ - ...searchParams, - sortBy: 'lastEdited', - ascending: !searchParams.ascending - }) - }}> { setSearchParams({ ...searchParams, sortBy: 'userCount', ascending: !searchParams.ascending }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'lastEdited', + ascending: !searchParams.ascending + }) }}> { setSearchParams({ @@ -136,7 +175,7 @@ export const PadPage = ()=>{
    + +
    +
    +
    + +
    + + + + + + +
    + + + 0 +
    + +
    +
    +
    +

    + + █   +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    JavaScript license information
    + + + diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 2ce13f8f7..89667286b 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -3,8 +3,11 @@ import { resolve } from 'path' import { defineConfig } from 'vite' export default defineConfig({ - base: '/views/', + base: '/views/', build: { + commonjsOptions:{ + transformMixedEsModules: true, + }, outDir: resolve(__dirname, '../src/static/oidc'), rollupOptions: { input: { @@ -14,4 +17,31 @@ export default defineConfig({ }, emptyOutDir: true, }, + server:{ + proxy:{ + '/static':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + }, + '/views/manifest.json':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/views/, ''), + }, + '/locales.json':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/views/, ''), + }, + '/locales':{ + target: 'http://localhost:9001', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/views/, ''), + }, + } + } }) diff --git a/var/.gitignore b/var/.gitignore index 1dea48383..d75cb9e42 100644 --- a/var/.gitignore +++ b/var/.gitignore @@ -1,3 +1,5 @@ sqlite.db minified* -installed_plugins.json \ No newline at end of file +installed_plugins.json +dirty.db +rusty.db