Compare commits
21 Commits
v0.10.3-et
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fa3f2437a3 | ||
![]() |
8dc5238fcb | ||
![]() |
238350b940 | ||
![]() |
99bf7b1889 | ||
![]() |
d72c91644d | ||
![]() |
e8e28b5df1 | ||
![]() |
d5c10b6e02 | ||
![]() |
3085b9ffa0 | ||
![]() |
b2a3fb0f87 | ||
![]() |
1e8b4cc885 | ||
![]() |
4d1a9cc147 | ||
![]() |
1b8b702270 | ||
![]() |
61c32fb473 | ||
![]() |
ad876bb790 | ||
![]() |
2524848dae | ||
![]() |
669c1f3079 | ||
![]() |
590f673167 | ||
![]() |
307793f000 | ||
![]() |
96f549fe42 | ||
![]() |
3de4332477 | ||
![]() |
9fc005032c |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
|
||||
tests/
|
||||
build/
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
electron_app/
|
||||
karma-reports/
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
.idea/
|
||||
.tmp/
|
||||
config.json*
|
16
.github/CONTRIBUTING.md
vendored
16
.github/CONTRIBUTING.md
vendored
@@ -1,16 +0,0 @@
|
||||
# Contributing to [etkecc/synapse-admin](https://github.com/etkecc/synapse-admin)
|
||||
|
||||
While etke.cc fork is intended to accept more QoL changes and features,
|
||||
it's good idea to open PR into the upstream repo: [Awesome-Technologies/Synapse-Admin](https://github.com/Awesome-Technologies/synapse-admin).
|
||||
|
||||
1. Use the etkecc/synapse-admin **master** branch as your branch upstream: `git checkout master; git pull; git checkout -b my-new-feature`
|
||||
2. Once your changes are ready, please, open **2** PRs: one from your branch to `Awesome-Technologies/Synapse-Admin` **master**, and another one to `etkecc/synapse-admin` **main**
|
||||
3. Once PR is accepted in the `etkecc/synapse-admin`, update `README.md` file (either directly in the `main` branch, or via another PR) to add link to the merged PR in the [Fork differences](https://github.com/etkecc/synapse-admin#fork-differences) section
|
||||
|
||||
### Why?
|
||||
|
||||
The upstream project may not want to accept all the changes, so to ensure they are not lost, we will gladly add them to the etke.cc fork.
|
||||
Unfortunately, it's challenging to keep changes separated, so to avoid messing upstream and fork changes (e.g., CI changes that should not be pushed to the upstream, as they intended for this fork specifically), there are 2 branches:
|
||||
|
||||
* `master` - read-only copy of upstream's master branch to easily sync changes, and use it as base for new PRs
|
||||
* `main` - fork-own branch with all changes
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
liberapay: etkecc
|
20
.github/dependabot.yml
vendored
Normal file
20
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
# Major updates for react-admin have breaking changes
|
||||
- dependency-name: "react-admin"
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
26
.github/workflows/build-test.yml
vendored
Normal file
26
.github/workflows/build-test.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: build-test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Install dependencies
|
||||
run: yarn --immutable
|
||||
- name: Run checks
|
||||
run: yarn lint
|
||||
- name: Run tests
|
||||
run: yarn test
|
63
.github/workflows/docker-release.yml
vendored
Normal file
63
.github/workflows/docker-release.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Create docker image(s) and push to docker hub and ghcr.io
|
||||
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
|
||||
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/heads
|
||||
# prettier-ignore
|
||||
branches:
|
||||
# Push events on master branch
|
||||
- master
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
awesometechnologies/synapse-admin
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and Push Tag
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
31
.github/workflows/edge_ghpage.yml
vendored
Normal file
31
.github/workflows/edge_ghpage.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Build and Deploy Edge version to GH Pages
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
fetch-tags: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Install and Build 🔧
|
||||
run: |
|
||||
yarn install --immutable
|
||||
yarn build --base=/synapse-admin
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.7.3
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: dist
|
30
.github/workflows/github-release.yml
vendored
Normal file
30
.github/workflows/github-release.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Create release tarball and attach to tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
- run: yarn install --immutable
|
||||
- run: yarn build
|
||||
- run: |
|
||||
version=`git describe --dirty --tags || echo unknown`
|
||||
cp -r dist synapse-admin-$version
|
||||
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
||||
- uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
with:
|
||||
files: dist/*.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
119
.github/workflows/workflow.yml
vendored
119
.github/workflows/workflow.yml
vendored
@@ -1,119 +0,0 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "v*" ]
|
||||
env:
|
||||
bunny_version: v0.1.0
|
||||
base_path: ./
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
pull-requests: read
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --network-timeout=300000
|
||||
- name: Set version into manifest.json
|
||||
run: |
|
||||
TAG=$(git describe --tags --abbrev=0 || echo "latest")
|
||||
sed -i "s|\"icons\"|\"version\": \"$TAG\",\\n \"icons\"|g" public/manifest.json
|
||||
- name: Build
|
||||
run: yarn build --base=${{ env.base_path }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: dist/
|
||||
name: dist
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
overwrite: true
|
||||
include-hidden-files: true
|
||||
|
||||
docker:
|
||||
name: Docker
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
registry.etke.cc/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
|
||||
type=semver,pattern={{raw}}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
cdn:
|
||||
name: CDN
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Upload
|
||||
run: |
|
||||
wget -O bunny-upload.tar.gz https://github.com/etkecc/bunny-upload/releases/download/${{ env.bunny_version }}/bunny-upload_Linux_x86_64.tar.gz
|
||||
tar -xzf bunny-upload.tar.gz
|
||||
echo "${{ secrets.BUNNY_CONFIG }}" > bunny-config.yaml
|
||||
./bunny-upload -c bunny-config.yaml
|
||||
|
||||
github-release:
|
||||
name: Github Release
|
||||
needs: build
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Prepare release
|
||||
run: |
|
||||
mv dist synapse-admin
|
||||
tar chvzf synapse-admin.tar.gz synapse-admin
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: synapse-admin.tar.gz
|
||||
generate_release_notes: true
|
||||
make_latest: "true"
|
||||
draft: false
|
||||
prerelease: false
|
893
.yarn/releases/yarn-4.1.1.cjs
vendored
893
.yarn/releases/yarn-4.1.1.cjs
vendored
File diff suppressed because one or more lines are too long
925
.yarn/releases/yarn-4.4.1.cjs
vendored
Executable file
925
.yarn/releases/yarn-4.4.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
||||
|
27
Dockerfile
27
Dockerfile
@@ -1,5 +1,26 @@
|
||||
FROM ghcr.io/static-web-server/static-web-server:2
|
||||
# Builder
|
||||
FROM node:lts as builder
|
||||
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
||||
# Base path for synapse admin
|
||||
ARG BASE_PATH=./
|
||||
|
||||
ENV SERVER_ROOT=/app
|
||||
WORKDIR /src
|
||||
|
||||
COPY ./dist /app
|
||||
# Copy .yarn directory to the working directory (must be on a separate line!)
|
||||
# Use https://docs.docker.com/engine/reference/builder/#copy---parents when available
|
||||
COPY .yarn .yarn
|
||||
COPY package.json .yarnrc.yml yarn.lock ./
|
||||
|
||||
# Disable telemetry and install packages
|
||||
RUN yarn config set enableTelemetry 0 && yarn install --immutable --network-timeout=300000
|
||||
|
||||
COPY . /src
|
||||
RUN yarn build --base=$BASE_PATH
|
||||
|
||||
# App
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
COPY --from=builder /src/dist /app
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html \
|
||||
&& ln -s /app /usr/share/nginx/html
|
||||
|
30
README.md
30
README.md
@@ -9,36 +9,6 @@
|
||||
|
||||
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
||||
|
||||
## Fork differences
|
||||
|
||||
### Available via CDN
|
||||
|
||||
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
||||
|
||||
### Changes
|
||||
|
||||
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
|
||||
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
||||
user-friendly interface for managing Synapse homeservers.
|
||||
|
||||
The following changes are already implemented:
|
||||
|
||||
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
||||
* [Fix user's default tab not being shown](https://github.com/etkecc/synapse-admin/pull/8)
|
||||
* [Add identifier when authorizing with password](https://github.com/Awesome-Technologies/synapse-admin/pull/601)
|
||||
* [Add ability to toggle whether to show locked users](https://github.com/Awesome-Technologies/synapse-admin/pull/573)
|
||||
* [Fix user's display name in header on user's page](https://github.com/etkecc/synapse-admin/pull/9)
|
||||
* [Fix footer overlapping content](https://github.com/Awesome-Technologies/synapse-admin/issues/574)
|
||||
* Switch from nginx to [SWS](https://static-web-server.net/) for serving the app, reducing the size of the Docker image
|
||||
* [Fix redirect URL after user creation](https://github.com/etkecc/synapse-admin/pull/16)
|
||||
* [Display actual Synapse errors](https://github.com/etkecc/synapse-admin/pull/17)
|
||||
* [Fix base_url being undefined on unsuccessful login](https://github.com/etkecc/synapse-admin/pull/18)
|
||||
* [Put the version into manifest.json](https://github.com/Awesome-Technologies/synapse-admin/issues/507) (CI only)
|
||||
* [Federation page improvements](https://github.com/Awesome-Technologies/synapse-admin/pull/583) (using theme colors)
|
||||
* [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
|
||||
|
||||
_the list will be updated as new changes are added_
|
||||
|
||||
## Usage
|
||||
|
||||
### Supported Synapse
|
||||
|
@@ -2,13 +2,13 @@ services:
|
||||
synapse-admin:
|
||||
container_name: synapse-admin
|
||||
hostname: synapse-admin
|
||||
image: ghcr.io/etkecc/synapse-admin:latest
|
||||
image: awesometechnologies/synapse-admin:latest
|
||||
# build:
|
||||
# context: .
|
||||
|
||||
# to use the docker-compose as standalone without a local repo clone,
|
||||
# replace the context definition with this:
|
||||
# context: https://github.com/etkecc/synapse-admin.git
|
||||
# context: https://github.com/Awesome-Technologies/synapse-admin.git
|
||||
|
||||
# args:
|
||||
# - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
|
@@ -121,8 +121,8 @@
|
||||
</div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<footer
|
||||
style="position: relative; z-index: 2; height: 2em; margin-top: 0; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||
<a id="copyright" href="https://github.com/etkecc/synapse-admin"
|
||||
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
||||
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
||||
Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
|
||||
</a>
|
||||
|
15
justfile
15
justfile
@@ -1,15 +0,0 @@
|
||||
# Shows help
|
||||
default:
|
||||
@just --list --justfile {{ justfile() }}
|
||||
|
||||
# build the app
|
||||
build: __install
|
||||
@yarn run build --base=./
|
||||
|
||||
# run the app in a production mode
|
||||
run: build
|
||||
@python -m http.server -d dist 1313
|
||||
|
||||
# install the project
|
||||
__install:
|
||||
@yarn install --immutable --network-timeout=300000
|
31
package.json
31
package.json
@@ -8,12 +8,13 @@
|
||||
"homepage": ".",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/etkecc/synapse-admin"
|
||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||
},
|
||||
"packageManager": "yarn@4.1.1",
|
||||
"packageManager": "yarn@4.4.1",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.7.0",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@mui/utils": "^6.1.3",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
@@ -30,7 +31,7 @@
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-yaml": "^1.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -40,8 +41,8 @@
|
||||
"ts-jest": "^29.2.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.16.1",
|
||||
"vite": "^5.3.4",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-version-mark": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -51,25 +52,25 @@
|
||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||
"@mui/icons-material": "^5.16.4",
|
||||
"@mui/material": "^5.16.4",
|
||||
"@tanstack/react-query": "^5.59.12",
|
||||
"history": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"papaparse": "^5.4.1",
|
||||
"query-string": "^7.1.3",
|
||||
"ra-core": "^4.16.20",
|
||||
"ra-i18n-polyglot": "^4.16.20",
|
||||
"ra-language-english": "^4.16.20",
|
||||
"ra-language-farsi": "^4.2.0",
|
||||
"ra-language-french": "^4.16.20",
|
||||
"ra-core": "^5.2.3",
|
||||
"ra-i18n-polyglot": "^5.2.3",
|
||||
"ra-language-english": "^5.8.2",
|
||||
"ra-language-farsi": "^5.0.0",
|
||||
"ra-language-french": "^5.2.3",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"ra-language-russian": "^4.14.2",
|
||||
"react": "^18.3.1",
|
||||
"react-admin": "^4.16.20",
|
||||
"react-admin": "^5.2.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-is": "^18.3.1",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router": "^6.25.1",
|
||||
"react-router-dom": "^6.25.1"
|
||||
"react-router": "^6.28.1",
|
||||
"react-router-dom": "^6.28.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite serve",
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
fetchMock.enableMocks();
|
||||
|
||||
import App from "./App";
|
||||
|
||||
|
@@ -53,7 +53,6 @@ const App = () => (
|
||||
authProvider={authProvider}
|
||||
dataProvider={dataProvider}
|
||||
i18nProvider={i18nProvider}
|
||||
darkTheme={{ palette: { mode: "dark" } }}
|
||||
>
|
||||
<CustomRoutes>
|
||||
<Route path="/import_users" element={<ImportFeature />} />
|
||||
|
@@ -1,105 +0,0 @@
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||
import { Fragment, useState } from "react";
|
||||
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin";
|
||||
import ActionDelete from "@mui/icons-material/Delete";
|
||||
import ActionCheck from "@mui/icons-material/CheckCircle";
|
||||
import AlertError from "@mui/icons-material/ErrorOutline";
|
||||
|
||||
interface DeleteRoomButtonProps {
|
||||
selectedIds: Identifier[];
|
||||
confirmTitle: string;
|
||||
confirmContent: string;
|
||||
}
|
||||
|
||||
const resourceName = "rooms";
|
||||
|
||||
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
|
||||
const translate = useTranslate();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [block, setBlock] = useState(true);
|
||||
|
||||
const notify = useNotify();
|
||||
const redirect = useRedirect();
|
||||
|
||||
const [deleteMany, { isLoading }] = useDeleteMany();
|
||||
const unselectAll = useUnselectAll(resourceName);
|
||||
const recordIds = props.selectedIds;
|
||||
|
||||
const handleDialogOpen = () => setOpen(true);
|
||||
const handleDialogClose = () => setOpen(false);
|
||||
|
||||
const handleDelete = (values: {block: boolean}) => {
|
||||
deleteMany(
|
||||
resourceName,
|
||||
{ ids: recordIds, meta: values },
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify("resources.rooms.action.erase.success");
|
||||
handleDialogClose();
|
||||
unselectAll();
|
||||
redirect("/rooms");
|
||||
},
|
||||
onError: (error) =>
|
||||
notify("resources.rooms.action.erase.failure", { type: 'error' as NotificationType }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setOpen(false);
|
||||
handleDelete({ block: block });
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Button
|
||||
onClick={handleDialogOpen}
|
||||
disabled={isLoading}
|
||||
className={"ra-delete-button"}
|
||||
key="button"
|
||||
size="small"
|
||||
sx={{
|
||||
"&.MuiButton-sizeSmall": {
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
}}
|
||||
color={"error"}
|
||||
startIcon={<ActionDelete />}
|
||||
>
|
||||
{translate("ra.action.delete")}
|
||||
</Button>
|
||||
<Dialog open={open} onClose={handleDialogClose}>
|
||||
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{translate(props.confirmContent)}</DialogContentText>
|
||||
<SimpleForm toolbar={false}>
|
||||
<BooleanInput
|
||||
fullWidth
|
||||
source="block"
|
||||
value={block}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
|
||||
label="resources.rooms.action.erase.fields.block"
|
||||
defaultValue={true}
|
||||
/>
|
||||
</SimpleForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button disabled={false} onClick={handleDialogClose} startIcon={<AlertError />}>
|
||||
{translate("ra.action.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={false}
|
||||
onClick={handleConfirm}
|
||||
className={"ra-confirm RaConfirm-confirmPrimary"}
|
||||
autoFocus
|
||||
startIcon={<ActionCheck />}
|
||||
>
|
||||
{translate("ra.action.confirm")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteRoomButton;
|
@@ -121,7 +121,7 @@ const FilePicker = () => {
|
||||
|
||||
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
||||
/* First, verify the presence of required fields */
|
||||
const missingFields = expectedFields.filter(eF => meta.fields?.find(mF => eF === mF));
|
||||
const missingFields = expectedFields.filter(eF => !meta.fields?.includes(eF));
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
||||
|
@@ -20,7 +20,7 @@ import {
|
||||
useTranslate,
|
||||
useUnselectAll,
|
||||
} from "react-admin";
|
||||
import { useMutation } from "react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||
const translate = useTranslate();
|
||||
@@ -43,7 +43,6 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||
<TextInput
|
||||
source="body"
|
||||
label="resources.servernotices.fields.body"
|
||||
fullWidth
|
||||
multiline
|
||||
rows="4"
|
||||
resettable
|
||||
@@ -64,6 +63,10 @@ export const ServerNoticeButton = () => {
|
||||
const handleDialogOpen = () => setOpen(true);
|
||||
const handleDialogClose = () => setOpen(false);
|
||||
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSend = (values: Partial<RaRecord>) => {
|
||||
create(
|
||||
"servernotices",
|
||||
@@ -100,13 +103,12 @@ export const ServerNoticeBulkButton = () => {
|
||||
const unselectAllUsers = useUnselectAll("users");
|
||||
const dataProvider = useDataProvider();
|
||||
|
||||
const { mutate: sendNotices, isLoading } = useMutation(
|
||||
data =>
|
||||
const { mutate: sendNotices, isPending } = useMutation({
|
||||
mutationFn: (data) =>
|
||||
dataProvider.createMany("servernotices", {
|
||||
ids: selectedIds,
|
||||
data: data,
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify("resources.servernotices.action.send_success");
|
||||
unselectAllUsers();
|
||||
@@ -116,12 +118,11 @@ export const ServerNoticeBulkButton = () => {
|
||||
notify("resources.servernotices.action.send_failure", {
|
||||
type: "error",
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
|
||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isPending}>
|
||||
<MessageIcon />
|
||||
</Button>
|
||||
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
||||
|
@@ -1,6 +0,0 @@
|
||||
export type MatrixError = {
|
||||
errcode: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const displayError = (errcode: string, status: number, message: string) => `${errcode} (${status}): ${message}`;
|
@@ -28,7 +28,7 @@ import {
|
||||
useRefresh,
|
||||
useTranslate,
|
||||
} from "react-admin";
|
||||
import { useMutation } from "react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { dateParser } from "./date";
|
||||
@@ -55,14 +55,12 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||
<DateTimeInput
|
||||
fullWidth
|
||||
source="before_ts"
|
||||
label="delete_media.fields.before_ts"
|
||||
defaultValue={0}
|
||||
parse={dateParser}
|
||||
/>
|
||||
<NumberInput
|
||||
fullWidth
|
||||
source="size_gt"
|
||||
label="delete_media.fields.size_gt"
|
||||
defaultValue={0}
|
||||
@@ -70,7 +68,6 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||
step={1024}
|
||||
/>
|
||||
<BooleanInput
|
||||
fullWidth
|
||||
source="keep_profiles"
|
||||
label="delete_media.fields.keep_profiles"
|
||||
defaultValue={true}
|
||||
@@ -86,9 +83,8 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const notify = useNotify();
|
||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||
const { mutate: deleteMedia, isLoading } = useMutation(
|
||||
(values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||
{
|
||||
const { mutate: deleteMedia, isPending } = useMutation({
|
||||
mutationFn: (values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||
onSuccess: () => {
|
||||
notify("delete_media.action.send_success");
|
||||
closeDialog();
|
||||
@@ -98,8 +94,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const openDialog = () => setOpen(true);
|
||||
const closeDialog = () => setOpen(false);
|
||||
@@ -110,7 +105,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||
{...props}
|
||||
label="delete_media.action.send"
|
||||
onClick={openDialog}
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
sx={{
|
||||
color: theme.palette.error.main,
|
||||
"&:hover": {
|
||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const de: SynapseTranslationMessages = {
|
||||
...formalGermanMessages,
|
||||
ra: {
|
||||
...formalGermanMessages.ra,
|
||||
navigation: {
|
||||
...formalGermanMessages.ra.navigation,
|
||||
no_filtered_results: "Keine Ergebnisse",
|
||||
clear_filters: "Alle Filter entfernen",
|
||||
},
|
||||
},
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "Heimserver URL",
|
||||
@@ -125,7 +133,6 @@ const de: SynapseTranslationMessages = {
|
||||
erased: "Gelöscht",
|
||||
guests: "Zeige Gäste",
|
||||
show_deactivated: "Zeige deaktivierte Benutzer",
|
||||
show_locked: "Zeige gesperrte Benutzer",
|
||||
user_id: "Suche Benutzer",
|
||||
displayname: "Anzeigename",
|
||||
password: "Passwort",
|
||||
@@ -143,7 +150,6 @@ const de: SynapseTranslationMessages = {
|
||||
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
||||
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
||||
erase: "DSGVO konformes Löschen der Benutzerdaten",
|
||||
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt",
|
||||
},
|
||||
action: {
|
||||
erase: "Lösche Benutzerdaten",
|
||||
@@ -199,11 +205,6 @@ const de: SynapseTranslationMessages = {
|
||||
title: "Raum löschen",
|
||||
content:
|
||||
"Sind Sie sicher dass Sie den Raum löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Alle Nachrichten und Medien, die der Raum beinhaltet werden vom Server gelöscht!",
|
||||
fields: {
|
||||
block: "Benutzer blockieren und daran hindern, dem Raum beizutreten",
|
||||
},
|
||||
success: "Raum/Räume erfolgreich gelöscht.",
|
||||
failure: "Der/die Raum/Räume konnten nicht gelöscht werden.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -142,7 +142,6 @@ const en: SynapseTranslationMessages = {
|
||||
password: "Changing password will log user out of all sessions.",
|
||||
deactivate: "You must provide a password to re-activate an account.",
|
||||
erase: "Mark the user as GDPR-erased",
|
||||
erase_admin_error: "Deleting own user is not allowed.",
|
||||
},
|
||||
action: {
|
||||
erase: "Erase user data",
|
||||
@@ -198,11 +197,6 @@ const en: SynapseTranslationMessages = {
|
||||
title: "Delete room",
|
||||
content:
|
||||
"Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!",
|
||||
fields: {
|
||||
block: "Block and prevent users from joining the room",
|
||||
},
|
||||
success: "Room/s successfully deleted.",
|
||||
failure: "The room/s could not be deleted.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -120,7 +120,6 @@ const fa: SynapseTranslationMessages = {
|
||||
deactivated: "غیرفعال",
|
||||
guests: "نمایش مهمانان",
|
||||
show_deactivated: "نمایش کاربران غیرفعال شده",
|
||||
show_locked: "نمایش کاربران قفل شده",
|
||||
user_id: "جستجوی کاربر",
|
||||
displayname: "نام نمایشی",
|
||||
password: "رمز عبور",
|
||||
|
@@ -124,7 +124,6 @@ const fr: SynapseTranslationMessages = {
|
||||
deactivated: "Désactivé",
|
||||
guests: "Afficher les visiteurs",
|
||||
show_deactivated: "Afficher les utilisateurs désactivés",
|
||||
show_locked: "Afficher les utilisateurs verrouillés",
|
||||
user_id: "Rechercher un utilisateur",
|
||||
displayname: "Nom d'affichage",
|
||||
password: "Mot de passe",
|
||||
@@ -140,7 +139,6 @@ const fr: SynapseTranslationMessages = {
|
||||
helper: {
|
||||
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
||||
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
||||
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
||||
},
|
||||
action: {
|
||||
erase: "Effacer les données de l'utilisateur",
|
||||
@@ -196,11 +194,6 @@ const fr: SynapseTranslationMessages = {
|
||||
title: "Supprimer le salon",
|
||||
content:
|
||||
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
|
||||
fields: {
|
||||
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
|
||||
},
|
||||
success: "Salle/s supprimées avec succès.",
|
||||
failure: "La/les salle/s n'ont pas pu être supprimées.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
6
src/i18n/index.d.ts
vendored
6
src/i18n/index.d.ts
vendored
@@ -138,7 +138,6 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
password?: string;
|
||||
deactivate: string;
|
||||
erase: string;
|
||||
erase_admin_error: string;
|
||||
};
|
||||
action: {
|
||||
erase: string;
|
||||
@@ -192,11 +191,6 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
erase: {
|
||||
title: string;
|
||||
content: string;
|
||||
fields: {
|
||||
block: string;
|
||||
},
|
||||
success: string;
|
||||
failure: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@@ -121,7 +121,6 @@ const it: SynapseTranslationMessages = {
|
||||
deactivated: "Disattivato",
|
||||
guests: "Mostra gli ospiti",
|
||||
show_deactivated: "Mostra gli utenti disattivati",
|
||||
show_locked: "Mostra gli utenti bloccati",
|
||||
user_id: "Cerca utente",
|
||||
displayname: "Nickname",
|
||||
password: "Password",
|
||||
@@ -142,7 +141,6 @@ const it: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "Cancella i dati dell'utente",
|
||||
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const ru: SynapseTranslationMessages = {
|
||||
...russianMessages,
|
||||
ra: {
|
||||
...russianMessages.ra,
|
||||
navigation: {
|
||||
...russianMessages.ra.navigation,
|
||||
no_filtered_results: "Нет результатов",
|
||||
clear_filters: "Все фильтры сбросить",
|
||||
},
|
||||
},
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "Адрес домашнего сервера",
|
||||
@@ -133,7 +141,6 @@ const ru: SynapseTranslationMessages = {
|
||||
erased: "Удалён",
|
||||
guests: "Показывать гостей",
|
||||
show_deactivated: "Показывать деактивированных",
|
||||
show_locked: "Показывать заблокированных",
|
||||
user_id: "Поиск пользователя",
|
||||
displayname: "Отображаемое имя",
|
||||
password: "Пароль",
|
||||
@@ -151,7 +158,6 @@ const ru: SynapseTranslationMessages = {
|
||||
password: "Смена пароля завершит все сессии пользователя.",
|
||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||
},
|
||||
action: {
|
||||
erase: "Удалить данные пользователя",
|
||||
@@ -210,11 +216,6 @@ const ru: SynapseTranslationMessages = {
|
||||
title: "Удалить комнату",
|
||||
content:
|
||||
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
|
||||
fields: {
|
||||
block: "Заблокировать и запретить пользователям присоединяться к комнате",
|
||||
},
|
||||
success: "Комната/ы успешно удалены",
|
||||
failure: "Комната/ы не могут быть удалены.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const zh: SynapseTranslationMessages = {
|
||||
...chineseMessages,
|
||||
ra: {
|
||||
...chineseMessages.ra,
|
||||
navigation: {
|
||||
...chineseMessages.ra.navigation,
|
||||
no_filtered_results: "没有结果",
|
||||
clear_filters: "清除所有过滤器",
|
||||
},
|
||||
},
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "服务器 URL",
|
||||
@@ -120,7 +128,6 @@ const zh: SynapseTranslationMessages = {
|
||||
deactivated: "被禁用",
|
||||
guests: "显示访客",
|
||||
show_deactivated: "显示被禁用的账户",
|
||||
show_locked: "显示被锁定的账户",
|
||||
user_id: "搜索用户",
|
||||
displayname: "显示名字",
|
||||
password: "密码",
|
||||
@@ -135,7 +142,6 @@ const zh: SynapseTranslationMessages = {
|
||||
helper: {
|
||||
deactivate: "您必须提供一串密码来激活账户。",
|
||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||
erase_admin_error: "不允许删除自己的用户",
|
||||
},
|
||||
action: {
|
||||
erase: "抹除用户信息",
|
||||
|
@@ -5,7 +5,7 @@ import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { AppContext } from "./AppContext";
|
||||
|
||||
fetch("config.json")
|
||||
fetch(`${import.meta.env.BASE_URL}/config.json`)
|
||||
.then(res => res.json())
|
||||
.then(props =>
|
||||
createRoot(document.getElementById("root")).render(
|
||||
|
@@ -168,9 +168,7 @@ const LoginPage = () => {
|
||||
const [matrixVersions, setMatrixVersions] = useState("");
|
||||
|
||||
const handleUsernameChange = () => {
|
||||
if (formData.base_url || allowSingleBaseUrl) {
|
||||
return;
|
||||
}
|
||||
if (formData.base_url || allowSingleBaseUrl) return;
|
||||
// check if username is a full qualified userId then set base_url accordingly
|
||||
const domain = splitMxid(formData.username)?.domain;
|
||||
if (domain) {
|
||||
@@ -182,9 +180,6 @@ const LoginPage = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!formData.base_url) {
|
||||
form.setValue("base_url", "");
|
||||
}
|
||||
if (formData.base_url === "" && allowMultipleBaseUrls) {
|
||||
form.setValue("base_url", restrictBaseUrl[0]);
|
||||
}
|
||||
@@ -222,7 +217,6 @@ const LoginPage = () => {
|
||||
disabled={loading || !supportPassAuth}
|
||||
onBlur={handleUsernameChange}
|
||||
resettable
|
||||
fullWidth
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
@@ -234,7 +228,6 @@ const LoginPage = () => {
|
||||
autoComplete="current-password"
|
||||
disabled={loading || !supportPassAuth}
|
||||
resettable
|
||||
fullWidth
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
@@ -247,7 +240,6 @@ const LoginPage = () => {
|
||||
disabled={loading}
|
||||
readOnly={allowSingleBaseUrl}
|
||||
resettable={allowAnyBaseUrl}
|
||||
fullWidth
|
||||
validate={[required(), validateBaseUrl]}
|
||||
>
|
||||
{allowMultipleBaseUrls &&
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { get } from "lodash";
|
||||
import { MouseEvent } from "react";
|
||||
|
||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
import { blue } from "@mui/material/colors";
|
||||
import {
|
||||
Button,
|
||||
Datagrid,
|
||||
@@ -31,17 +33,10 @@ import {
|
||||
} from "react-admin";
|
||||
|
||||
import { DATE_FORMAT } from "../components/date";
|
||||
import { get } from "lodash";
|
||||
import { lighten, useTheme } from '@mui/material';
|
||||
|
||||
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||
|
||||
const destinationRowSx = (record: RaRecord) => ({
|
||||
backgroundColor: record.retry_last_ts > 0 ? "warning.light" : "primary.contrastText",
|
||||
"& .MuiButtonBase-root": {
|
||||
color: "primary.dark",
|
||||
},
|
||||
});
|
||||
|
||||
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
||||
|
||||
export const DestinationReconnectButton = () => {
|
||||
@@ -106,6 +101,16 @@ const RetryDateField = (props: DateFieldProps) => {
|
||||
};
|
||||
|
||||
export const DestinationList = (props: ListProps) => {
|
||||
const { palette: { error, mode }, } = useTheme();
|
||||
const destinationRowSx = (record: RaRecord) => ({
|
||||
backgroundColor: record.retry_last_ts > 0 ? lighten(error[mode], 0.5) : undefined,
|
||||
"& > td": mode === 'dark' ? {
|
||||
color: record.retry_last_ts > 0 ? "black" : "white",
|
||||
"& > button": {
|
||||
color: blue[700],
|
||||
},
|
||||
} : undefined,
|
||||
});
|
||||
return (
|
||||
<List
|
||||
{...props}
|
||||
|
@@ -25,7 +25,7 @@ import {
|
||||
useRefresh,
|
||||
useUnselectAll,
|
||||
} from "react-admin";
|
||||
import { useMutation } from "react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import AvatarField from "../components/AvatarField";
|
||||
|
||||
@@ -70,13 +70,12 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
||||
const refresh = useRefresh();
|
||||
const unselectAllRooms = useUnselectAll("rooms");
|
||||
const dataProvider = useDataProvider();
|
||||
const { mutate, isLoading } = useMutation(
|
||||
() =>
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: () =>
|
||||
dataProvider.createMany("room_directory", {
|
||||
ids: selectedIds,
|
||||
data: {},
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify("resources.room_directory.action.send_success");
|
||||
unselectAllRooms();
|
||||
@@ -86,11 +85,10 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
||||
notify("resources.room_directory.action.send_failure", {
|
||||
type: "error",
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isLoading}>
|
||||
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isPending}>
|
||||
<RoomDirectoryIcon />
|
||||
</Button>
|
||||
);
|
||||
@@ -102,6 +100,10 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
||||
const refresh = useRefresh();
|
||||
const [create, { isLoading }] = useCreate();
|
||||
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
create(
|
||||
"room_directory",
|
||||
|
@@ -36,7 +36,6 @@ import {
|
||||
TopToolbar,
|
||||
useRecordContext,
|
||||
useTranslate,
|
||||
useListContext,
|
||||
} from "react-admin";
|
||||
|
||||
import {
|
||||
@@ -46,7 +45,6 @@ import {
|
||||
RoomDirectoryPublishButton,
|
||||
} from "./room_directory";
|
||||
import { DATE_FORMAT } from "../components/date";
|
||||
import DeleteRoomButton from "../components/DeleteRoomButton";
|
||||
|
||||
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||
|
||||
@@ -72,8 +70,8 @@ const RoomShowActions = () => {
|
||||
return (
|
||||
<TopToolbar>
|
||||
{publishButton}
|
||||
<DeleteRoomButton
|
||||
selectedIds={[record.id]}
|
||||
<DeleteButton
|
||||
mutationMode="pessimistic"
|
||||
confirmTitle="resources.rooms.action.erase.title"
|
||||
confirmContent="resources.rooms.action.erase.content"
|
||||
/>
|
||||
@@ -209,20 +207,17 @@ export const RoomShow = (props: ShowProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const RoomBulkActionButtons = () => {
|
||||
const record = useListContext();
|
||||
return (
|
||||
const RoomBulkActionButtons = () => (
|
||||
<>
|
||||
<RoomDirectoryBulkPublishButton />
|
||||
<RoomDirectoryBulkUnpublishButton />
|
||||
<DeleteRoomButton
|
||||
selectedIds={record.selectedIds}
|
||||
<BulkDeleteButton
|
||||
confirmTitle="resources.rooms.action.erase.title"
|
||||
confirmContent="resources.rooms.action.erase.content"
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const roomFilters = [<SearchInput source="search_term" alwaysOn />];
|
||||
|
||||
|
@@ -8,8 +8,6 @@ import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
||||
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, ownerDocument } from "@mui/material";
|
||||
import {
|
||||
ArrayInput,
|
||||
ArrayField,
|
||||
@@ -44,17 +42,12 @@ import {
|
||||
useRecordContext,
|
||||
useTranslate,
|
||||
Pagination,
|
||||
SaveButton,
|
||||
CreateButton,
|
||||
ExportButton,
|
||||
TopToolbar,
|
||||
Toolbar,
|
||||
NumberField,
|
||||
useListContext,
|
||||
useNotify,
|
||||
ToolbarClasses,
|
||||
Identifier,
|
||||
RaRecord,
|
||||
} from "react-admin";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
@@ -101,51 +94,16 @@ const userFilters = [
|
||||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||
];
|
||||
|
||||
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode, ownUserIsSelected: boolean }> = (props) => {
|
||||
const ownUserIsSelected = props.ownUserIsSelected;
|
||||
const notify = useNotify();
|
||||
const translate = useTranslate();
|
||||
|
||||
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (ownUserIsSelected) {
|
||||
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>)
|
||||
ev.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return <div onClickCapture={handleDeleteClick}>
|
||||
{props.children}
|
||||
</div>
|
||||
};
|
||||
|
||||
const UserBulkActionButtons = () => {
|
||||
const record = useListContext();
|
||||
const [ ownUserIsSelected, setOwnUserIsSelected ] = useState(false);
|
||||
const selectedIds = record.selectedIds;
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
const notify = useNotify();
|
||||
const translate = useTranslate();
|
||||
|
||||
useEffect(() => {
|
||||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||||
}, [ selectedIds ]);
|
||||
|
||||
|
||||
return <>
|
||||
const UserBulkActionButtons = () => (
|
||||
<>
|
||||
<ServerNoticeBulkButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<BulkDeleteButton
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle="resources.users.helper.erase"
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</UserPreventSelfDelete>
|
||||
</>
|
||||
};
|
||||
|
||||
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
||||
return `/users/${id}`;
|
||||
};
|
||||
);
|
||||
|
||||
export const UserList = (props: ListProps) => (
|
||||
<List
|
||||
@@ -156,7 +114,10 @@ export const UserList = (props: ListProps) => (
|
||||
actions={<UserListActions />}
|
||||
pagination={<UserPagination />}
|
||||
>
|
||||
<Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}>
|
||||
<Datagrid
|
||||
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
|
||||
bulkActionButtons={<UserBulkActionButtons />}
|
||||
>
|
||||
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
||||
<TextField source="id" sortBy="name" />
|
||||
<TextField source="displayname" />
|
||||
@@ -181,16 +142,10 @@ const validateAddress = [required(), maxLength(255)];
|
||||
const UserEditActions = () => {
|
||||
const record = useRecordContext();
|
||||
const translate = useTranslate();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
let ownUserIsSelected = false;
|
||||
if (record && record.id) {
|
||||
ownUserIsSelected = record.id === ownUserId;
|
||||
}
|
||||
|
||||
return (
|
||||
<TopToolbar>
|
||||
{!record?.deactivated && <ServerNoticeButton />}
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<DeleteButton
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle={translate("resources.users.helper.erase", {
|
||||
@@ -198,15 +153,17 @@ const UserEditActions = () => {
|
||||
})}
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</UserPreventSelfDelete>
|
||||
</TopToolbar>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserCreate = (props: CreateProps) => (
|
||||
<Create { ...props} redirect={(resource, id, data) => {
|
||||
return `users/${id}`;
|
||||
}}>
|
||||
<Create
|
||||
{...props}
|
||||
redirect={(resource: string | undefined, id: Identifier | undefined) => {
|
||||
return `${resource}/${id}`;
|
||||
}}
|
||||
>
|
||||
<SimpleForm>
|
||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||
<TextInput source="displayname" validate={maxLength(256)} />
|
||||
@@ -237,49 +194,16 @@ const UserTitle = () => {
|
||||
{translate("resources.users.name", {
|
||||
smart_count: 1,
|
||||
})}{" "}
|
||||
{record ? ( record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
||||
{record ? `"${record.displayname}"` : ""}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const UserEditToolbar = () => {
|
||||
const record = useRecordContext();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
let ownUserIsSelected = false;
|
||||
if (record && record.id) {
|
||||
ownUserIsSelected = record.id === ownUserId;
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={ToolbarClasses.defaultToolbar}>
|
||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||
<SaveButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<DeleteButton />
|
||||
</UserPreventSelfDelete>
|
||||
</Toolbar>
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
|
||||
const UserBooleanInput = (props) => {
|
||||
const record = useRecordContext();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
const isOwnUser = false;
|
||||
let ownUserIsSelected = false;
|
||||
if (record && (record.id === ownUserId)) {
|
||||
ownUserIsSelected = true;
|
||||
}
|
||||
|
||||
return <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}><BooleanInput {...props} disabled={ownUserIsSelected} /></UserPreventSelfDelete>
|
||||
}
|
||||
|
||||
export const UserEdit = (props: EditProps) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||
<TabbedForm>
|
||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
|
||||
<TextInput source="id" disabled />
|
||||
@@ -288,7 +212,7 @@ export const UserEdit = (props: EditProps) => {
|
||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||||
<BooleanInput source="admin" />
|
||||
<BooleanInput source="locked" />
|
||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||
<BooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||
<BooleanInput source="erased" disabled />
|
||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||
<TextField source="consent_version" />
|
||||
|
@@ -2,7 +2,6 @@ import fetchMock from "jest-fetch-mock";
|
||||
|
||||
import authProvider from "./authProvider";
|
||||
import storage from "../storage";
|
||||
import { HttpError } from "ra-core";
|
||||
|
||||
fetchMock.enableMocks();
|
||||
|
||||
@@ -31,7 +30,17 @@ describe("authProvider", () => {
|
||||
|
||||
expect(ret).toBe(undefined);
|
||||
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
||||
body: JSON.stringify({
|
||||
device_id: null,
|
||||
initial_device_display_name: "Synapse Admin",
|
||||
type: "m.login.password",
|
||||
user: "@user:example.com",
|
||||
password: "secret",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: "@user:example.com",
|
||||
}
|
||||
}),
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
@@ -105,7 +114,7 @@ describe("authProvider", () => {
|
||||
});
|
||||
|
||||
it("should reject if error.status is 403", async () => {
|
||||
await expect(authProvider.checkError(new HttpError("test-error", 403, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined();
|
||||
await expect(authProvider.checkError({ status: 403 })).rejects.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { AuthProvider, HttpError, Options, fetchUtils, useTranslate } from "react-admin";
|
||||
import { AuthProvider, Options, fetchUtils } from "react-admin";
|
||||
|
||||
import storage from "../storage";
|
||||
import { MatrixError, displayError } from "../components/error";
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
// called when the user attempts to log in
|
||||
@@ -32,11 +31,12 @@ const authProvider: AuthProvider = {
|
||||
}
|
||||
: {
|
||||
type: "m.login.password",
|
||||
user: username,
|
||||
password: password,
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: username,
|
||||
},
|
||||
password: password,
|
||||
}
|
||||
)
|
||||
),
|
||||
@@ -45,36 +45,13 @@ const authProvider: AuthProvider = {
|
||||
// use the base_url from login instead of the well_known entry from the
|
||||
// server, since the admin might want to access the admin API via some
|
||||
// private address
|
||||
if (!base_url) {
|
||||
// there is some kind of bug with base_url being present in the form, but not submitted
|
||||
// ref: https://github.com/etkecc/synapse-admin/issues/14
|
||||
storage.removeItem("base_url")
|
||||
throw new Error("Homeserver URL is required.");
|
||||
}
|
||||
base_url = base_url.replace(/\/+$/g, "");
|
||||
storage.setItem("base_url", base_url);
|
||||
|
||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetchUtils.fetchJson(login_api_url, options);
|
||||
} catch(err) {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
const errorBody = error.body as MatrixError;
|
||||
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message);
|
||||
|
||||
return Promise.reject(
|
||||
new HttpError(
|
||||
errMsg,
|
||||
errorStatus,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const json = response.json;
|
||||
const { json } = await fetchUtils.fetchJson(login_api_url, options);
|
||||
storage.setItem("home_server", json.home_server);
|
||||
storage.setItem("user_id", json.user_id);
|
||||
storage.setItem("access_token", json.access_token);
|
||||
@@ -101,12 +78,10 @@ const authProvider: AuthProvider = {
|
||||
}
|
||||
},
|
||||
// called when the API returns an error
|
||||
checkError: (err: HttpError) => {
|
||||
const errorBody = err.body as MatrixError;
|
||||
const status = err.status;
|
||||
|
||||
checkError: ({ status }: { status: number }) => {
|
||||
console.log("checkError " + status);
|
||||
if (status === 401 || status === 403) {
|
||||
return Promise.reject({message: displayError(errorBody.errcode, status, errorBody.error)});
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { stringify } from "query-string";
|
||||
|
||||
import { DataProvider, DeleteParams, HttpError, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
||||
|
||||
import {
|
||||
DataProvider,
|
||||
DeleteParams,
|
||||
Identifier,
|
||||
Options,
|
||||
PaginationPayload,
|
||||
RaRecord,
|
||||
SortPayload,
|
||||
fetchUtils
|
||||
} from "react-admin";
|
||||
import storage from "../storage";
|
||||
import { MatrixError, displayError } from "../components/error";
|
||||
|
||||
// Adds the access token to all requests
|
||||
const jsonClient = async (url: string, options: Options = {}) => {
|
||||
const jsonClient = (url: string, options: Options = {}) => {
|
||||
const token = storage.getItem("access_token");
|
||||
console.log("httpClient " + url);
|
||||
if (token != null) {
|
||||
@@ -15,17 +21,7 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
||||
token: `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const response = await fetchUtils.fetchJson(url, options);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
const errorBody = error.body as MatrixError;
|
||||
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message);
|
||||
|
||||
return Promise.reject(new HttpError(errMsg, errorStatus, errorBody));
|
||||
}
|
||||
return fetchUtils.fetchJson(url, options);
|
||||
};
|
||||
|
||||
const mxcUrlToHttp = (mxcUrl: string) => {
|
||||
@@ -270,7 +266,7 @@ const resourceMap = {
|
||||
total: json => json.total_rooms,
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
|
||||
body: { block: params.meta?.block ?? false },
|
||||
body: { block: false },
|
||||
}),
|
||||
},
|
||||
reports: {
|
||||
@@ -503,8 +499,8 @@ const dataProvider: SynapseDataProvider = {
|
||||
getList: async (resource, params) => {
|
||||
console.log("getList " + resource);
|
||||
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
||||
const { page, perPage } = params.pagination;
|
||||
const { field, order } = params.sort;
|
||||
const { page, perPage } = params.pagination as PaginationPayload;
|
||||
const { field, order } = params.sort as SortPayload;
|
||||
const from = (page - 1) * perPage;
|
||||
const query = {
|
||||
from: from,
|
||||
|
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
vitePluginVersionMark({
|
||||
command: "git describe --tags || git rev-parse --short HEAD",
|
||||
command: "git describe --tags",
|
||||
ifMeta: true,
|
||||
ifLog: true,
|
||||
ifGlobal: true,
|
||||
|
Reference in New Issue
Block a user