Compare commits

..

70 Commits

Author SHA1 Message Date
Michael Albert 95de50b925 Bump version to 0.8.5
Change-Id: I8c8f0af01d693bb76dbf706a92fdd4dfa6ba9a8b
2022-02-17 20:56:27 +01:00
dependabot[bot] 1a150a10fd Bump url-parse from 1.5.3 to 1.5.7 (#244)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-17 20:53:56 +01:00
Dirk Klimpel 158e7dbe98 Move date format to a constant in EventReports.js (#237) 2022-02-17 20:52:13 +01:00
Dirk Klimpel a642f11503 Fix typo notification in german language file (#233) 2022-02-17 20:51:42 +01:00
Dirk Klimpel 888a3f001b Move date format to a constant in rooms.js (#238) 2022-02-17 20:51:11 +01:00
Dirk Klimpel 4b0845bee8 Move date format to a constant in users.js (#223)
* Move date format to a constant in `users.js`

* yarn prettier
2022-02-17 20:50:38 +01:00
Dirk Klimpel f449e3277a Replace deprecated fade in devices.js (#220) 2022-02-17 20:49:34 +01:00
Dirk Klimpel 3303f253b4 Replace deprecated fade in media.js (#221) 2022-02-17 20:49:19 +01:00
dependabot[bot] 0250954ee7 Bump follow-redirects from 1.14.7 to 1.14.8 (#243)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-17 20:45:37 +01:00
Dirk Klimpel efed8b2774 Add token based registration creation and management (#200)
* Add token based registration creation and management

* yarn fix

* Apply suggestions from code review

Remove empty line

* move date to `const date_format`
2022-02-17 20:45:21 +01:00
Nya Candy c891afa611 feat: support SSO login (#196)
* feat: support SSO login

* fix: lint

* fix: add back homeserver force protection

* fix: add back login notice

* fix: simplify login options
2022-02-17 20:24:46 +01:00
Michael Albert 38541b8f02 Fix english translation for room deletion
Change-Id: I5bb02e64832902a79379d66c77d3128169d3fca8
2022-02-17 20:15:49 +01:00
Dirk Klimpel 0f4c382c18 Remove not needed translation backtolist (#208) 2022-01-31 17:50:14 +01:00
Dirk Klimpel b90d4ef00f Add more headlines for installation to README (#216) 2022-01-31 17:39:18 +01:00
Dominik Fuchß 3fb33facc5 GitHub Pages Deploy (#189)
* Create edge_ghpage.yml

Added Build and Deploy Edge version to GH Pages

* Update edge_ghpage.yml

Added missing node setup

* Update edge_ghpage.yml

Restrict building of GH Pages to main / master branch & workflow dispatch

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>
2022-01-31 17:38:34 +01:00
Dirk Klimpel c4a68ff1d5 Fix typo in .prettierrc (#224) 2022-01-31 17:24:37 +01:00
dependabot[bot] c4f0fa48ec Bump nanoid from 3.1.30 to 3.2.0 (#229)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.2.0)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 17:20:39 +01:00
dependabot[bot] 26ed63d65e Bump follow-redirects from 1.14.6 to 1.14.7 (#227)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 17:20:29 +01:00
Michael Albert b9e81b2278 Bump version and update dependencies
Change-Id: I0c0349b4429ce06bea51453c092f0a11156aaa05
2021-12-17 20:38:18 +01:00
sakkiii f6f437b17a version tag on docker hub (#187)
* version tag on docker hub

* resolve name collision

* master branch added for latest tag

* prettier-ignore fix
2021-12-08 22:02:43 +01:00
Dirk Klimpel 91af8f1c04 Add sorting users by creation timestamp (#174)
* Add `creation_ts` to list users

* remove filter

* Bring back origin columns sort order
2021-12-08 21:59:09 +01:00
Aaron R abc9d5154e Switch Dockerfile to use current LTS version of Node (#205)
Node 17 current fails due to https://github.com/webpack/webpack/issues/14532. It probably makes sense to use the current LTS version of Node instead of the absolute latest version of Node so these kinds of bleeding edge issues are less likely to happen.
2021-11-15 21:35:23 +01:00
dependabot[bot] 8228d7d2c2 Bump tar from 6.1.8 to 6.1.11 (#207)
Bumps [tar](https://github.com/npm/node-tar) from 6.1.8 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.8...v6.1.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 21:18:58 +01:00
Dirk Klimpel 4adc20f80d replace undoable prop with mutationMode prop (#202) 2021-11-15 21:18:29 +01:00
Dirk Klimpel a5c7d7dd22 Make items in "Room directory" are clickable (#199) 2021-11-15 21:15:11 +01:00
dependabot[bot] dc5c2c1d68 Bump tmpl from 1.0.4 to 1.0.5 (#193)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 21:11:49 +01:00
Dirk Klimpel 42b3252353 Add pristine to UserEdit and ServerNotice (#185) 2021-11-15 21:02:47 +01:00
Dirk Klimpel 1a17d3e69b Automatically set the homeserver for a new user (#184)
and enhance form validation
2021-11-15 20:57:38 +01:00
Dirk Klimpel 79ef38ee6b Enable modify user external_ids (#179)
* Enable modify user `external_ids`

* add input validation
2021-11-15 20:40:05 +01:00
Manuel Stahl 0ff4b30d71 Remove update repo stop from docker release action
See https://github.com/peter-evans/dockerhub-description/issues/10

Change-Id: I42d2d4e1a28117be1419591f7d43653591182f0c
2021-08-26 10:43:11 +02:00
Manuel Stahl 6c4ff6c791 Add write permissions to github release action
Change-Id: Ie7db1e7410bbc1c0fccbc2d00119363629e10f22
2021-08-26 10:40:11 +02:00
Manuel Stahl 9c36ed6566 Increment version
Change-Id: I48fb812632dc2e498ad267477809a16fc882cf4c
2021-08-25 10:42:34 +02:00
dklimpel 8536f552d4 Add button to quarantine media (#180)
Change-Id: I6496826fdf75ab8b7b3ed5a9056abf86a50caea3
2021-08-25 10:42:34 +02:00
Manuel Stahl aaf782d24f Use .gitignore for prettier as well
Change-Id: Ibfe73812a5375cc251b859b8aa441583c0cdcd41
2021-08-25 10:42:34 +02:00
Manuel Stahl 2886203594 Add github action that builds docker images and pushes them to docker hub
Change-Id: I60d39cc266c2e8b905e2ac4a367bd5a22b79b57b
2021-08-25 10:42:34 +02:00
csett86 865fc98336 Add github action that packages a release tarball (#148)
Change-Id: I368a834a27f69550596a041c1e6b84afd40011b7
2021-08-24 09:58:35 +02:00
Dirk Klimpel e6f01f035b Add buttons to protect and unprotect users' media from quarantine (#150) 2021-08-19 11:51:07 +02:00
Manuel Stahl 32e088ac5a yarn: Upgrade packages
- babel 7.15
- eslint 7.32
- react-admin 3.17

Change-Id: Ief4ad5870ae721fb432e19686607376b83eff12a
2021-08-18 09:48:41 +02:00
Dirk Klimpel bf3d13916f Add SSO external_ids to user (#168) 2021-08-18 09:33:22 +02:00
Manuel Stahl 07b1df5855 Add Github action badge for build checks 2021-08-18 09:27:25 +02:00
Dirk Klimpel 634341ea07 Add GitHub Action to run CI tests (#162) 2021-08-18 09:18:09 +02:00
Dirk Klimpel 361643c3da Fix link in README.md (#175) 2021-08-17 08:22:36 +02:00
Dirk Klimpel 5262518699 Update links to new Synapse documentation (#164) 2021-07-06 09:57:44 +02:00
Dirk Klimpel 78a282863a Fix broken CI with language files (#165) 2021-07-06 09:56:09 +02:00
Michael Albert ff0201273a Bump version and update packages
Change-Id: If148587c9e5895ab6b8a70ec34c9190f0bb8f2e0
2021-07-05 14:48:25 +02:00
Dirk Klimpel e50c95b4be Fix CSV import button (#154) 2021-07-05 14:32:51 +02:00
Dirk Klimpel 9f16e5c6ba Change delete room API to DELETE (#151) 2021-07-05 14:32:44 +02:00
dependabot[bot] 509a45cba4 Bump dns-packet from 1.3.1 to 1.3.4 (#145)
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-15 11:03:47 +02:00
dependabot[bot] 5c6e5a9641 Bump ws from 6.2.1 to 6.2.2 (#152)
Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-15 11:03:31 +02:00
Dirk Klimpel e3d5d51342 Fix broken RoomDirectoryFilter (#155) 2021-06-15 11:03:03 +02:00
Michael Albert 985673b161 Increment version
Change-Id: I149896f55be7840b240d92fed5880e3f5624b857
2021-05-25 15:03:15 +02:00
John Francis Sukamto d72357f64f Update en.js (#144)
Suggested a UI name change for media size (assuming length is size in bytes)
2021-05-25 15:01:13 +02:00
Dirk Klimpel e19c34324b Allow fixed homeserver (#142) 2021-05-18 12:39:53 +02:00
Dirk Klimpel 3ea1f51eb5 Add a new tab to rooms with forward extremities (#107)
Add a new tab to rooms with forward extremities.
2021-05-08 19:10:51 +02:00
Manuel Stahl 229518e456 Show room alias or room id in room list if room has no name
Change-Id: Iad769f31347566ccf0b8a978b31f5123553e9dbc
2021-05-05 20:24:15 +02:00
Dirk Klimpel 5a5a7143af Enable sorting of user list (#133)
New in Synapse 1.32.0
Fixes: #132, #136
2021-05-05 19:36:47 +02:00
Manuel Stahl dda8ba5e85 Update nodejs version for travis
Change-Id: I7d44f5df7d4479efcb1d44f5ba23467effad147e
2021-05-05 19:31:50 +02:00
Manuel Stahl 5208198b76 Replace enzyme with testing-library/react
Enzyme is not compatible with react 17.

Change-Id: If9bca2c482bfe10a18d2ee2bc213dab966849b5b
2021-05-05 19:23:01 +02:00
Manuel Stahl c8082a7198 Remove TestContext from App.test.js
The TestContext is only required for components that depend on react-admin,
but not for the Admin component itself.

Change-Id: I3e07cb6bfa592f1bf59ca282cdf1c2e6c922f619
2021-05-05 19:23:01 +02:00
Michael Albert 10831796e3 Add missing translation
Change-Id: Iab3203742498d2c6768b5885c0522ff8365b58f2
2021-05-05 09:41:05 +00:00
Michael Albert 5ee5288edf Fix some DOM errors
Change-Id: I22a108fd5ce6a344e629e4af0345a0221de44052
2021-05-05 09:40:48 +00:00
Manuel Stahl a5528d9fe7 yarn: Upgrade packages
- eslint 7.25
- ra-language-german 3.13
- react-admin 3.15
- react-dom 17.0
- react-scripts 4.0

Change-Id: Iad982cf647470bc16194000519a72c401009c9fa
2021-05-04 18:42:05 +02:00
Manuel Stahl e2fd934851 Allow base URL with path
Ignore and remove trailing slashes.
Fixes #134.

Change-Id: Iedf266e9a93e6939f7f66707fee59a2b56226216
2021-05-04 18:41:54 +02:00
Manuel Stahl 0bc1ce3226 Reuse device_id for synapse-admin on login
Change-Id: I47bbfd1e33ef8bffb618101ae233aeb093cf0ada
2021-05-04 17:10:49 +02:00
Manuel Stahl 41ce58bac8 Enable sorting in tab of users' media (#138) 2021-05-04 16:18:12 +02:00
Manuel Stahl 57c41cc069 Increment version 2021-05-04 15:06:41 +02:00
Nya Candy 0e3375c5ad Add zh-cn support (#131) 2021-05-04 15:06:41 +02:00
Dirk Klimpel 2cdd41b615 Add a new tab to rooms with state events (#108)
Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2021-05-04 14:42:27 +02:00
Dirk Klimpel 2ab4343970 Add the lists of public rooms on the server (#105)
* Add room directory and the switches to rooms settings

* Fix react admin version
2021-05-04 13:52:43 +02:00
Dirk Klimpel 0268cc0e94 Add joined_local_devices to rooms (#96) 2021-05-04 13:49:20 +02:00
29 changed files with 6156 additions and 4578 deletions
+5
View File
@@ -0,0 +1,5 @@
# This setting allows to fix the homeserver.
# If you set this setting, the user will not be able to select
# the server and have to use synapse-admin with this server.
#REACT_APP_SERVER=https://yourmatrixserver.example.com
+21
View File
@@ -0,0 +1,21 @@
name: build-test
on:
push:
branches: ["master"]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Run tests
run: yarn test
+51
View File
@@ -0,0 +1,51 @@
name: Create docker image(s) and push to docker hub
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:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Calculate docker image tag
id: set-tag
run: |
case "${GITHUB_REF}" in
refs/heads/master|refs/heads/main)
tag=latest
;;
refs/tags/*)
tag=${GITHUB_REF#refs/tags/}
;;
*)
tag=${GITHUB_SHA}
;;
esac
echo "::set-output name=tag::$tag"
- name: Build and Push Tag
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
platforms: linux/amd64,linux/arm64
+26
View File
@@ -0,0 +1,26 @@
name: Build and Deploy Edge version to GH Pages
on:
workflow_dispatch:
push:
branches:
- main
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1
- uses: actions/setup-node@v2
with:
node-version: "14"
- name: Install and Build 🔧
run: |
yarn install
yarn build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.1.5
with:
branch: gh-pages
folder: build
@@ -8,6 +8,9 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -21,7 +24,6 @@ jobs:
mkdir -p dist mkdir -p dist
cp -r build synapse-admin-$version cp -r build synapse-admin-$version
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
rm -r synapse-admin-$version
- uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf - uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
with: with:
files: dist/*.tar.gz files: dist/*.tar.gz
+1 -1
View File
@@ -7,5 +7,5 @@
"trailingComma": "es5", "trailingComma": "es5",
"bracketSpacing": true, "bracketSpacing": true,
"jsxBracketSameLine": false, "jsxBracketSameLine": false,
"arrowParens": "avoid", "arrowParens": "avoid"
} }
+1 -1
View File
@@ -1,5 +1,5 @@
language: node_js language: node_js
node_js: node_js:
- 13 - lts/*
cache: yarn cache: yarn
+1 -1
View File
@@ -1,5 +1,5 @@
# Builder # Builder
FROM node:current as builder FROM node:lts as builder
WORKDIR /src WORKDIR /src
+26 -10
View File
@@ -1,13 +1,14 @@
[![Build Status](https://travis-ci.org/Awesome-Technologies/synapse-admin.svg?branch=master)](https://travis-ci.org/Awesome-Technologies/synapse-admin) [![Build Status](https://travis-ci.org/Awesome-Technologies/synapse-admin.svg?branch=master)](https://travis-ci.org/Awesome-Technologies/synapse-admin)
[![build-test](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
# Synapse admin ui # Synapse admin ui
This project is built using [react-admin](https://marmelab.com/react-admin/). This project is built using [react-admin](https://marmelab.com/react-admin/).
It needs at least Synapse v1.23.0 for all functions to work as expected! It needs at least Synapse v1.42.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`. You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/version_api.rst). See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html).
After entering the URL on the login page of synapse-admin the server version appears below the input field. After entering the URL on the login page of synapse-admin the server version appears below the input field.
@@ -16,16 +17,26 @@ You need access to the following endpoints:
- `/_matrix` - `/_matrix`
- `/_synapse/admin` - `/_synapse/admin`
See also [Synapse administration endpoints](https://github.com/matrix-org/synapse/blob/develop/docs/reverse_proxy.md#synapse-administration-endpoints) See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints)
## Step-By-Step install: ## Step-By-Step install
You have two options: You have three options:
1. Download the source code from github and run using nodejs 1. [Download the tarball and serve with any webserver](#steps-for-1)
2. Run the Docker container 2. [Download the source code from github and run using nodejs](#steps-for-2)
3. [Run the Docker container](#steps-for-3)
Steps for 1): ### Steps for 1)
- make sure you have a webserver installed that can serve static files (any webserver like nginx or apache will do)
- configure a vhost for synapse admin on your webserver
- download the .tar.gz from the latest release: https://github.com/Awesome-Technologies/synapse-admin/releases/latest
- unpack the .tar.gz
- move or symlink the `synapse-admin-x.x.x` into your vhosts root dir
- open the url of the vhost in your browser
### Steps for 2)
- make sure you have installed the following: git, yarn, nodejs - make sure you have installed the following: git, yarn, nodejs
- download the source code: `git clone https://github.com/Awesome-Technologies/synapse-admin.git` - download the source code: `git clone https://github.com/Awesome-Technologies/synapse-admin.git`
@@ -33,9 +44,14 @@ Steps for 1):
- download dependencies: `yarn install` - download dependencies: `yarn install`
- start web server: `yarn start` - start web server: `yarn start`
Steps for 2): You can fix the homeserver, so that the user can no longer define it himself.
Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver.example.com yarn start`)
or by editing it in the [.env](.env) file. See also the
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the (docker-compose.yml)[docker-compose.yml]: `docker-compose up -d` ### Steps for 3)
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail. > note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
+13 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.7.0", "version": "0.8.5",
"description": "Admin GUI for the Matrix.org server Synapse", "description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH", "author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -11,25 +11,24 @@
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.1.1", "@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^10.0.2", "@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.0.11", "@testing-library/user-event": "^13.1.8",
"enzyme": "^3.11.0", "eslint": "^7.25.0",
"enzyme-adapter-react-16": "^1.15.2", "eslint-config-prettier": "^8.3.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.2",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^2.0.0", "prettier": "^2.2.0",
"ra-test": "^3.14.0" "ra-test": "^3.15.0"
}, },
"dependencies": { "dependencies": {
"papaparse": "^5.2.0", "papaparse": "^5.2.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"ra-language-german": "^2.1.2", "ra-language-chinese": "^2.0.10",
"ra-language-german": "^3.13.4",
"react": "^17.0.0", "react": "^17.0.0",
"react-admin": "^3.14.0", "react-admin": "^3.15.0",
"react-dom": "^16.14.0", "react-dom": "^17.0.2",
"react-scripts": "^3.4.4" "react-scripts": "^4.0.0"
}, },
"scripts": { "scripts": {
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start", "start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
@@ -37,7 +36,7 @@
"fix:other": "yarn prettier --write", "fix:other": "yarn prettier --write",
"fix:code": "yarn test:lint --fix", "fix:code": "yarn test:lint --fix",
"fix": "yarn fix:code && yarn fix:other", "fix": "yarn fix:code && yarn fix:other",
"prettier": "prettier \"**/*.{js,jsx,json,md,scss,yaml,yml}\"", "prettier": "prettier --ignore-path .gitignore \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
"test:code": "react-scripts test", "test:code": "react-scripts test",
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .", "test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
"test:style": "yarn prettier --list-different", "test:style": "yarn prettier --list-different",
+24
View File
@@ -8,19 +8,29 @@ import { RoomList, RoomShow } from "./components/rooms";
import { ReportList, ReportShow } from "./components/EventReports"; import { ReportList, ReportShow } from "./components/EventReports";
import LoginPage from "./components/LoginPage"; import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group"; import UserIcon from "@material-ui/icons/Group";
import ConfirmationNumberIcon from "@material-ui/icons/ConfirmationNumber";
import EqualizerIcon from "@material-ui/icons/Equalizer"; import EqualizerIcon from "@material-ui/icons/Equalizer";
import { UserMediaStatsList } from "./components/statistics"; import { UserMediaStatsList } from "./components/statistics";
import RoomIcon from "@material-ui/icons/ViewList"; import RoomIcon from "@material-ui/icons/ViewList";
import ReportIcon from "@material-ui/icons/Warning"; import ReportIcon from "@material-ui/icons/Warning";
import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { ImportFeature } from "./components/ImportFeature"; import { ImportFeature } from "./components/ImportFeature";
import {
RegistrationTokenCreate,
RegistrationTokenEdit,
RegistrationTokenList,
} from "./components/RegistrationTokens";
import { RoomDirectoryList } from "./components/RoomDirectory";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import germanMessages from "./i18n/de"; import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en"; import englishMessages from "./i18n/en";
import chineseMessages from "./i18n/zh";
// TODO: Can we use lazy loading together with browser locale? // TODO: Can we use lazy loading together with browser locale?
const messages = { const messages = {
de: germanMessages, de: germanMessages,
en: englishMessages, en: englishMessages,
zh: chineseMessages,
}; };
const i18nProvider = polyglotI18nProvider( const i18nProvider = polyglotI18nProvider(
locale => (messages[locale] ? messages[locale] : messages.en), locale => (messages[locale] ? messages[locale] : messages.en),
@@ -57,6 +67,18 @@ const App = () => (
show={ReportShow} show={ReportShow}
icon={ReportIcon} icon={ReportIcon}
/> />
<Resource
name="room_directory"
list={RoomDirectoryList}
icon={FolderSharedIcon}
/>
<Resource
name="registration_tokens"
list={RegistrationTokenList}
create={RegistrationTokenCreate}
edit={RegistrationTokenEdit}
icon={ConfirmationNumberIcon}
/>
<Resource name="connections" /> <Resource name="connections" />
<Resource name="devices" /> <Resource name="devices" />
<Resource name="room_members" /> <Resource name="room_members" />
@@ -64,6 +86,8 @@ const App = () => (
<Resource name="joined_rooms" /> <Resource name="joined_rooms" />
<Resource name="pushers" /> <Resource name="pushers" />
<Resource name="servernotices" /> <Resource name="servernotices" />
<Resource name="forward_extremities" />
<Resource name="room_state" />
</Admin> </Admin>
); );
+2 -7
View File
@@ -1,14 +1,9 @@
import React from "react"; import React from "react";
import { TestContext } from "ra-test"; import { render } from "@testing-library/react";
import { shallow } from "enzyme";
import App from "./App"; import App from "./App";
describe("App", () => { describe("App", () => {
it("renders", () => { it("renders", () => {
shallow( render(<App />);
<TestContext>
<App />
</TestContext>
);
}); });
}); });
+12 -24
View File
@@ -15,6 +15,15 @@ import {
import PageviewIcon from "@material-ui/icons/Pageview"; import PageviewIcon from "@material-ui/icons/Pageview";
import ViewListIcon from "@material-ui/icons/ViewList"; import ViewListIcon from "@material-ui/icons/ViewList";
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const ReportPagination = props => ( const ReportPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
@@ -33,14 +42,7 @@ export const ReportShow = props => {
<DateField <DateField
source="received_ts" source="received_ts"
showTime showTime
options={{ options={date_format}
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={true} sortable={true}
/> />
<ReferenceField source="user_id" reference="users"> <ReferenceField source="user_id" reference="users">
@@ -72,14 +74,7 @@ export const ReportShow = props => {
<DateField <DateField
source="event_json.origin_server_ts" source="event_json.origin_server_ts"
showTime showTime
options={{ options={date_format}
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={true} sortable={true}
/> />
<ReferenceField source="sender" reference="users"> <ReferenceField source="sender" reference="users">
@@ -116,14 +111,7 @@ export const ReportList = ({ ...props }) => {
<DateField <DateField
source="received_ts" source="received_ts"
showTime showTime
options={{ options={date_format}
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={true} sortable={true}
/> />
<TextField sortable={false} source="user_id" /> <TextField sortable={false} source="user_id" />
+95 -7
View File
@@ -78,10 +78,48 @@ const LoginPage = ({ theme }) => {
const login = useLogin(); const login = useLogin();
const notify = useNotify(); const notify = useNotify();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true);
var locale = useLocale(); var locale = useLocale();
const setLocale = useSetLocale(); const setLocale = useSetLocale();
const translate = useTranslate(); const translate = useTranslate();
const base_url = localStorage.getItem("base_url"); const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER;
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
if (loginToken) {
const ssoToken = loginToken[1];
console.log("SSO token is", ssoToken);
// Prevent further requests
window.history.replaceState(
{},
"",
window.location.href.replace(loginToken[0], "#").split("#")[0]
);
const baseUrl = localStorage.getItem("sso_base_url");
localStorage.removeItem("sso_base_url");
if (baseUrl) {
const auth = {
base_url: baseUrl,
username: null,
password: null,
loginToken: ssoToken,
};
console.log("Base URL is:", baseUrl);
console.log("SSO Token is:", ssoToken);
console.log("Let's try token login...");
login(auth).catch(error => {
alert(
typeof error === "string"
? error
: typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error"
: error.message
);
console.error(error);
});
}
}
const renderInput = ({ const renderInput = ({
meta: { touched, error } = {}, meta: { touched, error } = {},
@@ -111,7 +149,9 @@ const LoginPage = ({ theme }) => {
if (!values.base_url.match(/^(http|https):\/\//)) { if (!values.base_url.match(/^(http|https):\/\//)) {
errors.base_url = translate("synapseadmin.auth.protocol_error"); errors.base_url = translate("synapseadmin.auth.protocol_error");
} else if ( } else if (
!values.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/) !values.base_url.match(
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/
)
) { ) {
errors.base_url = translate("synapseadmin.auth.url_error"); errors.base_url = translate("synapseadmin.auth.url_error");
} }
@@ -134,6 +174,14 @@ const LoginPage = ({ theme }) => {
}); });
}; };
const handleSSO = () => {
localStorage.setItem("sso_base_url", ssoBaseUrl);
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
window.location.href
)}`;
window.location.href = ssoFullUrl;
};
const extractHomeServer = username => { const extractHomeServer = username => {
const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/; const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/;
if (!username) return null; if (!username) return null;
@@ -147,7 +195,7 @@ const LoginPage = ({ theme }) => {
const [serverVersion, setServerVersion] = useState(""); const [serverVersion, setServerVersion] = useState("");
const handleUsernameChange = _ => { const handleUsernameChange = _ => {
if (formData.base_url) return; if (formData.base_url || cfg_base_url) return;
// check if username is a full qualified userId then set base_url accordially // check if username is a full qualified userId then set base_url accordially
const home_server = extractHomeServer(formData.username); const home_server = extractHomeServer(formData.username);
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`; const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
@@ -185,6 +233,31 @@ const LoginPage = ({ theme }) => {
.catch(_ => { .catch(_ => {
setServerVersion(""); setServerVersion("");
}); });
// Set SSO Url
const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`;
let supportPass = false,
supportSSO = false;
fetchUtils
.fetchJson(authMethodUrl, { method: "GET" })
.then(({ json }) => {
json.flows.forEach(f => {
if (f.type === "m.login.password") {
supportPass = true;
} else if (f.type === "m.login.sso") {
supportSSO = true;
}
});
setSupportPassAuth(supportPass);
if (supportSSO) {
setSSOBaseUrl(formData.base_url);
} else {
setSSOBaseUrl("");
}
})
.catch(_ => {
setSSOBaseUrl("");
});
}, },
[formData.base_url] [formData.base_url]
); );
@@ -197,8 +270,9 @@ const LoginPage = ({ theme }) => {
name="username" name="username"
component={renderInput} component={renderInput}
label={translate("ra.auth.username")} label={translate("ra.auth.username")}
disabled={loading} disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange} onBlur={handleUsernameChange}
resettable
fullWidth fullWidth
/> />
</div> </div>
@@ -208,7 +282,8 @@ const LoginPage = ({ theme }) => {
component={renderInput} component={renderInput}
label={translate("ra.auth.password")} label={translate("ra.auth.password")}
type="password" type="password"
disabled={loading} disabled={loading || !supportPassAuth}
resettable
fullWidth fullWidth
/> />
</div> </div>
@@ -217,7 +292,8 @@ const LoginPage = ({ theme }) => {
name="base_url" name="base_url"
component={renderInput} component={renderInput}
label={translate("synapseadmin.auth.base_url")} label={translate("synapseadmin.auth.base_url")}
disabled={loading} disabled={cfg_base_url || loading}
resettable
fullWidth fullWidth
/> />
</div> </div>
@@ -228,7 +304,7 @@ const LoginPage = ({ theme }) => {
return ( return (
<Form <Form
initialValues={{ base_url: base_url }} initialValues={{ base_url: cfg_base_url || base_url }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
validate={validate} validate={validate}
render={({ handleSubmit }) => ( render={({ handleSubmit }) => (
@@ -255,6 +331,7 @@ const LoginPage = ({ theme }) => {
> >
<MenuItem value="de">Deutsch</MenuItem> <MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>
<MenuItem value="zh">简体中文</MenuItem>
</Select> </Select>
</div> </div>
<FormDataConsumer> <FormDataConsumer>
@@ -266,13 +343,24 @@ const LoginPage = ({ theme }) => {
variant="contained" variant="contained"
type="submit" type="submit"
color="primary" color="primary"
disabled={loading} disabled={loading || !supportPassAuth}
className={classes.button} className={classes.button}
fullWidth fullWidth
> >
{loading && <CircularProgress size={25} thickness={2} />} {loading && <CircularProgress size={25} thickness={2} />}
{translate("ra.auth.sign_in")} {translate("ra.auth.sign_in")}
</Button> </Button>
<Button
variant="contained"
color="secondary"
onClick={handleSSO}
disabled={loading || ssoBaseUrl === ""}
className={classes.button}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("synapseadmin.auth.sso_sign_in")}
</Button>
</CardActions> </CardActions>
</Card> </Card>
<Notification /> <Notification />
+2 -2
View File
@@ -1,11 +1,11 @@
import React from "react"; import React from "react";
import { render } from "@testing-library/react";
import { TestContext } from "ra-test"; import { TestContext } from "ra-test";
import { shallow } from "enzyme";
import LoginPage from "./LoginPage"; import LoginPage from "./LoginPage";
describe("LoginForm", () => { describe("LoginForm", () => {
it("renders", () => { it("renders", () => {
shallow( render(
<TestContext> <TestContext>
<LoginPage /> <LoginPage />
</TestContext> </TestContext>
+132
View File
@@ -0,0 +1,132 @@
import React from "react";
import {
BooleanInput,
Create,
Datagrid,
DateField,
DateTimeInput,
Edit,
Filter,
List,
maxValue,
number,
NumberField,
NumberInput,
regex,
SimpleForm,
TextInput,
TextField,
Toolbar,
} from "react-admin";
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
const validateUsesAllowed = [number()];
const validateLength = [number(), maxValue(64)];
const dateParser = v => {
const d = new Date(v);
if (isNaN(d)) return 0;
return d.getTime();
};
const dateFormatter = v => {
if (v === undefined || v === null) return;
const d = new Date(v);
const pad = "00";
const year = d.getFullYear().toString();
const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
const day = (pad + d.getDate().toString()).slice(-2);
const hour = (pad + d.getHours().toString()).slice(-2);
const minute = (pad + d.getMinutes().toString()).slice(-2);
// target format yyyy-MM-ddThh:mm
return `${year}-${month}-${day}T${hour}:${minute}`;
};
const RegistrationTokenFilter = props => (
<Filter {...props}>
<BooleanInput source="valid" alwaysOn />
</Filter>
);
export const RegistrationTokenList = props => {
return (
<List
{...props}
filters={<RegistrationTokenFilter />}
filterDefaultValues={{ valid: true }}
pagination={false}
perPage={500}
>
<Datagrid rowClick="edit">
<TextField source="token" sortable={false} />
<NumberField source="uses_allowed" sortable={false} />
<NumberField source="pending" sortable={false} />
<NumberField source="completed" sortable={false} />
<DateField
source="expiry_time"
showTime
options={date_format}
sortable={false}
/>
</Datagrid>
</List>
);
};
export const RegistrationTokenCreate = props => (
<Create {...props}>
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}>
<TextInput
source="token"
autoComplete="off"
validate={validateToken}
resettable
/>
<NumberInput
source="length"
validate={validateLength}
helperText="resources.registration_tokens.helper.length"
step={1}
/>
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<DateTimeInput source="expiry_time" parse={dateParser} />
</SimpleForm>
</Create>
);
export const RegistrationTokenEdit = props => {
return (
<Edit {...props}>
<SimpleForm>
<TextInput source="token" disabled />
<NumberInput source="pending" disabled />
<NumberInput source="completed" disabled />
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<DateTimeInput
source="expiry_time"
parse={dateParser}
format={dateFormatter}
/>
</SimpleForm>
</Edit>
);
};
+256
View File
@@ -0,0 +1,256 @@
import React, { Fragment } from "react";
import Avatar from "@material-ui/core/Avatar";
import { Chip } from "@material-ui/core";
import { connect } from "react-redux";
import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { makeStyles } from "@material-ui/core/styles";
import {
BooleanField,
BulkDeleteButton,
Button,
Datagrid,
DeleteButton,
Filter,
List,
NumberField,
Pagination,
TextField,
useCreate,
useMutation,
useNotify,
useTranslate,
useRefresh,
useUnselectAll,
} from "react-admin";
const useStyles = makeStyles({
small: {
height: "40px",
width: "40px",
},
});
const RoomDirectoryPagination = props => (
<Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} />
);
export const RoomDirectoryDeleteButton = props => {
const translate = useTranslate();
return (
<DeleteButton
{...props}
label="resources.room_directory.action.erase"
redirect={false}
mutationMode="pessimistic"
confirmTitle={translate("resources.room_directory.action.title", {
smart_count: 1,
})}
confirmContent={translate("resources.room_directory.action.content", {
smart_count: 1,
})}
resource="room_directory"
icon={<FolderSharedIcon />}
/>
);
};
export const RoomDirectoryBulkDeleteButton = props => (
<BulkDeleteButton
{...props}
label="resources.room_directory.action.erase"
mutationMode="pessimistic"
confirmTitle="resources.room_directory.action.title"
confirmContent="resources.room_directory.action.content"
resource="room_directory"
icon={<FolderSharedIcon />}
/>
);
export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
const notify = useNotify();
const refresh = useRefresh();
const unselectAll = useUnselectAll();
const [createMany, { loading }] = useMutation();
const handleSend = values => {
createMany(
{
type: "createMany",
resource: "room_directory",
payload: { ids: selectedIds, data: {} },
},
{
onSuccess: ({ data }) => {
notify("resources.room_directory.action.send_success");
unselectAll("rooms");
refresh();
},
onFailure: error =>
notify("resources.room_directory.action.send_failure", "error"),
}
);
};
return (
<Button
label="resources.room_directory.action.create"
onClick={handleSend}
disabled={loading}
>
<FolderSharedIcon />
</Button>
);
};
export const RoomDirectorySaveButton = ({ record }) => {
const notify = useNotify();
const refresh = useRefresh();
const [create, { loading }] = useCreate("room_directory");
const handleSend = values => {
create(
{
payload: { data: { id: record.id } },
},
{
onSuccess: ({ data }) => {
notify("resources.room_directory.action.send_success");
refresh();
},
onFailure: error =>
notify("resources.room_directory.action.send_failure", "error"),
}
);
};
return (
<Button
label="resources.room_directory.action.create"
onClick={handleSend}
disabled={loading}
>
<FolderSharedIcon />
</Button>
);
};
const RoomDirectoryBulkActionButtons = props => (
<Fragment>
<RoomDirectoryBulkDeleteButton {...props} />
</Fragment>
);
const AvatarField = ({ source, className, record = {} }) => (
<Avatar src={record[source]} className={className} />
);
const RoomDirectoryFilter = ({ ...props }) => {
const translate = useTranslate();
return (
<Filter {...props}>
<Chip
label={translate("resources.rooms.fields.room_id")}
source="room_id"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.topic")}
source="topic"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.canonical_alias")}
source="canonical_alias"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
</Filter>
);
};
export const FilterableRoomDirectoryList = ({
roomDirectoryFilters,
dispatch,
...props
}) => {
const classes = useStyles();
const translate = useTranslate();
const filter = roomDirectoryFilters;
const roomIdFilter = filter && filter.room_id ? true : false;
const topicFilter = filter && filter.topic ? true : false;
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
return (
<List
{...props}
pagination={<RoomDirectoryPagination />}
bulkActionButtons={<RoomDirectoryBulkActionButtons />}
filters={<RoomDirectoryFilter />}
perPage={100}
>
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}>
<AvatarField
source="avatar_src"
sortable={false}
className={classes.small}
label={translate("resources.rooms.fields.avatar")}
/>
<TextField
source="name"
sortable={false}
label={translate("resources.rooms.fields.name")}
/>
{roomIdFilter && (
<TextField
source="room_id"
sortable={false}
label={translate("resources.rooms.fields.room_id")}
/>
)}
{canonicalAliasFilter && (
<TextField
source="canonical_alias"
sortable={false}
label={translate("resources.rooms.fields.canonical_alias")}
/>
)}
{topicFilter && (
<TextField
source="topic"
sortable={false}
label={translate("resources.rooms.fields.topic")}
/>
)}
<NumberField
source="num_joined_members"
sortable={false}
label={translate("resources.rooms.fields.joined_members")}
/>
<BooleanField
source="world_readable"
sortable={false}
label={translate("resources.room_directory.fields.world_readable")}
/>
<BooleanField
source="guest_can_join"
sortable={false}
label={translate("resources.room_directory.fields.guest_can_join")}
/>
</Datagrid>
</List>
);
};
function mapStateToProps(state) {
return {
roomDirectoryFilters:
state.admin.resources.room_directory.list.params.displayedFilters,
};
}
export const RoomDirectoryList = connect(mapStateToProps)(
FilterableRoomDirectoryList
);
+4 -1
View File
@@ -24,7 +24,10 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
const ServerNoticeToolbar = props => ( const ServerNoticeToolbar = props => (
<Toolbar {...props}> <Toolbar {...props}>
<SaveButton label="resources.servernotices.action.send" /> <SaveButton
label="resources.servernotices.action.send"
disabled={props.pristine}
/>
<Button label="ra.action.cancel" onClick={onClose}> <Button label="ra.action.cancel" onClick={onClose}>
<IconCancel /> <IconCancel />
</Button> </Button>
+2 -2
View File
@@ -8,7 +8,7 @@ import {
} from "react-admin"; } from "react-admin";
import ActionDelete from "@material-ui/icons/Delete"; import ActionDelete from "@material-ui/icons/Delete";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator"; import { alpha } from "@material-ui/core/styles/colorManipulator";
import classnames from "classnames"; import classnames from "classnames";
const useStyles = makeStyles( const useStyles = makeStyles(
@@ -16,7 +16,7 @@ const useStyles = makeStyles(
deleteButton: { deleteButton: {
color: theme.palette.error.main, color: theme.palette.error.main,
"&:hover": { "&:hover": {
backgroundColor: fade(theme.palette.error.main, 0.12), backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices // Reset on mouse devices
"@media (hover: none)": { "@media (hover: none)": {
backgroundColor: "transparent", backgroundColor: "transparent",
+186 -4
View File
@@ -1,7 +1,8 @@
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import classnames from "classnames"; import classnames from "classnames";
import { fade } from "@material-ui/core/styles/colorManipulator"; import { alpha } from "@material-ui/core/styles/colorManipulator";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { Tooltip } from "@material-ui/core";
import { import {
BooleanInput, BooleanInput,
Button, Button,
@@ -10,23 +11,29 @@ import {
SaveButton, SaveButton,
SimpleForm, SimpleForm,
Toolbar, Toolbar,
useCreate,
useDelete, useDelete,
useNotify, useNotify,
useRefresh,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import IconCancel from "@material-ui/icons/Cancel"; import BlockIcon from "@material-ui/icons/Block";
import ClearIcon from "@material-ui/icons/Clear";
import DeleteSweepIcon from "@material-ui/icons/DeleteSweep";
import Dialog from "@material-ui/core/Dialog"; import Dialog from "@material-ui/core/Dialog";
import DialogContent from "@material-ui/core/DialogContent"; import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle"; import DialogTitle from "@material-ui/core/DialogTitle";
import DeleteSweepIcon from "@material-ui/icons/DeleteSweep"; import IconCancel from "@material-ui/icons/Cancel";
import LockIcon from "@material-ui/icons/Lock";
import LockOpenIcon from "@material-ui/icons/LockOpen";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
deleteButton: { deleteButton: {
color: theme.palette.error.main, color: theme.palette.error.main,
"&:hover": { "&:hover": {
backgroundColor: fade(theme.palette.error.main, 0.12), backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices // Reset on mouse devices
"@media (hover: none)": { "@media (hover: none)": {
backgroundColor: "transparent", backgroundColor: "transparent",
@@ -143,3 +150,178 @@ export const DeleteMediaButton = props => {
</Fragment> </Fragment>
); );
}; };
export const ProtectMediaButton = props => {
const { record } = props;
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { loading }] = useCreate("protect_media");
const [deleteOne] = useDelete("protect_media");
if (!record) return null;
const handleProtect = () => {
create(
{ payload: { data: record } },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.protect_media.action.send_failure", "error"),
}
);
};
const handleUnprotect = () => {
deleteOne(
{ payload: { ...record } },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.protect_media.action.send_failure", "error"),
}
);
};
return (
/*
Wrapping Tooltip with <div>
https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
*/
<Fragment>
{record.quarantined_by && (
<Tooltip
title={translate("resources.protect_media.action.none", {
_: "resources.protect_media.action.none",
})}
>
<div>
{/*
Button instead BooleanField for
consistent appearance and position in the column
*/}
<Button disabled={true}>
<ClearIcon />
</Button>
</div>
</Tooltip>
)}
{record.safe_from_quarantine && (
<Tooltip
title={translate("resources.protect_media.action.delete", {
_: "resources.protect_media.action.delete",
})}
arrow
>
<div>
<Button onClick={handleUnprotect} disabled={loading}>
<LockIcon />
</Button>
</div>
</Tooltip>
)}
{!record.safe_from_quarantine && !record.quarantined_by && (
<Tooltip
title={translate("resources.protect_media.action.create", {
_: "resources.protect_media.action.create",
})}
>
<div>
<Button onClick={handleProtect} disabled={loading}>
<LockOpenIcon />
</Button>
</div>
</Tooltip>
)}
</Fragment>
);
};
export const QuarantineMediaButton = props => {
const { record } = props;
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { loading }] = useCreate("quarantine_media");
const [deleteOne] = useDelete("quarantine_media");
if (!record) return null;
const handleQuarantaine = () => {
create(
{ payload: { data: record } },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.quarantine_media.action.send_failure", "error"),
}
);
};
const handleRemoveQuarantaine = () => {
deleteOne(
{ payload: { ...record } },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.quarantine_media.action.send_failure", "error"),
}
);
};
return (
<Fragment>
{record.safe_from_quarantine && (
<Tooltip
title={translate("resources.quarantine_media.action.none", {
_: "resources.quarantine_media.action.none",
})}
>
<div>
<Button disabled={true}>
<ClearIcon />
</Button>
</div>
</Tooltip>
)}
{record.quarantined_by && (
<Tooltip
title={translate("resources.quarantine_media.action.delete", {
_: "resources.quarantine_media.action.delete",
})}
>
<div>
<Button onClick={handleRemoveQuarantaine} disabled={loading}>
<BlockIcon color="error" />
</Button>
</div>
</Tooltip>
)}
{!record.safe_from_quarantine && !record.quarantined_by && (
<Tooltip
title={translate("resources.quarantine_media.action.create", {
_: "resources.quarantine_media.action.create",
})}
>
<div>
<Button onClick={handleQuarantaine} disabled={loading}>
<BlockIcon />
</Button>
</div>
</Tooltip>
)}
</Fragment>
);
};
+132 -17
View File
@@ -2,11 +2,13 @@ import React, { Fragment } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
BooleanField, BooleanField,
BulkDeleteWithConfirmButton, BulkDeleteButton,
DateField,
Datagrid, Datagrid,
DeleteButton, DeleteButton,
Filter, Filter,
List, List,
NumberField,
Pagination, Pagination,
ReferenceField, ReferenceField,
ReferenceManyField, ReferenceManyField,
@@ -17,16 +19,43 @@ import {
TabbedShowLayout, TabbedShowLayout,
TextField, TextField,
TopToolbar, TopToolbar,
useRecordContext,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import get from "lodash/get"; import get from "lodash/get";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import { Tooltip, Typography, Chip } from "@material-ui/core"; import { Tooltip, Typography, Chip } from "@material-ui/core";
import FastForwardIcon from "@material-ui/icons/FastForward";
import HttpsIcon from "@material-ui/icons/Https"; import HttpsIcon from "@material-ui/icons/Https";
import NoEncryptionIcon from "@material-ui/icons/NoEncryption"; import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
import PageviewIcon from "@material-ui/icons/Pageview"; import PageviewIcon from "@material-ui/icons/Pageview";
import UserIcon from "@material-ui/icons/Group"; import UserIcon from "@material-ui/icons/Group";
import ViewListIcon from "@material-ui/icons/ViewList"; import ViewListIcon from "@material-ui/icons/ViewList";
import VisibilityIcon from "@material-ui/icons/Visibility"; import VisibilityIcon from "@material-ui/icons/Visibility";
import EventIcon from "@material-ui/icons/Event";
import {
RoomDirectoryBulkDeleteButton,
RoomDirectoryBulkSaveButton,
RoomDirectoryDeleteButton,
RoomDirectorySaveButton,
} from "./RoomDirectory";
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const useStyles = makeStyles(theme => ({
helper_forward_extremities: {
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
margin: "0.5em",
},
}));
const RoomPagination = props => ( const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
@@ -73,22 +102,33 @@ const RoomTitle = ({ record }) => {
}; };
const RoomShowActions = ({ basePath, data, resource }) => { const RoomShowActions = ({ basePath, data, resource }) => {
const translate = useTranslate(); var roomDirectoryStatus = "";
if (data) {
roomDirectoryStatus = data.public;
}
return ( return (
<TopToolbar> <TopToolbar>
{roomDirectoryStatus === false && (
<RoomDirectorySaveButton record={data} />
)}
{roomDirectoryStatus === true && (
<RoomDirectoryDeleteButton record={data} />
)}
<DeleteButton <DeleteButton
basePath={basePath} basePath={basePath}
record={data} record={data}
resource={resource} resource={resource}
undoable={false} mutationMode="pessimistic"
confirmTitle={translate("synapseadmin.rooms.delete.title")} confirmTitle="resources.rooms.action.erase.title"
confirmContent={translate("synapseadmin.rooms.delete.message")} confirmContent="resources.rooms.action.erase.content"
/> />
</TopToolbar> </TopToolbar>
); );
}; };
export const RoomShow = props => { export const RoomShow = props => {
const classes = useStyles({ props });
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}> <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
@@ -97,7 +137,9 @@ export const RoomShow = props => {
<TextField source="room_id" /> <TextField source="room_id" />
<TextField source="name" /> <TextField source="name" />
<TextField source="canonical_alias" /> <TextField source="canonical_alias" />
<TextField source="creator" /> <ReferenceField source="creator" reference="users">
<TextField source="id" />
</ReferenceField>
</Tab> </Tab>
<Tab <Tab
@@ -107,6 +149,7 @@ export const RoomShow = props => {
> >
<TextField source="joined_members" /> <TextField source="joined_members" />
<TextField source="joined_local_members" /> <TextField source="joined_local_members" />
<TextField source="joined_local_devices" />
<TextField source="state_events" /> <TextField source="state_events" />
<TextField source="version" /> <TextField source="version" />
<TextField <TextField
@@ -197,6 +240,63 @@ export const RoomShow = props => {
]} ]}
/> />
</Tab> </Tab>
<Tab
label={translate("resources.room_state.name", { smart_count: 2 })}
icon={<EventIcon />}
path="state"
>
<ReferenceManyField
reference="room_state"
target="room_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }}>
<TextField source="type" sortable={false} />
<DateField
source="origin_server_ts"
showTime
options={date_format}
sortable={false}
/>
<TextField source="content" sortable={false} />
<ReferenceField
source="sender"
reference="users"
sortable={false}
>
<TextField source="id" />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</Tab>
<Tab
label="resources.forward_extremities.name"
icon={<FastForwardIcon />}
path="forward_extremities"
>
<div className={classes.helper_forward_extremities}>
{translate("resources.rooms.helper.forward_extremities")}
</div>
<ReferenceManyField
reference="forward_extremities"
target="room_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }}>
<TextField source="id" sortable={false} />
<DateField
source="received_ts"
showTime
options={date_format}
sortable={false}
/>
<NumberField source="depth" sortable={false} />
<TextField source="state_group" sortable={false} />
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout> </TabbedShowLayout>
</Show> </Show>
); );
@@ -204,7 +304,14 @@ export const RoomShow = props => {
const RoomBulkActionButtons = props => ( const RoomBulkActionButtons = props => (
<Fragment> <Fragment>
<BulkDeleteWithConfirmButton {...props} /> <RoomDirectoryBulkSaveButton {...props} />
<RoomDirectoryBulkDeleteButton {...props} />
<BulkDeleteButton
{...props}
confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic"
/>
</Fragment> </Fragment>
); );
@@ -241,14 +348,27 @@ const RoomFilter = ({ ...props }) => {
); );
}; };
const FilterableRoomList = ({ ...props }) => { const RoomNameField = props => {
const filter = props.roomFilters; const { source } = props;
const record = useRecordContext(props);
return (
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
);
};
RoomNameField.propTypes = {
label: PropTypes.string,
record: PropTypes.object,
source: PropTypes.string.isRequired,
};
const FilterableRoomList = ({ roomFilters, dispatch, ...props }) => {
const filter = roomFilters;
const localMembersFilter = const localMembersFilter =
filter && filter.joined_local_members ? true : false; filter && filter.joined_local_members ? true : false;
const stateEventsFilter = filter && filter.state_events ? true : false; const stateEventsFilter = filter && filter.state_events ? true : false;
const versionFilter = filter && filter.version ? true : false; const versionFilter = filter && filter.version ? true : false;
const federateableFilter = filter && filter.federatable ? true : false; const federateableFilter = filter && filter.federatable ? true : false;
const translate = useTranslate();
return ( return (
<List <List
@@ -256,12 +376,7 @@ const FilterableRoomList = ({ ...props }) => {
pagination={<RoomPagination />} pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }} sort={{ field: "name", order: "ASC" }}
filters={<RoomFilter />} filters={<RoomFilter />}
bulkActionButtons={ bulkActionButtons={<RoomBulkActionButtons />}
<RoomBulkActionButtons
confirmTitle={translate("synapseadmin.rooms.delete.title")}
confirmContent={translate("synapseadmin.rooms.delete.message")}
/>
}
> >
<Datagrid rowClick="show"> <Datagrid rowClick="show">
<EncryptionField <EncryptionField
@@ -269,7 +384,7 @@ const FilterableRoomList = ({ ...props }) => {
sortBy="encryption" sortBy="encryption"
label={<HttpsIcon />} label={<HttpsIcon />}
/> />
<TextField source="name" /> <RoomNameField source="name" />
<TextField source="joined_members" /> <TextField source="joined_members" />
{localMembersFilter && <TextField source="joined_local_members" />} {localMembersFilter && <TextField source="joined_local_members" />}
{stateEventsFilter && <TextField source="state_events" />} {stateEventsFilter && <TextField source="state_events" />}
+106 -96
View File
@@ -1,12 +1,13 @@
import React, { cloneElement, Fragment } from "react"; import React, { cloneElement, Fragment } from "react";
import Avatar from "@material-ui/core/Avatar"; import Avatar from "@material-ui/core/Avatar";
import PersonPinIcon from "@material-ui/icons/PersonPin"; import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
import ContactMailIcon from "@material-ui/icons/ContactMail"; import ContactMailIcon from "@material-ui/icons/ContactMail";
import DevicesIcon from "@material-ui/icons/Devices"; import DevicesIcon from "@material-ui/icons/Devices";
import GetAppIcon from "@material-ui/icons/GetApp"; import GetAppIcon from "@material-ui/icons/GetApp";
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import NotificationsIcon from "@material-ui/icons/Notifications"; import NotificationsIcon from "@material-ui/icons/Notifications";
import PermMediaIcon from "@material-ui/icons/PermMedia"; import PermMediaIcon from "@material-ui/icons/PermMedia";
import PersonPinIcon from "@material-ui/icons/PersonPin";
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import ViewListIcon from "@material-ui/icons/ViewList"; import ViewListIcon from "@material-ui/icons/ViewList";
import { import {
ArrayInput, ArrayInput,
@@ -35,8 +36,9 @@ import {
BulkDeleteButton, BulkDeleteButton,
DeleteButton, DeleteButton,
SaveButton, SaveButton,
maxLength,
regex, regex,
useRedirect, required,
useTranslate, useTranslate,
Pagination, Pagination,
CreateButton, CreateButton,
@@ -45,11 +47,13 @@ import {
sanitizeListRestProps, sanitizeListRestProps,
NumberField, NumberField,
} from "react-admin"; } from "react-admin";
import { Link } from "react-router-dom";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices"; import { DeviceRemoveButton } from "./devices";
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
const redirect = (basePath, id, data) => { const redirect = () => {
return { return {
pathname: "/import_users", pathname: "/import_users",
}; };
@@ -67,6 +71,15 @@ const useStyles = makeStyles({
}, },
}); });
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const UserListActions = ({ const UserListActions = ({
currentSort, currentSort,
className, className,
@@ -85,7 +98,6 @@ const UserListActions = ({
total, total,
...rest ...rest
}) => { }) => {
const redirectTo = useRedirect();
return ( return (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}> <TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters && {filters &&
@@ -106,12 +118,7 @@ const UserListActions = ({
maxResults={maxResults} maxResults={maxResults}
/> />
{/* Add your custom actions */} {/* Add your custom actions */}
<Button <Button component={Link} to={redirect} label="CSV Import">
onClick={() => {
redirectTo(redirect);
}}
label="CSV Import"
>
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} /> <GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
</Button> </Button>
</TopToolbar> </TopToolbar>
@@ -139,19 +146,17 @@ const UserFilter = props => (
</Filter> </Filter>
); );
const UserBulkActionButtons = props => { const UserBulkActionButtons = props => (
const translate = useTranslate(); <Fragment>
return ( <ServerNoticeBulkButton {...props} />
<Fragment> <BulkDeleteButton
<ServerNoticeBulkButton {...props} /> {...props}
<BulkDeleteButton label="resources.users.action.erase"
{...props} confirmTitle="resources.users.helper.erase"
label="resources.users.action.erase" mutationMode="pessimistic"
title={translate("resources.users.helper.erase")} />
/> </Fragment>
</Fragment> );
);
};
const AvatarField = ({ source, className, record = {} }) => ( const AvatarField = ({ source, className, record = {} }) => (
<Avatar src={record[source]} className={className} /> <Avatar src={record[source]} className={className} />
@@ -164,6 +169,7 @@ export const UserList = props => {
{...props} {...props}
filters={<UserFilter />} filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }} filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />} actions={<UserListActions maxResults={10000} />}
bulkActionButtons={<UserBulkActionButtons />} bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />} pagination={<UserPagination />}
@@ -171,24 +177,36 @@ export const UserList = props => {
<Datagrid rowClick="edit"> <Datagrid rowClick="edit">
<AvatarField <AvatarField
source="avatar_src" source="avatar_src"
sortable={false}
className={classes.small} className={classes.small}
sortBy="avatar_url"
/>
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<BooleanField source="deactivated" />
<DateField
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
options={date_format}
/> />
<TextField source="id" sortable={false} />
<TextField source="displayname" sortable={false} />
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} />
<BooleanField source="deactivated" sortable={false} />
</Datagrid> </Datagrid>
</List> </List>
); );
}; };
// https://matrix.org/docs/spec/appendices#user-identifiers // https://matrix.org/docs/spec/appendices#user-identifiers
const validateUser = regex( // here only local part of user_id
/^@[a-z0-9._=\-/]+:.*/, // maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
"synapseadmin.users.invalid_user_id" // localStorage.getItem("home_server").length is not valid here
); const validateUser = [
required(),
maxLength(253),
regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id"),
];
const validateAddress = [required(), maxLength(255)];
export function generateRandomUser() { export function generateRandomUser() {
const homeserver = localStorage.getItem("home_server"); const homeserver = localStorage.getItem("home_server");
@@ -235,10 +253,13 @@ const UserEditToolbar = props => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Toolbar {...props}> <Toolbar {...props}>
<SaveButton submitOnEnter={true} /> <SaveButton submitOnEnter={true} disabled={props.pristine} />
<DeleteButton <DeleteButton
label="resources.users.action.erase" label="resources.users.action.erase"
title={translate("resources.users.helper.erase")} confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1,
})}
mutationMode="pessimistic"
/> />
<ServerNoticeButton /> <ServerNoticeButton />
</Toolbar> </Toolbar>
@@ -249,8 +270,12 @@ export const UserCreate = props => (
<Create {...props}> <Create {...props}>
<SimpleForm> <SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} /> <TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" /> <TextInput source="displayname" validate={maxLength(256)} />
<PasswordInput source="password" autoComplete="new-password" /> <PasswordInput
source="password"
autoComplete="new-password"
validate={maxLength(512)}
/>
<BooleanInput source="admin" /> <BooleanInput source="admin" />
<ArrayInput source="threepids"> <ArrayInput source="threepids">
<SimpleFormIterator> <SimpleFormIterator>
@@ -260,8 +285,19 @@ export const UserCreate = props => (
{ id: "email", name: "resources.users.email" }, { id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" }, { id: "msisdn", name: "resources.users.msisdn" },
]} ]}
validate={required()}
/>
<TextInput source="address" validate={validateAddress} />
</SimpleFormIterator>
</ArrayInput>
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
<SimpleFormIterator>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
validate={required()}
/> />
<TextInput source="address" />
</SimpleFormIterator> </SimpleFormIterator>
</ArrayInput> </ArrayInput>
</SimpleForm> </SimpleForm>
@@ -302,18 +338,7 @@ export const UserEdit = props => {
source="deactivated" source="deactivated"
helperText="resources.users.helper.deactivate" helperText="resources.users.helper.deactivate"
/> />
<DateField <DateField source="creation_ts_ms" showTime options={date_format} />
source="creation_ts_ms"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
/>
<TextField source="consent_version" /> <TextField source="consent_version" />
</FormTab> </FormTab>
@@ -336,6 +361,23 @@ export const UserEdit = props => {
</ArrayInput> </ArrayInput>
</FormTab> </FormTab>
<FormTab
label="synapseadmin.users.tabs.sso"
icon={<AssignmentIndIcon />}
path="sso"
>
<ArrayInput source="external_ids" label={false}>
<SimpleFormIterator>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
validate={required()}
/>
</SimpleFormIterator>
</ArrayInput>
</FormTab>
<FormTab <FormTab
label={translate("resources.devices.name", { smart_count: 2 })} label={translate("resources.devices.name", { smart_count: 2 })}
icon={<DevicesIcon />} icon={<DevicesIcon />}
@@ -353,14 +395,7 @@ export const UserEdit = props => {
<DateField <DateField
source="last_seen_ts" source="last_seen_ts"
showTime showTime
options={{ options={date_format}
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false} sortable={false}
/> />
<DeviceRemoveButton /> <DeviceRemoveButton />
@@ -388,14 +423,7 @@ export const UserEdit = props => {
<DateField <DateField
source="last_seen" source="last_seen"
showTime showTime
options={{ options={date_format}
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false} sortable={false}
/> />
<TextField <TextField
@@ -419,41 +447,23 @@ export const UserEdit = props => {
addLabel={false} addLabel={false}
pagination={<UserPagination />} pagination={<UserPagination />}
perPage={50} perPage={50}
sort={{ field: "created_ts", order: "DESC" }}
> >
<Datagrid style={{ width: "100%" }}> <Datagrid style={{ width: "100%" }}>
<DateField <DateField source="created_ts" showTime options={date_format} />
source="created_ts"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<DateField <DateField
source="last_access_ts" source="last_access_ts"
showTime showTime
options={{ options={date_format}
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/> />
<TextField source="media_id" sortable={false} /> <TextField source="media_id" />
<NumberField source="media_length" sortable={false} /> <NumberField source="media_length" />
<TextField source="media_type" sortable={false} /> <TextField source="media_type" />
<TextField source="upload_name" sortable={false} /> <TextField source="upload_name" />
<TextField source="quarantined_by" sortable={false} /> <TextField source="quarantined_by" />
<BooleanField source="safe_from_quarantine" sortable={false} /> <QuarantineMediaButton label="resources.quarantine_media.action.name" />
<DeleteButton undoable={false} redirect={false} /> <ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
<DeleteButton mutationMode="pessimistic" redirect={false} />
</Datagrid> </Datagrid>
</ReferenceManyField> </ReferenceManyField>
</FormTab> </FormTab>
+97 -17
View File
@@ -1,6 +1,6 @@
import germanMessages from "ra-language-german"; import germanMessages from "ra-language-german";
export default { const de = {
...germanMessages, ...germanMessages,
synapseadmin: { synapseadmin: {
auth: { auth: {
@@ -10,10 +10,11 @@ export default {
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'", username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen", protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
url_error: "Keine gültige Matrix Server URL", url_error: "Keine gültige Matrix Server URL",
sso_sign_in: "Anmeldung mit SSO",
}, },
users: { users: {
invalid_user_id: invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver", tabs: { sso: "SSO" },
}, },
rooms: { rooms: {
details: "Raumdetails", details: "Raumdetails",
@@ -23,11 +24,6 @@ export default {
detail: "Details", detail: "Details",
permission: "Berechtigungen", permission: "Berechtigungen",
}, },
delete: {
title: "Raum löschen",
message:
"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!",
},
}, },
reports: { tabs: { basic: "Allgemein", detail: "Details" } }, reports: { tabs: { basic: "Allgemein", detail: "Details" } },
}, },
@@ -101,7 +97,6 @@ export default {
}, },
resources: { resources: {
users: { users: {
backtolist: "Zurück zur Liste",
name: "Benutzer", name: "Benutzer",
email: "E-Mail", email: "E-Mail",
msisdn: "Telefon", msisdn: "Telefon",
@@ -125,6 +120,7 @@ export default {
address: "Adresse", address: "Adresse",
creation_ts_ms: "Zeitpunkt der Erstellung", creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen", consent_version: "Zugestimmte Geschäftsbedingungen",
auth_provider: "Provider",
}, },
helper: { helper: {
deactivate: deactivate:
@@ -143,16 +139,23 @@ export default {
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Mitglieder", joined_members: "Mitglieder",
joined_local_members: "Lokale Mitglieder", joined_local_members: "Lokale Mitglieder",
state_events: "Ereignisse", joined_local_devices: "Lokale Endgeräte",
state_events: "Zustandsereignisse / Komplexität",
version: "Version", version: "Version",
is_encrypted: "Verschlüsselt", is_encrypted: "Verschlüsselt",
encryption: "Verschlüsselungs-Algorithmus", encryption: "Verschlüsselungs-Algorithmus",
federatable: "Fö­de­rierbar", federatable: "Fö­de­rierbar",
public: "Öffentlich", public: "Sichtbar im Raumverzeichnis",
creator: "Ersteller", creator: "Ersteller",
join_rules: "Beitrittsregeln", join_rules: "Beitrittsregeln",
guest_access: "Gastzugriff", guest_access: "Gastzugriff",
history_visibility: "Historie-Sichtbarkeit", history_visibility: "Historie-Sichtbarkeit",
topic: "Thema",
avatar: "Avatar",
},
helper: {
forward_extremities:
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.",
}, },
enums: { enums: {
join_rules: { join_rules: {
@@ -173,6 +176,13 @@ export default {
}, },
unencrypted: "Nicht verschlüsselt", unencrypted: "Nicht verschlüsselt",
}, },
action: {
erase: {
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!",
},
},
}, },
reports: { reports: {
name: "Ereignisbericht |||| Ereignisberichte", name: "Ereignisbericht |||| Ereignisberichte",
@@ -231,7 +241,7 @@ export default {
media_type: "Typ", media_type: "Typ",
upload_name: "Dateiname", upload_name: "Dateiname",
quarantined_by: "Zur Quarantäne hinzugefügt", quarantined_by: "Zur Quarantäne hinzugefügt",
safe_from_quarantine: "Geschützt vor Quarantäne", safe_from_quarantine: "Schutz vor Quarantäne",
created_ts: "Erstellt", created_ts: "Erstellt",
last_access_ts: "Letzter Zugriff", last_access_ts: "Letzter Zugriff",
}, },
@@ -249,8 +259,26 @@ export default {
send_failure: "Beim Versenden ist ein Fehler aufgetreten.", send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
}, },
helper: { helper: {
send: send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
"Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.", },
},
protect_media: {
action: {
create: "Ungeschützt, Schutz erstellen",
delete: "Geschützt, Schutz aufheben",
none: "In Quarantäne",
send_success: "Erfolgreich den Schutz-Status geändert.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
},
quarantine_media: {
action: {
name: "Quarantäne",
create: "Zur Quarantäne hinzufügen",
delete: "In Quarantäne, Quarantäne aufheben",
none: "Geschützt vor Quarantäne",
send_success: "Erfolgreich den Quarantäne-Status geändert.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
}, },
}, },
pushers: { pushers: {
@@ -279,8 +307,7 @@ export default {
send_failure: "Beim Versenden ist ein Fehler aufgetreten.", send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
}, },
helper: { helper: {
send: send: 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
}, },
}, },
user_media_statistics: { user_media_statistics: {
@@ -290,6 +317,54 @@ export default {
media_length: "Größe der Dateien", media_length: "Größe der Dateien",
}, },
}, },
forward_extremities: {
name: "Vorderextremitäten",
fields: {
id: "Event-ID",
received_ts: "Zeitstempel",
depth: "Tiefe",
state_group: "Zustandsgruppe",
},
},
room_state: {
name: "Zustandsereignisse",
fields: {
type: "Typ",
content: "Inhalt",
origin_server_ts: "Sendezeit",
sender: "Absender",
},
},
room_directory: {
name: "Raumverzeichnis",
fields: {
world_readable: "Gastbenutzer dürfen ohne Beitritt lesen",
guest_can_join: "Gastbenutzer dürfen beitreten",
},
action: {
title:
"Raum aus Verzeichnis löschen |||| %{smart_count} Räume aus Verzeichnis löschen",
content:
"Möchten Sie den Raum wirklich aus dem Raumverzeichnis löschen? |||| Möchten Sie die %{smart_count} Räume wirklich aus dem Raumverzeichnis löschen?",
erase: "Lösche aus Verzeichnis",
create: "Eintragen ins Verzeichnis",
send_success: "Raum erfolgreich eingetragen.",
send_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
},
registration_tokens: {
name: "Registrierungstoken",
fields: {
token: "Token",
valid: "Gültige Token",
uses_allowed: "Anzahl",
pending: "Ausstehend",
completed: "Abgeschlossen",
expiry_time: "Ablaufzeit",
length: "Länge",
},
helper: { length: "Länge des Tokens, wenn kein Token vorgegeben wird." },
},
}, },
ra: { ra: {
...germanMessages.ra, ...germanMessages.ra,
@@ -310,7 +385,7 @@ export default {
}, },
}, },
notification: { notification: {
...germanMessages.ra.notifiaction, ...germanMessages.ra.notification,
logged_out: "Abgemeldet", logged_out: "Abgemeldet",
}, },
page: { page: {
@@ -318,5 +393,10 @@ export default {
empty: "Keine Einträge vorhanden", empty: "Keine Einträge vorhanden",
invite: "", invite: "",
}, },
navigation: {
...germanMessages.ra.navigation,
skip_nav: "Zum Inhalt springen",
},
}, },
}; };
export default de;
+92 -16
View File
@@ -1,6 +1,6 @@
import englishMessages from "ra-language-english"; import englishMessages from "ra-language-english";
export default { const en = {
...englishMessages, ...englishMessages,
synapseadmin: { synapseadmin: {
auth: { auth: {
@@ -10,10 +10,11 @@ export default {
username_error: "Please enter fully qualified user ID: '@user:domain'", username_error: "Please enter fully qualified user ID: '@user:domain'",
protocol_error: "URL has to start with 'http://' or 'https://'", protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL", url_error: "Not a valid Matrix server URL",
sso_sign_in: "Sign in with SSO",
}, },
users: { users: {
invalid_user_id: invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver", tabs: { sso: "SSO" },
}, },
rooms: { rooms: {
tabs: { tabs: {
@@ -22,11 +23,6 @@ export default {
detail: "Details", detail: "Details",
permission: "Permissions", permission: "Permissions",
}, },
delete: {
title: "Delete room",
message:
"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!",
},
}, },
reports: { tabs: { basic: "Basic", detail: "Details" } }, reports: { tabs: { basic: "Basic", detail: "Details" } },
}, },
@@ -100,7 +96,6 @@ export default {
}, },
resources: { resources: {
users: { users: {
backtolist: "Back to list",
name: "User |||| Users", name: "User |||| Users",
email: "Email", email: "Email",
msisdn: "Phone", msisdn: "Phone",
@@ -124,6 +119,7 @@ export default {
address: "Address", address: "Address",
creation_ts_ms: "Creation timestamp", creation_ts_ms: "Creation timestamp",
consent_version: "Consent version", consent_version: "Consent version",
auth_provider: "Provider",
}, },
helper: { helper: {
deactivate: "You must provide a password to re-activate an account.", deactivate: "You must provide a password to re-activate an account.",
@@ -141,16 +137,23 @@ export default {
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Members", joined_members: "Members",
joined_local_members: "Local members", joined_local_members: "Local members",
state_events: "State events", joined_local_devices: "Local devices",
state_events: "State events / Complexity",
version: "Version", version: "Version",
is_encrypted: "Encrypted", is_encrypted: "Encrypted",
encryption: "Encryption", encryption: "Encryption",
federatable: "Federatable", federatable: "Federatable",
public: "Public", public: "Visible in room directory",
creator: "Creator", creator: "Creator",
join_rules: "Join rules", join_rules: "Join rules",
guest_access: "Guest access", guest_access: "Guest access",
history_visibility: "History visibility", history_visibility: "History visibility",
topic: "Topic",
avatar: "Avatar",
},
helper: {
forward_extremities:
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.",
}, },
enums: { enums: {
join_rules: { join_rules: {
@@ -171,6 +174,13 @@ export default {
}, },
unencrypted: "Unencrypted", unencrypted: "Unencrypted",
}, },
action: {
erase: {
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!",
},
},
}, },
reports: { reports: {
name: "Reported event |||| Reported events", name: "Reported event |||| Reported events",
@@ -225,7 +235,7 @@ export default {
name: "Media", name: "Media",
fields: { fields: {
media_id: "Media ID", media_id: "Media ID",
media_length: "Lenght", media_length: "File Size (in Bytes)",
media_type: "Type", media_type: "Type",
upload_name: "File name", upload_name: "File name",
quarantined_by: "Quarantined by", quarantined_by: "Quarantined by",
@@ -247,8 +257,26 @@ export default {
send_failure: "An error has occurred.", send_failure: "An error has occurred.",
}, },
helper: { helper: {
send: send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
"This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.", },
},
protect_media: {
action: {
create: "Unprotected, create protection",
delete: "Protected, remove protection",
none: "In quarantine",
send_success: "Successfully changed the protection status.",
send_failure: "An error has occurred.",
},
},
quarantine_media: {
action: {
name: "Quarantine",
create: "Add to quarantine",
delete: "In quarantine, unquarantine",
none: "Protected from quarantine",
send_success: "Successfully changed the quarantine status.",
send_failure: "An error has occurred.",
}, },
}, },
pushers: { pushers: {
@@ -277,8 +305,7 @@ export default {
send_failure: "An error has occurred.", send_failure: "An error has occurred.",
}, },
helper: { helper: {
send: send: 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
}, },
}, },
user_media_statistics: { user_media_statistics: {
@@ -288,5 +315,54 @@ export default {
media_length: "Media length", media_length: "Media length",
}, },
}, },
forward_extremities: {
name: "Forward Extremities",
fields: {
id: "Event ID",
received_ts: "Timestamp",
depth: "Depth",
state_group: "State group",
},
},
room_state: {
name: "State events",
fields: {
type: "Type",
content: "Content",
origin_server_ts: "time of send",
sender: "Sender",
},
},
room_directory: {
name: "Room directory",
fields: {
world_readable: "guest users may view without joining",
guest_can_join: "guest users may join",
},
action: {
title:
"Delete room from directory |||| Delete %{smart_count} rooms from directory",
content:
"Are you sure you want to remove this room from directory? |||| Are you sure you want to remove these %{smart_count} rooms from directory",
erase: "Delete from room directory",
create: "Publish in room directory",
send_success: "Room successfully published.",
send_failure: "An error has occurred.",
},
},
},
registration_tokens: {
name: "Registration tokens",
fields: {
token: "Token",
valid: "Valid token",
uses_allowed: "Uses allowed",
pending: "Pending",
completed: "Completed",
expiry_time: "Expiry time",
length: "Length",
},
helper: { length: "Length of the token if no token is given." },
}, },
}; };
export default en;
+290
View File
@@ -0,0 +1,290 @@
import chineseMessages from "ra-language-chinese";
const zh = {
...chineseMessages,
synapseadmin: {
auth: {
base_url: "服务器 URL",
welcome: "欢迎来到 Synapse-admin",
server_version: "Synapse 版本",
username_error: "请输入完整有效的用户 ID: '@user:domain'",
protocol_error: "URL 需要以'http://'或'https://'作为起始",
url_error: "不是一个有效的 Matrix 服务器地址",
sso_sign_in: "使用 SSO 登录",
},
users: {
invalid_user_id:
"必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "基本",
members: "成员",
detail: "细节",
permission: "权限",
},
delete: {
title: "删除房间",
message:
"您确定要删除这个房间吗?该操作无法被撤销。这个房间里所有的消息和分享的媒体都将被从服务器上删除!",
},
},
reports: { tabs: { basic: "基本", detail: "细节" } },
},
import_users: {
error: {
at_entry: "在条目 %{entry}: %{message}",
error: "错误",
required_field: "需要的值 '%{field}' 未被设置。",
invalid_value:
"第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
unreasonably_big: "拒绝加载过大的文件: %{size} MB",
already_in_progress: "一个导入进程已经在运行中",
id_exits: "ID %{id} 已经存在",
},
title: "通过 CSV 导入用户",
goToPdf: "转到 PDF",
cards: {
importstats: {
header: "导入用户",
users_total:
"%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
},
conflicts: {
header: "冲突处理策略",
mode: {
stop: "在冲突处停止",
skip: "显示错误并跳过冲突",
},
},
ids: {
header: "IDs",
all_ids_present: "每条记录的 ID",
count_ids_present:
"%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
mode: {
ignore: "忽略 CSV 中的 ID 并创建新的",
update: "更新已经存在的记录",
},
},
passwords: {
header: "密码",
all_passwords_present: "每条记录的密码",
count_passwords_present:
"%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
use_passwords: "使用 CSV 中标记的密码",
},
upload: {
header: "导入 CSV 文件",
explanation:
"在这里,你可以上传一个用逗号分隔的文件,用于创建或更新用户。该文件必须包括 'id' 和 'displayname' 字段。你可以在这里下载并修改一个示例文件:",
},
startImport: {
simulate_only: "模拟模式",
run_import: "导入",
},
results: {
header: "导入结果",
total: "共计 %{smart_count} 条记录 |||| 共计 %{smart_count} 条记录",
successful: "%{smart_count} 条记录导入成功",
skipped: "跳过 %{smart_count} 条记录",
download_skipped: "下载跳过的记录",
with_error:
"%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
simulated_only: "只是一次模拟运行",
},
},
},
resources: {
users: {
name: "用户",
email: "邮箱",
msisdn: "电话",
threepid: "邮箱 / 电话",
fields: {
avatar: "邮箱",
id: "用户 ID",
name: "用户名",
is_guest: "访客",
admin: "服务器管理员",
deactivated: "被禁用",
guests: "显示访客",
show_deactivated: "显示被禁用的账户",
user_id: "搜索用户",
displayname: "显示名字",
password: "密码",
avatar_url: "头像 URL",
avatar_src: "头像",
medium: "Medium",
threepids: "3PIDs",
address: "地址",
creation_ts_ms: "创建时间戳",
consent_version: "协议版本",
},
helper: {
deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了",
},
action: {
erase: "抹除用户信息",
},
},
rooms: {
name: "房间",
fields: {
room_id: "房间 ID",
name: "房间名",
canonical_alias: "别名",
joined_members: "成员",
joined_local_members: "本地成员",
state_events: "状态事件",
version: "版本",
is_encrypted: "已经加密",
encryption: "加密",
federatable: "可联合的",
public: "公开",
creator: "创建者",
join_rules: "加入规则",
guest_access: "访客访问",
history_visibility: "历史可见性",
},
enums: {
join_rules: {
public: "公开",
knock: "申请",
invite: "邀请",
private: "私有",
},
guest_access: {
can_join: "访客可以加入",
forbidden: "访客不可加入",
},
history_visibility: {
invited: "自从被邀请",
joined: "自从加入",
shared: "自从分享",
world_readable: "任何人",
},
unencrypted: "未加密",
},
},
reports: {
name: "报告事件",
fields: {
id: "ID",
received_ts: "报告时间",
user_id: "报告者",
name: "房间名",
score: "分数",
reason: "原因",
event_id: "事件 ID",
event_json: {
origin: "原始服务器",
origin_server_ts: "发送时间",
type: "事件类型",
content: {
msgtype: "内容类型",
body: "内容",
format: "格式",
formatted_body: "格式化的数据",
algorithm: "算法",
},
},
},
},
connections: {
name: "连接",
fields: {
last_seen: "日期",
ip: "IP 地址",
user_agent: "用户代理 (UA)",
},
},
devices: {
name: "设备",
fields: {
device_id: "设备 ID",
display_name: "设备名",
last_seen_ts: "时间戳",
last_seen_ip: "IP 地址",
},
action: {
erase: {
title: "移除 %{id}",
content: '您确定要移除设备 "%{name}"?',
success: "设备移除成功。",
failure: "出现了一个错误。",
},
},
},
users_media: {
name: "媒体文件",
fields: {
media_id: "媒体文件 ID",
media_length: "长度",
media_type: "类型",
upload_name: "文件名",
quarantined_by: "被隔离",
safe_from_quarantine: "取消隔离",
created_ts: "创建",
last_access_ts: "上一次访问",
},
},
delete_media: {
name: "媒体文件",
fields: {
before_ts: "最后访问时间",
size_gt: "大于 (字节)",
keep_profiles: "保留头像",
},
action: {
send: "删除媒体",
send_success: "请求发送成功。",
send_failure: "出现了一个错误。",
},
helper: {
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
},
},
pushers: {
name: "发布者",
fields: {
app: "App",
app_display_name: "App 名称",
app_id: "App ID",
device_display_name: "设备显示名",
kind: "类型",
lang: "语言",
profile_tag: "数据标签",
pushkey: "Pushkey",
data: { url: "URL" },
},
},
servernotices: {
name: "服务器提示",
send: "发送服务器提示",
fields: {
body: "信息",
},
action: {
send: "发送提示",
send_success: "服务器提示发送成功。",
send_failure: "出现了一个错误。",
},
helper: {
send: '向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
},
},
user_media_statistics: {
name: "用户的媒体文件",
fields: {
media_count: "媒体文件统计",
media_length: "媒体文件长度",
},
},
},
};
export default zh;
-3
View File
@@ -1,6 +1,3 @@
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import fetchMock from "jest-fetch-mock"; import fetchMock from "jest-fetch-mock";
configure({ adapter: new Adapter() });
fetchMock.enableMocks(); fetchMock.enableMocks();
+23 -8
View File
@@ -2,21 +2,37 @@ import { fetchUtils } from "react-admin";
const authProvider = { const authProvider = {
// called when the user attempts to log in // called when the user attempts to log in
login: ({ base_url, username, password }) => { login: ({ base_url, username, password, loginToken }) => {
// force homeserver for protection in case the form is manipulated
base_url = process.env.REACT_APP_SERVER || base_url;
console.log("login "); console.log("login ");
const options = { const options = {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify(
type: "m.login.password", Object.assign(
user: username, {
password: password, device_id: localStorage.getItem("device_id"),
initial_device_display_name: "Synapse Admin", initial_device_display_name: "Synapse Admin",
}), },
loginToken
? {
type: "m.login.token",
token: loginToken,
}
: {
type: "m.login.password",
user: username,
password: password,
}
)
),
}; };
// use the base_url from login instead of the well_known entry from the // 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 // server, since the admin might want to access the admin API via some
// private address // private address
base_url = base_url.replace(/\/+$/g, "");
localStorage.setItem("base_url", base_url); localStorage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url); const decoded_base_url = window.decodeURIComponent(base_url);
@@ -48,7 +64,6 @@ const authProvider = {
if (typeof access_token === "string") { if (typeof access_token === "string") {
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => { fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
localStorage.removeItem("access_token"); localStorage.removeItem("access_token");
localStorage.removeItem("device_id");
}); });
} }
return Promise.resolve(); return Promise.resolve();
+108 -4
View File
@@ -41,7 +41,9 @@ const resourceMap = {
data: "users", data: "users",
total: json => json.total, total: json => json.total,
create: data => ({ create: data => ({
endpoint: `/_synapse/admin/v2/users/${data.id}`, endpoint: `/_synapse/admin/v2/users/@${data.id}:${localStorage.getItem(
"home_server"
)}`,
body: data, body: data,
method: "PUT", method: "PUT",
}), }),
@@ -67,9 +69,8 @@ const resourceMap = {
return json.total_rooms; return json.total_rooms;
}, },
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v1/rooms/${params.id}/delete`, endpoint: `/_synapse/admin/v1/rooms/${params.id}`,
body: { block: false }, body: { block: false },
method: "POST",
}), }),
}, },
reports: { reports: {
@@ -117,6 +118,19 @@ const resourceMap = {
return json.total; return json.total;
}, },
}, },
room_state: {
map: rs => ({
...rs,
id: rs.event_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/rooms/${id}/state`,
}),
data: "state",
total: json => {
return json.state.length;
},
},
pushers: { pushers: {
map: p => ({ map: p => ({
...p, ...p,
@@ -170,6 +184,32 @@ const resourceMap = {
method: "POST", method: "POST",
}), }),
}, },
protect_media: {
map: pm => ({ id: pm.media_id }),
create: params => ({
endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unprotect/${params.media_id}`,
method: "POST",
}),
},
quarantine_media: {
map: qm => ({ id: qm.media_id }),
create: params => ({
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem(
"home_server"
)}/${params.media_id}`,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
"home_server"
)}/${params.media_id}`,
method: "POST",
}),
},
servernotices: { servernotices: {
map: n => ({ id: n.event_id }), map: n => ({ id: n.event_id }),
create: data => ({ create: data => ({
@@ -195,6 +235,65 @@ const resourceMap = {
return json.total; return json.total;
}, },
}, },
forward_extremities: {
map: fe => ({
...fe,
id: fe.event_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
}),
data: "results",
total: json => {
return json.count;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
}),
},
room_directory: {
path: "/_matrix/client/r0/publicRooms",
map: rd => ({
...rd,
id: rd.room_id,
public: !!rd.public,
guest_access: !!rd.guest_access,
avatar_src: mxcUrlToHttp(rd.avatar_url),
}),
data: "chunk",
total: json => {
return json.total_room_count_estimate;
},
create: params => ({
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
body: { visibility: "public" },
method: "PUT",
}),
delete: params => ({
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
body: { visibility: "private" },
method: "PUT",
}),
},
registration_tokens: {
path: "/_synapse/admin/v1/registration_tokens",
map: rt => ({
...rt,
id: rt.token,
}),
data: "registration_tokens",
total: json => {
return json.registration_tokens.length;
},
create: params => ({
endpoint: "/_synapse/admin/v1/registration_tokens/new",
body: params,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
}),
},
}; };
function filterNullValues(key, value) { function filterNullValues(key, value) {
@@ -216,7 +315,8 @@ function getSearchOrder(order) {
const dataProvider = { const dataProvider = {
getList: (resource, params) => { getList: (resource, params) => {
console.log("getList " + resource); console.log("getList " + resource);
const { user_id, name, guests, deactivated, search_term } = params.filter; const { user_id, name, guests, deactivated, search_term, valid } =
params.filter;
const { page, perPage } = params.pagination; const { page, perPage } = params.pagination;
const { field, order } = params.sort; const { field, order } = params.sort;
const from = (page - 1) * perPage; const from = (page - 1) * perPage;
@@ -228,6 +328,7 @@ const dataProvider = {
name: name, name: name,
guests: guests, guests: guests,
deactivated: deactivated, deactivated: deactivated,
valid: valid,
order_by: field, order_by: field,
dir: getSearchOrder(order), dir: getSearchOrder(order),
}; };
@@ -277,10 +378,13 @@ const dataProvider = {
getManyReference: (resource, params) => { getManyReference: (resource, params) => {
console.log("getManyReference " + resource); console.log("getManyReference " + resource);
const { page, perPage } = params.pagination; const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const from = (page - 1) * perPage; const from = (page - 1) * perPage;
const query = { const query = {
from: from, from: from,
limit: perPage, limit: perPage,
order_by: field,
dir: getSearchOrder(order),
}; };
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
+4445 -4342
View File
File diff suppressed because it is too large Load Diff