Compare commits

..

32 Commits

Author SHA1 Message Date
Manuel Stahl 7b5c0e2845 Merge tag '0.8.0' into amp.chat
Change-Id: I2e362a911083149c82a8c11b6c4594bb4c760a33
2021-05-04 15:07:42 +02:00
Manuel Stahl da8cb12756 Merge tag '0.7.2' into amp.chat
Change-Id: Ideb662d56977082af5757fed21573ff25ca52e27
2021-05-04 14:32:28 +02:00
Manuel Stahl 56a359b704 Merge tag '0.7.1' into amp.chat
Change-Id: Ib19b2cd22bae62b22057a3782ac978f83097fdd5
2021-05-04 14:32:05 +02:00
Michael Albert 5906dcc129 Merge tag '0.7.0' into amp.chat
Change-Id: I44a26f1fa0946a2b2beeb014d6905cdd2d15aaf6
2021-02-17 23:30:02 +01:00
Elshad Shirinov 270d48607a Allow server admin to create rooms for other users and change user power levels
Change-Id: Ie96e9e0102454835536b6f42d247f9e714e28480
2020-11-12 18:46:33 +01:00
Michael Albert 931fafc21d Allow to set user_type 'limited'
Change-Id: Ic3942a2150b9dfe57c106eb595b49b774fe8a30c
2020-10-20 18:56:04 +02:00
Michael Albert c604b47adc Allow to set a usertype
Change-Id: Ibfaa383b95dc5acc3b4dcd61f3f506f7c81f7dea
2020-10-20 13:57:23 +02:00
Manuel Stahl fb8cff3e3e Merge tag '0.5.0' into amp.chat
Change-Id: I410e194bc7b153c69e00f40a4486a46924cd510a
2020-09-03 09:08:01 +02:00
Michael Albert 725e24d944 Add credentials to PDF
Fix Umlauts in PDF
Reorder elements of PDF

Change-Id: I49335584ef282e4b960275013ea7d16053b9f773
2020-08-24 07:57:40 +00:00
Manuel Stahl dd00a76603 Merge tag '0.4.2' into amp.chat
Change-Id: Id12309f0a4d3ff9983325e69131d5eebe5bd0bde
2020-07-30 12:56:20 +02:00
Manuel Stahl 2915fd3e5b Merge tag '0.4.1' into amp.chat
Change-Id: I44c9f00e5aa7abe413f8a819e1143bebc4f08ce2
2020-07-28 15:09:48 +02:00
Michael Albert a4662c2557 Translate room info
Change-Id: I7f3121da3c910592ecfcb4bca9dee34f2757f567
2020-06-10 07:18:58 +00:00
Michael Albert f6ca169fbc Fix data provider
Change-Id: Id1c929f593833ed35327e70d1d0dc8182a4b7306
2020-05-25 21:20:49 +02:00
Michael Albert 07862591fd Possibility to encrypt new rooms
Change-Id: Ie415a0f8ecec646510ac8f2f0adca58064e30da5
2020-05-25 13:25:46 +02:00
Manuel Stahl ab649fbf70 Merge branch 'master' into amp.chat
Change-Id: I6141964157bcb7218e2e6368a3ca8d20eb4855e9
2020-05-05 13:44:06 +02:00
Timo Paulssen 880223e5de Offer invitations in room creation
Turns the "Create Room" form into a tabbed form with
tabs that mimic the room display. In the "Members" tab
an AutocompleteArrayInput allows selecting multiple
users by their displayname.

The displayname is also what is displayed ìn the
invitations list.

Creating the room immediately sends out the invitations
as well.

Change-Id: I3915144114ffe4c629848363c9cb7917634d04d8
2020-04-28 19:36:15 +02:00
Manuel Stahl 76fdc80e3e Merge branch 'master' into amp.chat
Change-Id: I08a7a34e041993c29bb12fff52d07534374cda4e
2020-04-28 16:35:40 +02:00
Manuel Stahl 375649756f Add page to show room details
Change-Id: Iec4f402c4322d775cc14c567069a3295ad383b44
2020-04-28 16:30:47 +02:00
Manuel Stahl 662735a91f Adapt for changes in v2/users API
Change-Id: I927b81882fa20e5b3de3d9fc216e2136f7036bba
2020-04-28 16:30:47 +02:00
Manuel Stahl 0823976edd Cleanup room creation
Change-Id: Ieb5189513d21606f8d0bea5692112350a68f2e14
2020-04-23 16:31:26 +02:00
Michael Albert d3cd2e9e33 Fix localStorage entry of homeserver url
Change-Id: I206e3b4428df1f51d4281ad4db26bd64bdffb85d
2020-04-21 17:42:43 +02:00
Timo Paulssen 24abcd4e4a Normalize alias a little, display initial sigil
turns all whitespace into underscores, shows leading
sigil if the alias is non-empty, so the user doesn't get
confused about whether they have to input a # or not.

Change-Id: Ic81e69cc3f0074d63a67b976c9bda32f8de025de
2020-04-20 19:31:33 +02:00
Timo Paulssen c1c32e3268 Offer "alias" field in room create form
Tries its best to not allow aliases that are too
long (full alias including leading #, : in the
middle, and homeserver domain name must not exceed
255 bytes.

Change-Id: I1e784a94cb599eca7e30736d666b20e37aad5444
2020-04-20 19:31:33 +02:00
Timo Paulssen ca15435625 Offer room creation form
A choice of public or private is offered, which maps to matrix'
visibility parameter. A name can also be provided.

Change-Id: I34d99acbc4624a9ed54ca6f6609573d5fc1049da
2020-04-20 19:31:33 +02:00
Michael Albert e9c3901b68 Merge branch 'master' into amp.chat
Change-Id: Iac4e56401aab3f7f39b623b617990ec1952f8cd0
2020-04-20 16:57:23 +02:00
Michael Albert 7aec6f9369 Allow searching for users
Change-Id: Icf4a3b05b24c66971f55b22e7540a1dc904a3a92
2020-04-20 11:22:06 +00:00
Michael Albert d2a3f07a59 Fix QR code creation
Change-Id: Ib6bbd1be6d4dca1f617043c3c2338924b2321ea3
2020-04-20 12:15:52 +02:00
Manuel Stahl bf7867f106 Merge branch 'master' into amp.chat
Change-Id: I45b7a6db27456aaa2eca66b406cdaa49e492e61e
2020-02-11 18:56:53 +01:00
Michael Albert f0e32abc4f Fix QR code creation
Change-Id: If05856a6fdafa43a93c6b57963820710db188d42
2020-02-11 17:35:19 +00:00
Michael Albert 61b1580735 Fix redirect after create/edit user
Change-Id: Icdb797bf6b1a47cbeff901b1952672584b2e8e8f
2020-02-11 17:34:32 +00:00
Manuel Stahl 0f7e4c1909 Create PDF with QR code on user create/edit
Change-Id: Ib89b68e956d96002ddbf6ac5ddcaea73b5b3e3fb
2020-02-10 13:10:08 +01:00
Michael Albert c9bce409d2 Prefill user_id and password on user creation
Change-Id: I3f604f38c1842f155f3b39da20ba45992ba522be
2020-02-10 13:10:08 +01:00
44 changed files with 9087 additions and 9251 deletions
-5
View File
@@ -1,5 +0,0 @@
# 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
-20
View File
@@ -1,20 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
ignore:
# Major updates for react-admin have breaking changes
- dependency-name: "react-admin"
update-types: ["version-update:semver-major"]
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
-21
View File
@@ -1,21 +0,0 @@
name: build-test
on:
push:
branches: ["master"]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Run tests
run: yarn test
-51
View File
@@ -1,51 +0,0 @@
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@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
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@v3
with:
context: .
push: true
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
platforms: linux/amd64,linux/arm64
-26
View File
@@ -1,26 +0,0 @@
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@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
- name: Install and Build 🔧
run: |
yarn install
yarn build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.1
with:
branch: gh-pages
folder: build
-31
View File
@@ -1,31 +0,0 @@
name: Create release tarball and attach to tag
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
- run: yarn install
- run: yarn build
- run: |
version=`git describe --dirty --tags || echo unknown`
mkdir -p dist
cp -r build synapse-admin-$version
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
with:
files: dist/*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-51
View File
@@ -1,51 +0,0 @@
name: Test docker image creation
on:
push:
# Sequence of patterns matched against refs/heads
# prettier-ignore
branches:
# Push events on branch fix_docker_cd
- fix_docker_cd
# 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@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
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@v3
with:
context: .
push: false
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
platforms: linux/amd64,linux/arm64
+2 -2
View File
@@ -6,6 +6,6 @@
"singleQuote": false, "singleQuote": false,
"trailingComma": "es5", "trailingComma": "es5",
"bracketSpacing": true, "bracketSpacing": true,
"bracketSameLine": false, "jsxBracketSameLine": false,
"arrowParens": "avoid" "arrowParens": "avoid",
} }
+1 -2
View File
@@ -1,6 +1,5 @@
dist: focal
language: node_js language: node_js
node_js: node_js:
- 17 - 13
cache: yarn cache: yarn
+3 -6
View File
@@ -1,14 +1,11 @@
# Builder # Builder
FROM node:lts as builder FROM node:current as builder
ARG PUBLIC_URL=/
ARG REACT_APP_SERVER
WORKDIR /src WORKDIR /src
COPY . /src COPY . /src
RUN yarn --network-timeout=300000 install RUN yarn --network-timeout=100000 install
RUN PUBLIC_URL=$PUBLIC_URL REACT_APP_SERVER=$REACT_APP_SERVER yarn build RUN yarn build
# App # App
+11 -50
View File
@@ -1,62 +1,31 @@
[![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE) [![Build Status](https://travis-ci.org/Awesome-Technologies/synapse-admin.svg?branch=master)](https://travis-ci.org/Awesome-Technologies/synapse-admin)
[![Build Status](https://api.travis-ci.com/Awesome-Technologies/synapse-admin.svg?branch=master)](https://app.travis-ci.com/github/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)
[![gh-pages](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/edge_ghpage.yml/badge.svg)](https://awesome-technologies.github.io/synapse-admin/)
[![docker-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/docker-release.yml/badge.svg)](https://hub.docker.com/r/awesometechnologies/synapse-admin)
[![github-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/github-release.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/releases)
# 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/).
## Usage It needs at least Synapse v1.27.0 for all functions to work as expected!
### Supported Synapse
It needs at least [Synapse](https://github.com/matrix-org/synapse) v1.52.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://matrix-org.github.io/synapse/develop/admin_api/version_api.html). See also [Synapse version API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/version_api.rst).
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.
### Prerequisites
You need access to the following endpoints: You need access to the following endpoints:
- `/_matrix` - `/_matrix`
- `/_synapse/admin` - `/_synapse/admin`
See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints) See also [Synapse administration endpoints](https://github.com/matrix-org/synapse/blob/develop/docs/reverse_proxy.md#synapse-administration-endpoints)
### Use without install ## Step-By-Step install:
You can use the current version of Synapse Admin without own installation direct You have two options:
via [GitHub Pages](https://awesome-technologies.github.io/synapse-admin/).
**Note:** 1. Download the source code from github and run using nodejs
If you want to use the deployment, you have to make sure that the admin endpoints (`/_synapse/admin`) are accessible for your browser. 2. Run the Docker container
**Remember: You have no need to expose these endpoints to the internet but to your network.**
If you want your own deployment, follow the [Step-By-Step Install Guide](#step-by-step-install) below.
### Step-By-Step install Steps for 1):
You have three options:
1. [Download the tarball and serve with any webserver](#steps-for-1)
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)
- 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`
@@ -64,14 +33,9 @@ You have three options:
- download dependencies: `yarn install` - download dependencies: `yarn install`
- start web server: `yarn start` - start web server: `yarn start`
You can fix the homeserver, so that the user can no longer define it himself. Steps for 2):
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/).
#### 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`
- 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.
@@ -86,9 +50,6 @@ or by editing it in the [.env](.env) file. See also the
context: https://github.com/Awesome-Technologies/synapse-admin.git context: https://github.com/Awesome-Technologies/synapse-admin.git
# args: # args:
# - NODE_OPTIONS="--max_old_space_size=1024" # - NODE_OPTIONS="--max_old_space_size=1024"
# # see #266, PUBLIC_URL must be without surrounding quotation marks
# - PUBLIC_URL=/synapse-admin
# - REACT_APP_SERVER="https://matrix.example.com"
ports: ports:
- "8080:80" - "8080:80"
restart: unless-stopped restart: unless-stopped
+1 -6
View File
@@ -12,15 +12,10 @@ services:
# replace the context definition with this: # replace the context definition with this:
# context: https://github.com/Awesome-Technologies/synapse-admin.git # context: https://github.com/Awesome-Technologies/synapse-admin.git
# args:
# if you're building on an architecture other than amd64, make sure # if you're building on an architecture other than amd64, make sure
# to define a maximum ram for node. otherwise the build will fail. # to define a maximum ram for node. otherwise the build will fail.
# args:
# - NODE_OPTIONS="--max_old_space_size=1024" # - NODE_OPTIONS="--max_old_space_size=1024"
# default is /
# - PUBLIC_URL=/synapse-admin
# You can use a fixed homeserver, so that the user can no longer
# define it himself
# - REACT_APP_SERVER="https://matrix.example.com"
ports: ports:
- "8080:80" - "8080:80"
restart: unless-stopped restart: unless-stopped
+20 -20
View File
@@ -1,6 +1,6 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.8.5", "version": "AMP/2021.05",
"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",
@@ -10,31 +10,31 @@
"url": "https://github.com/Awesome-Technologies/synapse-admin" "url": "https://github.com/Awesome-Technologies/synapse-admin"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^11.2.6", "@testing-library/react": "^10.0.2",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^12.0.11",
"eslint": "^8.32.0", "enzyme": "^3.11.0",
"eslint-config-prettier": "^8.3.0", "enzyme-adapter-react-16": "^1.15.2",
"eslint-config-react-app": "^7.0.1", "eslint": "^6.8.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.2",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^2.2.0", "prettier": "^2.0.0",
"ra-test": "^3.15.0" "ra-test": "^3.14.0"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.7.1", "@progress/kendo-drawing": "^1.6.0",
"@emotion/styled": "^11.6.0", "@progress/kendo-react-pdf": "^3.10.1",
"@mui/icons-material": "^5.3.1", "babel-preset-jest": "^24.9.0",
"@mui/material": "^5.4.0",
"papaparse": "^5.2.0", "papaparse": "^5.2.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"qrcode.react": "^1.0.0",
"ra-language-chinese": "^2.0.10", "ra-language-chinese": "^2.0.10",
"ra-language-french": "^4.2.0", "ra-language-german": "^2.1.2",
"ra-language-german": "^3.13.4",
"react": "^17.0.0", "react": "^17.0.0",
"react-admin": "^3.19.7", "react-admin": "^3.14.0",
"react-dom": "^17.0.2", "react-dom": "^16.14.0",
"react-scripts": "^5.0.1" "react-scripts": "^3.4.4"
}, },
"scripts": { "scripts": {
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start", "start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
@@ -42,7 +42,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 --ignore-path .gitignore \"**/*.{js,jsx,json,md,scss,yaml,yml}\"", "prettier": "prettier \"**/*.{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",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

+27 -1
View File
@@ -9,6 +9,32 @@
name="description" name="description"
content="Synapse-Admin" content="Synapse-Admin"
/> />
<style>
@font-face {
font-family: "DejaVu Sans";
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans";
font-weight: bold;
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Bold.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans";
font-style: italic;
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Oblique.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans";
font-weight: bold;
font-style: italic;
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Oblique.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans Mono";
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Mono.ttf") format("truetype");
}
</style>
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -46,4 +72,4 @@
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+16 -33
View File
@@ -4,36 +4,27 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider"; import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider"; import dataProvider from "./synapse/dataProvider";
import { UserList, UserCreate, UserEdit } from "./components/users"; import { UserList, UserCreate, UserEdit } from "./components/users";
import { RoomList, RoomShow } from "./components/rooms"; import { RoomList, RoomCreate, RoomShow, RoomEdit } 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 ConfirmationNumberIcon from "@mui/icons-material/ConfirmationNumber"; import UserIcon from "@material-ui/icons/Group";
import CloudQueueIcon from "@mui/icons-material/CloudQueue"; import EqualizerIcon from "@material-ui/icons/Equalizer";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import UserIcon from "@mui/icons-material/Group";
import { UserMediaStatsList } from "./components/statistics"; import { UserMediaStatsList } from "./components/statistics";
import RoomIcon from "@mui/icons-material/ViewList"; import RoomIcon from "@material-ui/icons/ViewList";
import ReportIcon from "@mui/icons-material/Warning"; import ReportIcon from "@material-ui/icons/Warning";
import FolderSharedIcon from "@mui/icons-material/FolderShared"; import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { DestinationList, DestinationShow } from "./components/destinations";
import { ImportFeature } from "./components/ImportFeature"; import { ImportFeature } from "./components/ImportFeature";
import {
RegistrationTokenCreate,
RegistrationTokenEdit,
RegistrationTokenList,
} from "./components/RegistrationTokens";
import { RoomDirectoryList } from "./components/RoomDirectory"; 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 frenchMessages from "./i18n/fr";
import chineseMessages from "./i18n/zh"; import chineseMessages from "./i18n/zh";
import ShowUserPdf from "./components/ShowUserPdf";
// 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,
fr: frenchMessages,
zh: chineseMessages, zh: chineseMessages,
}; };
const i18nProvider = polyglotI18nProvider( const i18nProvider = polyglotI18nProvider(
@@ -49,7 +40,7 @@ const App = () => (
dataProvider={dataProvider} dataProvider={dataProvider}
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
customRoutes={[ customRoutes={[
<Route key="userImport" path="/import_users" component={ImportFeature} />, <Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
]} ]}
> >
<Resource <Resource
@@ -59,7 +50,14 @@ const App = () => (
edit={UserEdit} edit={UserEdit}
icon={UserIcon} icon={UserIcon}
/> />
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} /> <Resource
name="rooms"
list={RoomList}
create={RoomCreate}
show={RoomShow}
edit={RoomEdit}
icon={RoomIcon}
/>
<Resource <Resource
name="user_media_statistics" name="user_media_statistics"
list={UserMediaStatsList} list={UserMediaStatsList}
@@ -76,19 +74,6 @@ const App = () => (
list={RoomDirectoryList} list={RoomDirectoryList}
icon={FolderSharedIcon} icon={FolderSharedIcon}
/> />
<Resource
name="destinations"
list={DestinationList}
show={DestinationShow}
icon={CloudQueueIcon}
/>
<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" />
@@ -96,9 +81,7 @@ 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" /> <Resource name="room_state" />
<Resource name="destination_rooms" />
</Admin> </Admin>
); );
+7 -2
View File
@@ -1,9 +1,14 @@
import React from "react"; import React from "react";
import { render } from "@testing-library/react"; import { TestContext } from "ra-test";
import { shallow } from "enzyme";
import App from "./App"; import App from "./App";
describe("App", () => { describe("App", () => {
it("renders", () => { it("renders", () => {
render(<App />); shallow(
<TestContext>
<App />
</TestContext>
);
}); });
}); });
+27 -14
View File
@@ -12,17 +12,8 @@ import {
TextField, TextField,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import PageviewIcon from "@mui/icons-material/Pageview"; import PageviewIcon from "@material-ui/icons/Pageview";
import ViewListIcon from "@mui/icons-material/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]} />
@@ -42,7 +33,14 @@ export const ReportShow = props => {
<DateField <DateField
source="received_ts" source="received_ts"
showTime showTime
options={date_format} options={{
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">
@@ -70,10 +68,18 @@ export const ReportShow = props => {
icon={<PageviewIcon />} icon={<PageviewIcon />}
path="detail" path="detail"
> >
{" "}
<DateField <DateField
source="event_json.origin_server_ts" source="event_json.origin_server_ts"
showTime showTime
options={date_format} options={{
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">
@@ -110,7 +116,14 @@ export const ReportList = ({ ...props }) => {
<DateField <DateField
source="received_ts" source="received_ts"
showTime showTime
options={date_format} options={{
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" />
+4 -4
View File
@@ -6,19 +6,19 @@ import {
Title, Title,
} from "react-admin"; } from "react-admin";
import { parse as parseCsv, unparse as unparseCsv } from "papaparse"; import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
import GetAppIcon from "@mui/icons-material/GetApp"; import GetAppIcon from "@material-ui/icons/GetApp";
import { import {
Button, Button,
Card, Card,
CardActions, CardActions,
CardContent, CardContent,
CardHeader, CardHeader,
Checkbox,
Container,
FormControlLabel, FormControlLabel,
Checkbox,
NativeSelect, NativeSelect,
} from "@mui/material"; } from "@material-ui/core";
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import Container from "@material-ui/core/Container/Container";
import { generateRandomUser } from "./users"; import { generateRandomUser } from "./users";
const LOGGING = true; const LOGGING = true;
+13 -101
View File
@@ -21,9 +21,9 @@ import {
MenuItem, MenuItem,
Select, Select,
TextField, TextField,
} from "@mui/material"; } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@material-ui/icons/Lock";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
main: { main: {
@@ -78,48 +78,10 @@ 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 } = {},
@@ -149,9 +111,7 @@ 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( !values.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/)
/^(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");
} }
@@ -169,19 +129,11 @@ const LoginPage = ({ theme }) => {
: typeof error === "undefined" || !error.message : typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error" ? "ra.auth.sign_in_error"
: error.message, : error.message,
{ type: "warning" } "warning"
); );
}); });
}; };
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;
@@ -195,7 +147,7 @@ const LoginPage = ({ theme }) => {
const [serverVersion, setServerVersion] = useState(""); const [serverVersion, setServerVersion] = useState("");
const handleUsernameChange = _ => { const handleUsernameChange = _ => {
if (formData.base_url || cfg_base_url) return; if (formData.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`;
@@ -233,31 +185,6 @@ 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]
); );
@@ -269,10 +196,9 @@ const LoginPage = ({ theme }) => {
autoFocus autoFocus
name="username" name="username"
component={renderInput} component={renderInput}
label="ra.auth.username" label={translate("ra.auth.username")}
disabled={loading || !supportPassAuth} disabled={loading}
onBlur={handleUsernameChange} onBlur={handleUsernameChange}
resettable
fullWidth fullWidth
/> />
</div> </div>
@@ -280,10 +206,9 @@ const LoginPage = ({ theme }) => {
<PasswordInput <PasswordInput
name="password" name="password"
component={renderInput} component={renderInput}
label="ra.auth.password" label={translate("ra.auth.password")}
type="password" type="password"
disabled={loading || !supportPassAuth} disabled={loading}
resettable
fullWidth fullWidth
/> />
</div> </div>
@@ -291,9 +216,8 @@ const LoginPage = ({ theme }) => {
<TextInput <TextInput
name="base_url" name="base_url"
component={renderInput} component={renderInput}
label="synapseadmin.auth.base_url" label={translate("synapseadmin.auth.base_url")}
disabled={cfg_base_url || loading} disabled={loading}
resettable
fullWidth fullWidth
/> />
</div> </div>
@@ -304,7 +228,7 @@ const LoginPage = ({ theme }) => {
return ( return (
<Form <Form
initialValues={{ base_url: cfg_base_url || base_url }} initialValues={{ base_url: base_url }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
validate={validate} validate={validate}
render={({ handleSubmit }) => ( render={({ handleSubmit }) => (
@@ -331,7 +255,6 @@ 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="fr">Français</MenuItem>
<MenuItem value="zh">简体中文</MenuItem> <MenuItem value="zh">简体中文</MenuItem>
</Select> </Select>
</div> </div>
@@ -344,24 +267,13 @@ const LoginPage = ({ theme }) => {
variant="contained" variant="contained"
type="submit" type="submit"
color="primary" color="primary"
disabled={loading || !supportPassAuth} disabled={loading}
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", () => {
render( shallow(
<TestContext> <TestContext>
<LoginPage /> <LoginPage />
</TestContext> </TestContext>
+3 -3
View File
@@ -1,10 +1,10 @@
// in src/Menu.js // in src/Menu.js
import * as React from "react"; import * as React from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useMediaQuery } from "@mui/material"; import { useMediaQuery } from "@material-ui/core";
import { MenuItemLink, getResources } from "react-admin"; import { MenuItemLink, getResources } from "react-admin";
import DefaultIcon from "@mui/icons-material/ViewList"; import DefaultIcon from "@material-ui/icons/ViewList";
import LabelIcon from "@mui/icons-material/Label"; import LabelIcon from "@material-ui/icons/Label";
const Menu = ({ onMenuClick, logout }) => { const Menu = ({ onMenuClick, logout }) => {
const isXSmall = useMediaQuery(theme => theme.breakpoints.down("xs")); const isXSmall = useMediaQuery(theme => theme.breakpoints.down("xs"));
-132
View File
@@ -1,132 +0,0 @@
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>
);
};
+19 -27
View File
@@ -1,7 +1,8 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { Avatar, Chip } from "@mui/material"; import Avatar from "@material-ui/core/Avatar";
import { Chip } from "@material-ui/core";
import { connect } from "react-redux"; import { connect } from "react-redux";
import FolderSharedIcon from "@mui/icons-material/FolderShared"; import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { import {
BooleanField, BooleanField,
@@ -18,7 +19,6 @@ import {
useMutation, useMutation,
useNotify, useNotify,
useTranslate, useTranslate,
useRecordContext,
useRefresh, useRefresh,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
@@ -59,7 +59,7 @@ export const RoomDirectoryBulkDeleteButton = props => (
<BulkDeleteButton <BulkDeleteButton
{...props} {...props}
label="resources.room_directory.action.erase" label="resources.room_directory.action.erase"
mutationMode="pessimistic" undoable={false}
confirmTitle="resources.room_directory.action.title" confirmTitle="resources.room_directory.action.title"
confirmContent="resources.room_directory.action.content" confirmContent="resources.room_directory.action.content"
resource="room_directory" resource="room_directory"
@@ -87,9 +87,7 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
refresh(); refresh();
}, },
onFailure: error => onFailure: error =>
notify("resources.room_directory.action.send_failure", { notify("resources.room_directory.action.send_failure", "error"),
type: "error",
}),
} }
); );
}; };
@@ -105,8 +103,7 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
); );
}; };
export const RoomDirectorySaveButton = props => { export const RoomDirectorySaveButton = ({ record }) => {
const record = useRecordContext();
const notify = useNotify(); const notify = useNotify();
const refresh = useRefresh(); const refresh = useRefresh();
const [create, { loading }] = useCreate("room_directory"); const [create, { loading }] = useCreate("room_directory");
@@ -122,9 +119,7 @@ export const RoomDirectorySaveButton = props => {
refresh(); refresh();
}, },
onFailure: error => onFailure: error =>
notify("resources.room_directory.action.send_failure", { notify("resources.room_directory.action.send_failure", "error"),
type: "error",
}),
} }
); );
}; };
@@ -176,13 +171,10 @@ const RoomDirectoryFilter = ({ ...props }) => {
); );
}; };
export const FilterableRoomDirectoryList = ({ export const FilterableRoomDirectoryList = ({ ...props }) => {
roomDirectoryFilters,
dispatch,
...props
}) => {
const classes = useStyles(); const classes = useStyles();
const filter = roomDirectoryFilters; const translate = useTranslate();
const filter = props.roomDirectoryFilters;
const roomIdFilter = filter && filter.room_id ? true : false; const roomIdFilter = filter && filter.room_id ? true : false;
const topicFilter = filter && filter.topic ? true : false; const topicFilter = filter && filter.topic ? true : false;
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false; const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
@@ -195,53 +187,53 @@ export const FilterableRoomDirectoryList = ({
filters={<RoomDirectoryFilter />} filters={<RoomDirectoryFilter />}
perPage={100} perPage={100}
> >
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}> <Datagrid>
<AvatarField <AvatarField
source="avatar_src" source="avatar_src"
sortable={false} sortable={false}
className={classes.small} className={classes.small}
label="resources.rooms.fields.avatar" label={translate("resources.rooms.fields.avatar")}
/> />
<TextField <TextField
source="name" source="name"
sortable={false} sortable={false}
label="resources.rooms.fields.name" label={translate("resources.rooms.fields.name")}
/> />
{roomIdFilter && ( {roomIdFilter && (
<TextField <TextField
source="room_id" source="room_id"
sortable={false} sortable={false}
label="resources.rooms.fields.room_id" label={translate("resources.rooms.fields.room_id")}
/> />
)} )}
{canonicalAliasFilter && ( {canonicalAliasFilter && (
<TextField <TextField
source="canonical_alias" source="canonical_alias"
sortable={false} sortable={false}
label="resources.rooms.fields.canonical_alias" label={translate("resources.rooms.fields.canonical_alias")}
/> />
)} )}
{topicFilter && ( {topicFilter && (
<TextField <TextField
source="topic" source="topic"
sortable={false} sortable={false}
label="resources.rooms.fields.topic" label={translate("resources.rooms.fields.topic")}
/> />
)} )}
<NumberField <NumberField
source="num_joined_members" source="num_joined_members"
sortable={false} sortable={false}
label="resources.rooms.fields.joined_members" label={translate("resources.rooms.fields.joined_members")}
/> />
<BooleanField <BooleanField
source="world_readable" source="world_readable"
sortable={false} sortable={false}
label="resources.room_directory.fields.world_readable" label={translate("resources.room_directory.fields.world_readable")}
/> />
<BooleanField <BooleanField
source="guest_can_join" source="guest_can_join"
sortable={false} sortable={false}
label="resources.room_directory.fields.guest_can_join" label={translate("resources.room_directory.fields.guest_can_join")}
/> />
</Datagrid> </Datagrid>
</List> </List>
+35
View File
@@ -0,0 +1,35 @@
import React, { useCallback } from "react";
import { SaveButton, useCreate, useRedirect, useNotify } from "react-admin";
const SaveQrButton = props => {
const [create] = useCreate("users");
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath } = props;
const handleSave = useCallback(
(values, redirect) => {
create(
{
payload: { data: { ...values } },
},
{
onSuccess: ({ data: newRecord }) => {
notify("ra.notification.created", "info", {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, {
password: values.password,
...newRecord,
});
},
}
);
},
[create, notify, redirectTo, basePath]
);
return <SaveButton {...props} onSave={handleSave} />;
};
export default SaveQrButton;
+10 -21
View File
@@ -9,28 +9,22 @@ import {
useCreate, useCreate,
useMutation, useMutation,
useNotify, useNotify,
useRecordContext,
useTranslate, useTranslate,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import MessageIcon from "@mui/icons-material/Message"; import MessageIcon from "@material-ui/icons/Message";
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@material-ui/icons/Cancel";
import { import Dialog from "@material-ui/core/Dialog";
Dialog, import DialogContent from "@material-ui/core/DialogContent";
DialogContent, import DialogContentText from "@material-ui/core/DialogContentText";
DialogContentText, import DialogTitle from "@material-ui/core/DialogTitle";
DialogTitle,
} from "@mui/material";
const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => { const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
const translate = useTranslate(); const translate = useTranslate();
const ServerNoticeToolbar = props => ( const ServerNoticeToolbar = props => (
<Toolbar {...props}> <Toolbar {...props}>
<SaveButton <SaveButton label="resources.servernotices.action.send" />
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>
@@ -67,8 +61,7 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
); );
}; };
export const ServerNoticeButton = props => { export const ServerNoticeButton = ({ record }) => {
const record = useRecordContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const notify = useNotify(); const notify = useNotify();
const [create, { loading }] = useCreate("servernotices"); const [create, { loading }] = useCreate("servernotices");
@@ -85,9 +78,7 @@ export const ServerNoticeButton = props => {
handleDialogClose(); handleDialogClose();
}, },
onFailure: () => onFailure: () =>
notify("resources.servernotices.action.send_failure", { notify("resources.servernotices.action.send_failure", "error"),
type: "error",
}),
} }
); );
}; };
@@ -133,9 +124,7 @@ export const ServerNoticeBulkButton = ({ selectedIds }) => {
handleDialogClose(); handleDialogClose();
}, },
onFailure: error => onFailure: error =>
notify("resources.servernotices.action.send_failure", { notify("resources.servernotices.action.send_failure", "error"),
type: "error",
}),
} }
); );
}; };
+210
View File
@@ -0,0 +1,210 @@
import React from "react";
import { Title, Button } from "react-admin";
import { makeStyles } from "@material-ui/core/styles";
import { PDFExport } from "@progress/kendo-react-pdf";
import QRCode from "qrcode.react";
function xor(a, b) {
var res = "";
for (var i = 0; i < a.length; i++) {
res += String.fromCharCode(a.charCodeAt(i) ^ b.charCodeAt(i % b.length));
}
return res;
}
function calculateQrString(serverUrl, username, password) {
const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7";
var urlString = "user=" + username + "&password=" + password;
urlString = xor(urlString, magicString); // xor with magic string
urlString = btoa(urlString); // to base64
return serverUrl + "/#" + urlString;
}
const ShowUserPdf = props => {
const useStyles = makeStyles(theme => ({
page: {
height: 800,
width: 566,
padding: "none",
backgroundColor: "white",
boxShadow: "5px 5px 5px black",
margin: "auto",
overflowX: "hidden",
overflowY: "hidden",
fontFamily: "DejaVu Sans, Sans-Serif",
fontSize: 15,
},
header: {
height: 144,
width: 534,
marginLeft: 32,
marginTop: 15,
},
name: {
width: 240,
fontSize: 35,
float: "left",
marginTop: 100,
},
logo: {
width: 90,
marginTop: 50,
marginRight: 70,
float: "right",
},
body: {
clear: "both",
},
table_cell: {
verticalAlign: "top",
},
code_note: {
marginLeft: 32,
marginTop: 86,
},
qr: {
marginTop: 15,
marginLeft: 32,
},
credentials_note: {
marginTop: 86,
marginLeft: 10,
},
credentials_text: {
marginLeft: 10,
fontSize: 12,
},
credentials: {
fontFamily: "DejaVu Sans Mono, monospace",
},
note: {
fontSize: 18,
marginTop: 100,
marginLeft: 32,
marginRight: 32,
},
}));
const classes = useStyles();
var resume;
const exportPDF = () => {
resume.save();
};
var qrCode = "";
var displayname = "";
var id = "";
var password = "";
var username = "";
var serverUrl = "";
if (
props.location.state &&
props.location.state.id &&
props.location.state.password
) {
id = props.location.state.id;
password = props.location.state.password;
username = id.substring(1, id.indexOf(":"));
serverUrl = "https://" + id.substring(id.indexOf(":") + 1);
const qrString = calculateQrString(serverUrl, username, password);
qrCode = <QRCode value={qrString} size={128} />;
displayname = props.location.state.displayname;
}
return (
<div>
<Title title="PDF" />
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
<PDFExport
paperSize={"A4"}
fileName="User.pdf"
title=""
subject=""
keywords=""
ref={r => (resume = r)}
>
<div className={classes.page}>
<div className={classes.header}>
<div className={classes.name}>{displayname}</div>
<img className={classes.logo} alt="Logo" src="images/logo.png" />
</div>
<div className={classes.body}>
<table>
<tbody>
<tr>
<td width="200px">
<div className={classes.code_note}>
Ihr persönlicher Anmeldecode:
</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_note}>
Ihre persönlichen Zugangsdaten:
</div>
</td>
</tr>
<tr>
<td>
<div className={classes.qr}>{qrCode}</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_text}>
<br />
<table>
<tbody>
<tr>
<td>Heimserver:</td>
<td>
<span className={classes.credentials}>
{serverUrl}
</span>
</td>
</tr>
<tr>
<td>Benutzername:</td>
<td>
<span className={classes.credentials}>
{username}
</span>
</td>
</tr>
<tr>
<td>Passwort:</td>
<td>
<span className={classes.credentials}>
{password}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<div className={classes.note}>
Hier können Sie Ihre selbst gewählte
Schlüsselsicherungs-Passphrase notieren:
<br />
<br />
<br />
<hr />
</div>
</div>
</div>
</PDFExport>
</div>
);
};
export default ShowUserPdf;
-185
View File
@@ -1,185 +0,0 @@
import React from "react";
import {
Button,
Datagrid,
DateField,
Filter,
List,
Pagination,
ReferenceField,
ReferenceManyField,
SearchInput,
Show,
Tab,
TabbedShowLayout,
TextField,
TopToolbar,
useRecordContext,
useDelete,
useNotify,
useRefresh,
useTranslate,
} from "react-admin";
import AutorenewIcon from "@material-ui/icons/Autorenew";
import FolderSharedIcon from "@material-ui/icons/FolderShared";
import ViewListIcon from "@material-ui/icons/ViewList";
const DestinationPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const destinationRowStyle = (record, index) => ({
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
});
const DestinationFilter = ({ ...props }) => {
return (
<Filter {...props}>
<SearchInput source="destination" alwaysOn />
</Filter>
);
};
export const DestinationReconnectButton = props => {
const record = useRecordContext();
const refresh = useRefresh();
const notify = useNotify();
const [handleReconnect, { isLoading }] = useDelete("destinations");
// Reconnect is not required if no error has occurred. (`failure_ts`)
if (!record || !record.failure_ts) return null;
const handleClick = e => {
// Prevents redirection to the detail page when clicking in the list
e.stopPropagation();
handleReconnect(
{ payload: { id: record.id } },
{
onSuccess: () => {
notify("ra.notification.updated", {
messageArgs: { smart_count: 1 },
});
refresh();
},
onFailure: () => {
notify("ra.message.error", { type: "error" });
},
}
);
};
return (
<Button
label="resources.destinations.action.reconnect"
onClick={handleClick}
disabled={isLoading}
>
<AutorenewIcon />
</Button>
);
};
const DestinationShowActions = props => (
<TopToolbar>
<DestinationReconnectButton />
</TopToolbar>
);
const DestinationTitle = props => {
const record = useRecordContext();
const translate = useTranslate();
return (
<span>
{translate("resources.destinations.name", 1)} {record.destination}
</span>
);
};
export const DestinationList = props => {
return (
<List
{...props}
filters={<DestinationFilter />}
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
bulkActionButtons={false}
>
<Datagrid
rowStyle={destinationRowStyle}
rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`}
>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
<DestinationReconnectButton />
</Datagrid>
</List>
);
};
export const DestinationShow = props => {
const translate = useTranslate();
return (
<Show
actions={<DestinationShowActions />}
title={<DestinationTitle />}
{...props}
>
<TabbedShowLayout>
<Tab label="status" icon={<ViewListIcon />}>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
</Tab>
<Tab
label={translate("resources.rooms.name", { smart_count: 2 })}
icon={<FolderSharedIcon />}
path="rooms"
>
<ReferenceManyField
reference="destination_rooms"
target="destination"
addLabel={false}
pagination={<DestinationPagination />}
perPage={50}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) => `/rooms/${id}/show`}
>
<TextField
source="room_id"
label="resources.rooms.fields.room_id"
/>
<TextField source="stream_ordering" sortable={false} />
<ReferenceField
label="resources.rooms.fields.name"
source="id"
reference="rooms"
sortable={false}
link=""
>
<TextField source="name" sortable={false} />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
);
};
+17 -12
View File
@@ -1,15 +1,14 @@
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import { import {
Button, Button,
useDelete, useMutation,
useNotify, useNotify,
Confirm, Confirm,
useRecordContext,
useRefresh, useRefresh,
} from "react-admin"; } from "react-admin";
import ActionDelete from "@mui/icons-material/Delete"; import ActionDelete from "@material-ui/icons/Delete";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { alpha } from "@mui/material/styles"; import { fade } from "@material-ui/core/styles/colorManipulator";
import classnames from "classnames"; import classnames from "classnames";
const useStyles = makeStyles( const useStyles = makeStyles(
@@ -17,7 +16,7 @@ const useStyles = makeStyles(
deleteButton: { deleteButton: {
color: theme.palette.error.main, color: theme.palette.error.main,
"&:hover": { "&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12), backgroundColor: fade(theme.palette.error.main, 0.12),
// Reset on mouse devices // Reset on mouse devices
"@media (hover: none)": { "@media (hover: none)": {
backgroundColor: "transparent", backgroundColor: "transparent",
@@ -29,13 +28,13 @@ const useStyles = makeStyles(
); );
export const DeviceRemoveButton = props => { export const DeviceRemoveButton = props => {
const record = useRecordContext(); const { record } = props;
const classes = useStyles(props); const classes = useStyles(props);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const refresh = useRefresh(); const refresh = useRefresh();
const notify = useNotify(); const notify = useNotify();
const [removeDevice, { isLoading }] = useDelete("devices"); const [removeDevice, { loading }] = useMutation();
if (!record) return null; if (!record) return null;
@@ -44,15 +43,21 @@ export const DeviceRemoveButton = props => {
const handleConfirm = () => { const handleConfirm = () => {
removeDevice( removeDevice(
{ payload: { id: record.id, user_id: record.user_id } }, {
type: "delete",
resource: "devices",
payload: {
id: record.id,
user_id: record.user_id,
},
},
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.devices.action.erase.success"); notify("resources.devices.action.erase.success");
refresh(); refresh();
}, },
onFailure: () => { onFailure: () =>
notify("resources.devices.action.erase.failure", { type: "error" }); notify("resources.devices.action.erase.failure", "error"),
},
} }
); );
setOpen(false); setOpen(false);
@@ -69,7 +74,7 @@ export const DeviceRemoveButton = props => {
</Button> </Button>
<Confirm <Confirm
isOpen={open} isOpen={open}
loading={isLoading} loading={loading}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onClose={handleDialogClose} onClose={handleDialogClose}
title="resources.devices.action.erase.title" title="resources.devices.action.erase.title"
+9 -204
View File
@@ -1,6 +1,6 @@
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import classnames from "classnames"; import classnames from "classnames";
import { alpha } from "@mui/material/styles"; import { fade } from "@material-ui/core/styles/colorManipulator";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { import {
BooleanInput, BooleanInput,
@@ -10,33 +10,23 @@ import {
SaveButton, SaveButton,
SimpleForm, SimpleForm,
Toolbar, Toolbar,
useCreate,
useDelete, useDelete,
useNotify, useNotify,
useRecordContext,
useRefresh,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import BlockIcon from "@mui/icons-material/Block"; import IconCancel from "@material-ui/icons/Cancel";
import ClearIcon from "@mui/icons-material/Clear"; import Dialog from "@material-ui/core/Dialog";
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep"; import DialogContent from "@material-ui/core/DialogContent";
import { import DialogContentText from "@material-ui/core/DialogContentText";
Dialog, import DialogTitle from "@material-ui/core/DialogTitle";
DialogContent, import DeleteSweepIcon from "@material-ui/icons/DeleteSweep";
DialogContentText,
DialogTitle,
Tooltip,
} from "@mui/material";
import IconCancel from "@mui/icons-material/Cancel";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
deleteButton: { deleteButton: {
color: theme.palette.error.main, color: theme.palette.error.main,
"&:hover": { "&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12), backgroundColor: fade(theme.palette.error.main, 0.12),
// Reset on mouse devices // Reset on mouse devices
"@media (hover: none)": { "@media (hover: none)": {
backgroundColor: "transparent", backgroundColor: "transparent",
@@ -130,9 +120,7 @@ export const DeleteMediaButton = props => {
handleDialogClose(); handleDialogClose();
}, },
onFailure: () => onFailure: () =>
notify("resources.delete_media.action.send_failure", { notify("resources.delete_media.action.send_failure", "error"),
type: "error",
}),
} }
); );
}; };
@@ -155,186 +143,3 @@ export const DeleteMediaButton = props => {
</Fragment> </Fragment>
); );
}; };
export const ProtectMediaButton = props => {
const record = useRecordContext();
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", {
type: "error",
}),
}
);
};
const handleUnprotect = () => {
deleteOne(
{ payload: { ...record } },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.protect_media.action.send_failure", {
type: "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 = useRecordContext();
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", {
type: "error",
}),
}
);
};
const handleRemoveQuarantaine = () => {
deleteOne(
{ payload: { ...record } },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.quarantine_media.action.send_failure", {
type: "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>
);
};
+402 -90
View File
@@ -1,39 +1,60 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Route, Link } from "react-router-dom";
import { import {
AutocompleteArrayInput,
AutocompleteInput,
BooleanInput,
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
DateField, Button,
Create,
Edit,
Datagrid, Datagrid,
DateField,
DeleteButton, DeleteButton,
Filter, Filter,
FormTab,
List, List,
NumberField,
Pagination, Pagination,
ReferenceArrayInput,
ReferenceField, ReferenceField,
ReferenceInput,
ReferenceManyField, ReferenceManyField,
SearchInput, SearchInput,
SelectField, SelectField,
Show, Show,
SimpleForm,
Tab, Tab,
TabbedForm,
TabbedShowLayout, TabbedShowLayout,
TextField, TextField,
TextInput,
Toolbar,
TopToolbar, TopToolbar,
useRecordContext, useDataProvider,
useRefresh,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import get from "lodash/get"; import get from "lodash/get";
import PropTypes from "prop-types"; import {
import { makeStyles } from "@material-ui/core/styles"; Tooltip,
import { Tooltip, Typography, Chip } from "@mui/material"; Typography,
import FastForwardIcon from "@mui/icons-material/FastForward"; Chip,
import HttpsIcon from "@mui/icons-material/Https"; Drawer,
import NoEncryptionIcon from "@mui/icons-material/NoEncryption"; styled,
import PageviewIcon from "@mui/icons-material/Pageview"; withStyles,
import UserIcon from "@mui/icons-material/Group"; Select,
import ViewListIcon from "@mui/icons-material/ViewList"; MenuItem,
import VisibilityIcon from "@mui/icons-material/Visibility"; } from "@material-ui/core";
import EventIcon from "@mui/icons-material/Event"; import HttpsIcon from "@material-ui/icons/Https";
import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
import PageviewIcon from "@material-ui/icons/Pageview";
import UserIcon from "@material-ui/icons/Group";
import ViewListIcon from "@material-ui/icons/ViewList";
import VisibilityIcon from "@material-ui/icons/Visibility";
import ContentSave from "@material-ui/icons/Save";
import EventIcon from "@material-ui/icons/Event";
import { import {
RoomDirectoryBulkDeleteButton, RoomDirectoryBulkDeleteButton,
RoomDirectoryBulkSaveButton, RoomDirectoryBulkSaveButton,
@@ -41,22 +62,6 @@ import {
RoomDirectorySaveButton, RoomDirectorySaveButton,
} from "./RoomDirectory"; } 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]} />
); );
@@ -87,21 +92,368 @@ const EncryptionField = ({ source, record = {}, emptyText }) => {
); );
}; };
const RoomTitle = props => { const validateDisplayName = fieldval => {
const record = useRecordContext(); return fieldval == null
const translate = useTranslate(); ? "synapseadmin.rooms.room_name_required"
var name = ""; : fieldval.length === 0
if (record) { ? "synapseadmin.rooms.room_name_required"
name = record.name !== "" ? record.name : record.id; : undefined;
};
function approximateAliasLength(alias, homeserver) {
/* TODO maybe handle punycode in homeserver name */
var te;
// Support for TextEncoder is quite widespread, but the polyfill is
// pretty large; We will only underestimate the size with the regular
// length attribute of String, so we never prevent the user from using
// an alias that is short enough for the server, but too long for our
// heuristic.
try {
te = new TextEncoder();
} catch (err) {
if (err instanceof ReferenceError) {
te = undefined;
}
} }
const aliasLength = te === undefined ? alias.length : te.encode(alias).length;
return "#".length + aliasLength + ":".length + homeserver.length;
}
const validateAlias = fieldval => {
if (fieldval === undefined) {
return undefined;
}
const homeserver = localStorage.getItem("home_server");
if (approximateAliasLength(fieldval, homeserver) > 255) {
return "synapseadmin.rooms.alias_too_long";
}
};
const removeLeadingWhitespace = fieldVal =>
fieldVal === undefined ? undefined : fieldVal.trimStart();
const replaceAllWhitespace = fieldVal =>
fieldVal === undefined ? undefined : fieldVal.replace(/\s/, "_");
const removeLeadingSigil = fieldVal =>
fieldVal === undefined
? undefined
: fieldVal.startsWith("#")
? fieldVal.substr(1)
: fieldVal;
const validateHasAliasIfPublic = formdata => {
let errors = {};
if (formdata.public) {
if (
formdata.canonical_alias === undefined ||
formdata.canonical_alias.trim().length === 0
) {
errors.canonical_alias = "synapseadmin.rooms.alias_required_if_public";
}
}
return errors;
};
export const RoomCreate = props => (
<Create {...props}>
<TabbedForm validate={validateHasAliasIfPublic}>
<FormTab label="synapseadmin.rooms.details" icon={<ViewListIcon />}>
<TextInput
source="name"
parse={removeLeadingWhitespace}
validate={validateDisplayName}
/>
<TextInput
source="canonical_alias"
parse={fv => replaceAllWhitespace(removeLeadingSigil(fv))}
validate={validateAlias}
placeholder="#"
/>
<ReferenceInput
reference="users"
source="owner"
filterToQuery={searchText => ({ user_id: searchText })}
>
<AutocompleteInput
optionText="displayname"
suggestionText="displayname"
/>
</ReferenceInput>
<BooleanInput source="public" label="synapseadmin.rooms.make_public" />
<BooleanInput
source="encrypt"
initialValue={true}
label="synapseadmin.rooms.encrypt"
/>
</FormTab>
<FormTab
label="resources.rooms.fields.invite_members"
icon={<UserIcon />}
>
<ReferenceArrayInput
reference="users"
source="invitees"
filterToQuery={searchText => ({ user_id: searchText })}
>
<AutocompleteArrayInput
optionText="displayname"
suggestionText="displayname"
/>
</ReferenceArrayInput>
</FormTab>
</TabbedForm>
</Create>
);
const RoomTitle = ({ record }) => {
const translate = useTranslate();
return ( return (
<span> <span>
{translate("resources.rooms.name", 1)} {name} {translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
</span> </span>
); );
}; };
// Explicitely passing "to" prop
// Toolbar adds all kinds of unsupported props to its children :(
const StyledLink = styles => {
const Styled = styled(Link)(styles);
return ({ to, children }) => <Styled to={to}>{children}</Styled>;
};
const RoomMemberEditToolbar = ({ backLink, translate, onSave, ...props }) => {
const SaveLink = StyledLink({
textDecoration: "none",
});
const CancelLink = StyledLink({
textDecoration: "none",
marginLeft: "1em",
});
const SaveIcon = styled(ContentSave)({
width: "1rem",
marginRight: "0.25em",
});
return (
<Toolbar {...props}>
<SaveLink to={backLink}>
<Button onClick={onSave} variant="contained">
<React.Fragment>
<SaveIcon />
{translate("ra.action.save")}
</React.Fragment>
</Button>
</SaveLink>
<CancelLink to={backLink}>
<Button>
<React.Fragment>{translate("ra.action.cancel")}</React.Fragment>
</Button>
</CancelLink>
</Toolbar>
);
};
const RoomMemberIdField = ({ memberId, data = {} }) => {
const value = get(data[memberId], "id");
return (
<Typography component="span" variant="body2">
{value}
</Typography>
);
};
const RoomMemberRoleInput = ({ memberId, data = {}, translate, onChange }) => {
const roleValue = get(data[memberId], "role");
const [role, setRole] = React.useState(roleValue);
React.useEffect(() => {
onChange(roleValue);
}, [onChange, roleValue]);
return (
<React.Fragment>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={role}
onChange={event => {
setRole(event.target.value);
onChange(event.target.value);
}}
>
<MenuItem value={"user"}>
{translate("resources.users.roles.user")}
</MenuItem>
<MenuItem value={"mod"}>
{translate("resources.users.roles.mod")}
</MenuItem>
<MenuItem value={"admin"}>
{translate("resources.users.roles.admin")}
</MenuItem>
</Select>
</React.Fragment>
);
};
const RoomMemberEdit = ({ backLink, memberId, ...props }) => {
const translate = useTranslate();
const refresh = useRefresh();
const dataProvider = useDataProvider();
const [role, setRole] = React.useState();
const { id } = props;
return (
<Edit title=" " {...props}>
<SimpleForm
toolbar={
<RoomMemberEditToolbar
backLink={backLink}
translate={translate}
onSave={() => {
dataProvider
.update("rooms", {
data: {
id,
member_roles: [{ member_id: memberId, role }],
},
})
.then(() => {
refresh();
});
}}
/>
}
>
<ReferenceManyField
reference="room_members"
target="room_id"
label="resources.users.fields.id"
>
<RoomMemberIdField memberId={memberId} />
</ReferenceManyField>
<ReferenceManyField
reference="room_members"
target="room_id"
label="resources.users.fields.role"
>
<RoomMemberRoleInput
memberId={memberId}
translate={translate}
onChange={setRole}
/>
</ReferenceManyField>
</SimpleForm>
</Edit>
);
};
const drawerStyles = {
paper: {
width: 300,
},
};
const StyledDrawer = withStyles(drawerStyles)(({ classes, ...props }) => (
<Drawer {...props} classes={classes} />
));
export const RoomEdit = props => {
const translate = useTranslate();
return (
<React.Fragment>
<Edit {...props} title={<RoomTitle />}>
<TabbedForm>
<FormTab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />}>
<ReferenceArrayInput
reference="users"
source="invitees"
filterToQuery={searchText => ({ user_id: searchText })}
>
<AutocompleteArrayInput
optionText="displayname"
suggestionText="displayname"
/>
</ReferenceArrayInput>
<ReferenceManyField
reference="room_members"
target="room_id"
addLabel={false}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) =>
`/rooms/${encodeURIComponent(
record.parentId
)}/${encodeURIComponent(id)}`
}
>
<TextField
source="id"
sortable={false}
label="resources.users.fields.id"
/>
<ReferenceField
label="resources.users.fields.displayname"
source="id"
reference="users"
sortable={false}
link=""
>
<TextField source="displayname" sortable={false} />
</ReferenceField>
<SelectField
source="role"
label="resources.users.fields.role"
choices={[
{
id: "user",
name: translate("resources.users.roles.user"),
},
{ id: "mod", name: translate("resources.users.roles.mod") },
{
id: "admin",
name: translate("resources.users.roles.admin"),
},
]}
/>
</Datagrid>
</ReferenceManyField>
</FormTab>
</TabbedForm>
</Edit>
<Route path="/rooms/:roomId/:memberId">
{({ match }) => {
const isMatch = !!match && !!match.params;
return (
<StyledDrawer open={isMatch} anchor="right">
{isMatch ? (
<RoomMemberEdit
{...props}
memberId={
isMatch ? decodeURIComponent(match.params.memberId) : null
}
backLink={`/rooms/${match.params.roomId}`}
/>
) : (
<div />
)}
</StyledDrawer>
);
}}
</Route>
</React.Fragment>
);
};
const RoomShowActions = ({ basePath, data, resource }) => { const RoomShowActions = ({ basePath, data, resource }) => {
var roomDirectoryStatus = ""; var roomDirectoryStatus = "";
if (data) { if (data) {
@@ -129,7 +481,6 @@ const RoomShowActions = ({ basePath, data, resource }) => {
}; };
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 />}>
@@ -159,11 +510,7 @@ export const RoomShow = props => {
/> />
</Tab> </Tab>
<Tab <Tab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />}>
label="synapseadmin.rooms.tabs.members"
icon={<UserIcon />}
path="members"
>
<ReferenceManyField <ReferenceManyField
reference="room_members" reference="room_members"
target="room_id" target="room_id"
@@ -245,7 +592,6 @@ export const RoomShow = props => {
]} ]}
/> />
</Tab> </Tab>
<Tab <Tab
label={translate("resources.room_state.name", { smart_count: 2 })} label={translate("resources.room_state.name", { smart_count: 2 })}
icon={<EventIcon />} icon={<EventIcon />}
@@ -261,7 +607,14 @@ export const RoomShow = props => {
<DateField <DateField
source="origin_server_ts" source="origin_server_ts"
showTime showTime
options={date_format} options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false} sortable={false}
/> />
<TextField source="content" sortable={false} /> <TextField source="content" sortable={false} />
@@ -275,33 +628,6 @@ export const RoomShow = props => {
</Datagrid> </Datagrid>
</ReferenceManyField> </ReferenceManyField>
</Tab> </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>
); );
@@ -315,7 +641,7 @@ const RoomBulkActionButtons = props => (
{...props} {...props}
confirmTitle="resources.rooms.action.erase.title" confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content" confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic" undoable={false}
/> />
</Fragment> </Fragment>
); );
@@ -353,22 +679,8 @@ const RoomFilter = ({ ...props }) => {
); );
}; };
const RoomNameField = props => { const FilterableRoomList = ({ ...props }) => {
const { source } = props; const filter = props.roomFilters;
const record = useRecordContext();
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;
@@ -389,7 +701,7 @@ const FilterableRoomList = ({ roomFilters, dispatch, ...props }) => {
sortBy="encryption" sortBy="encryption"
label={<HttpsIcon />} label={<HttpsIcon />}
/> />
<RoomNameField source="name" /> <TextField 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" />}
+163 -154
View File
@@ -1,14 +1,13 @@
import React, { cloneElement, Fragment } from "react"; import React, { cloneElement, Fragment } from "react";
import Avatar from "@mui/material/Avatar"; import Avatar from "@material-ui/core/Avatar";
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd"; import PersonPinIcon from "@material-ui/icons/PersonPin";
import ContactMailIcon from "@mui/icons-material/ContactMail"; import ContactMailIcon from "@material-ui/icons/ContactMail";
import DevicesIcon from "@mui/icons-material/Devices"; import DevicesIcon from "@material-ui/icons/Devices";
import GetAppIcon from "@mui/icons-material/GetApp"; import GetAppIcon from "@material-ui/icons/GetApp";
import NotificationsIcon from "@mui/icons-material/Notifications"; import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import PermMediaIcon from "@mui/icons-material/PermMedia"; import NotificationsIcon from "@material-ui/icons/Notifications";
import PersonPinIcon from "@mui/icons-material/PersonPin"; import PermMediaIcon from "@material-ui/icons/PermMedia";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; import ViewListIcon from "@material-ui/icons/ViewList";
import ViewListIcon from "@mui/icons-material/ViewList";
import { import {
ArrayInput, ArrayInput,
ArrayField, ArrayField,
@@ -29,17 +28,16 @@ import {
PasswordInput, PasswordInput,
TextField, TextField,
TextInput, TextInput,
SearchInput,
ReferenceField, ReferenceField,
ReferenceManyField, ReferenceManyField,
SearchInput, SelectField,
SelectInput, SelectInput,
BulkDeleteButton, BulkDeleteButton,
DeleteButton, DeleteButton,
SaveButton, SaveButton,
maxLength,
regex, regex,
required, useRedirect,
useRecordContext,
useTranslate, useTranslate,
Pagination, Pagination,
CreateButton, CreateButton,
@@ -48,18 +46,11 @@ import {
sanitizeListRestProps, sanitizeListRestProps,
NumberField, NumberField,
} from "react-admin"; } from "react-admin";
import { Link } from "react-router-dom"; import SaveQrButton from "./SaveQrButton";
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 = () => {
return {
pathname: "/import_users",
};
};
const useStyles = makeStyles({ const useStyles = makeStyles({
small: { small: {
height: "40px", height: "40px",
@@ -72,25 +63,6 @@ const useStyles = makeStyles({
}, },
}); });
const choices_medium = [
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
];
const choices_type = [
{ id: "bot", name: "bot" },
{ id: "support", name: "support" },
];
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,
@@ -109,6 +81,7 @@ const UserListActions = ({
total, total,
...rest ...rest
}) => { }) => {
const redirectTo = useRedirect();
return ( return (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}> <TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters && {filters &&
@@ -128,10 +101,6 @@ const UserListActions = ({
exporter={exporter} exporter={exporter}
maxResults={maxResults} maxResults={maxResults}
/> />
{/* Add your custom actions */}
<Button component={Link} to={redirect} label="CSV Import">
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
</Button>
</TopToolbar> </TopToolbar>
); );
}; };
@@ -164,7 +133,7 @@ const UserBulkActionButtons = props => (
{...props} {...props}
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase" confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic" undoable={false}
/> />
</Fragment> </Fragment>
); );
@@ -180,7 +149,6 @@ 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 />}
@@ -188,36 +156,60 @@ 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="id" sortable={false} />
<TextField source="displayname" /> <TextField source="displayname" />
<BooleanField source="is_guest" /> <BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" /> <BooleanField source="admin" sortable={false} />
<BooleanField source="deactivated" /> <SelectField
<DateField source="user_type"
source="creation_ts" choices={[
label="resources.users.fields.creation_ts_ms" { id: null, name: "resources.users.type.default" },
showTime { id: "free", name: "resources.users.type.free" },
options={date_format} { id: "limited", name: "resources.users.type.limited" },
]}
/> />
<BooleanField source="deactivated" sortable={false} />
</Datagrid> </Datagrid>
</List> </List>
); );
}; };
// https://matrix.org/docs/spec/appendices#user-identifiers // redirect to the related Author show page
// here only local part of user_id const redirect = (basePath, id, data) => {
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length return {
// localStorage.getItem("home_server").length is not valid here pathname: "/showpdf",
const validateUser = [ state: {
required(), id: data.id,
maxLength(253), displayname: data.displayname,
regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id"), password: data.password,
]; },
};
};
const validateAddress = [required(), maxLength(255)]; const UserCreateToolbar = props => (
<Toolbar {...props}>
<SaveQrButton
label="synapseadmin.action.save_and_show"
redirect={redirect}
submitOnEnter={true}
/>
<SaveButton
label="synapseadmin.action.save_only"
redirect="list"
submitOnEnter={false}
variant="text"
/>
</Toolbar>
);
// https://matrix.org/docs/spec/appendices#user-identifiers
const validateUser = regex(
/^@[a-z0-9._=\-/]+:.*/,
"synapseadmin.users.invalid_user_id"
);
export function generateRandomUser() { export function generateRandomUser() {
const homeserver = localStorage.getItem("home_server"); const homeserver = localStorage.getItem("home_server");
@@ -260,78 +252,65 @@ export function generateRandomUser() {
}; };
} }
const UserEditToolbar = props => ( const UserEditToolbar = props => {
<Toolbar {...props}>
<SaveButton submitOnEnter={true} disabled={props.pristine} />
</Toolbar>
);
const UserEditActions = ({ data }) => {
const translate = useTranslate(); const translate = useTranslate();
var userStatus = "";
if (data) {
userStatus = data.deactivated;
}
return ( return (
<TopToolbar> <Toolbar {...props}>
{!userStatus && <ServerNoticeButton record={data} />} <SaveQrButton
label="synapseadmin.action.save_and_show"
redirect={redirect}
submitOnEnter={true}
/>
<SaveButton
label="synapseadmin.action.save_only"
redirect="list"
submitOnEnter={false}
variant="text"
/>
<DeleteButton <DeleteButton
record={data}
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", { confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1, smart_count: 1,
})} })}
mutationMode="pessimistic" mutationMode="pessimistic"
/> />
</TopToolbar> <ServerNoticeButton />
</Toolbar>
); );
}; };
export const UserCreate = props => ( export const UserCreate = props => (
<Create {...props}> <Create record={generateRandomUser()} {...props}>
<SimpleForm> <SimpleForm toolbar={<UserCreateToolbar />}>
<TextInput source="id" autoComplete="off" validate={validateUser} /> <TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" validate={maxLength(256)} /> <TextInput source="displayname" />
<PasswordInput <PasswordInput source="password" autoComplete="new-password" />
source="password" <BooleanInput source="admin" />
autoComplete="new-password"
validate={maxLength(512)}
/>
<SelectInput <SelectInput
source="user_type" source="user_type"
choices={choices_type} choices={[
translateChoice={false} { id: null, name: "resources.users.type.default" },
allowEmpty={true} { id: "free", name: "resources.users.type.free" },
resettable { id: "limited", name: "resources.users.type.limited" },
]}
/> />
<BooleanInput source="admin" />
<ArrayInput source="threepids"> <ArrayInput source="threepids">
<SimpleFormIterator disableReordering> <SimpleFormIterator>
<SelectInput <SelectInput
source="medium" source="medium"
choices={choices_medium} choices={[
validate={required()} { id: "email", name: "resources.users.email" },
/> { id: "msisdn", name: "resources.users.msisdn" },
<TextInput source="address" validate={validateAddress} /> ]}
</SimpleFormIterator>
</ArrayInput>
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
<SimpleFormIterator disableReordering>
<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>
</Create> </Create>
); );
const UserTitle = props => { const UserTitle = ({ record }) => {
const record = useRecordContext();
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<span> <span>
@@ -347,7 +326,7 @@ export const UserEdit = props => {
const classes = useStyles(); const classes = useStyles();
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}> <Edit {...props} title={<UserTitle />}>
<TabbedForm toolbar={<UserEditToolbar />}> <TabbedForm toolbar={<UserEditToolbar />}>
<FormTab <FormTab
label={translate("resources.users.name", { smart_count: 1 })} label={translate("resources.users.name", { smart_count: 1 })}
@@ -360,24 +339,33 @@ export const UserEdit = props => {
/> />
<TextInput source="id" disabled /> <TextInput source="id" disabled />
<TextInput source="displayname" /> <TextInput source="displayname" />
<PasswordInput <PasswordInput source="password" autoComplete="new-password" />
source="password"
autoComplete="new-password"
helperText="resources.users.helper.password"
/>
<SelectInput <SelectInput
source="user_type" source="user_type"
choices={choices_type} choices={[
translateChoice={false} { id: null, name: "resources.users.type.default" },
allowEmpty={true} { id: "free", name: "resources.users.type.free" },
resettable { id: "limited", name: "resources.users.type.limited" },
]}
emptyText="resources.users.type.default"
/> />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
<BooleanInput <BooleanInput
source="deactivated" source="deactivated"
helperText="resources.users.helper.deactivate" helperText="resources.users.helper.deactivate"
/> />
<DateField source="creation_ts_ms" showTime options={date_format} /> <DateField
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>
@@ -387,26 +375,15 @@ export const UserEdit = props => {
path="threepid" path="threepid"
> >
<ArrayInput source="threepids"> <ArrayInput source="threepids">
<SimpleFormIterator disableReordering> <SimpleFormIterator>
<SelectInput source="medium" choices={choices_medium} /> <SelectInput
<TextInput source="address" /> source="medium"
</SimpleFormIterator> choices={[
</ArrayInput> { id: "email", name: "resources.users.email" },
</FormTab> { id: "msisdn", name: "resources.users.msisdn" },
]}
<FormTab
label="synapseadmin.users.tabs.sso"
icon={<AssignmentIndIcon />}
path="sso"
>
<ArrayInput source="external_ids" label={false}>
<SimpleFormIterator disableReordering>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
validate={required()}
/> />
<TextInput source="address" />
</SimpleFormIterator> </SimpleFormIterator>
</ArrayInput> </ArrayInput>
</FormTab> </FormTab>
@@ -428,7 +405,14 @@ export const UserEdit = props => {
<DateField <DateField
source="last_seen_ts" source="last_seen_ts"
showTime showTime
options={date_format} options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false} sortable={false}
/> />
<DeviceRemoveButton /> <DeviceRemoveButton />
@@ -456,7 +440,14 @@ export const UserEdit = props => {
<DateField <DateField
source="last_seen" source="last_seen"
showTime showTime
options={date_format} options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false} sortable={false}
/> />
<TextField <TextField
@@ -480,22 +471,40 @@ 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 source="created_ts" showTime options={date_format} /> <DateField
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={date_format} options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/> />
<TextField source="media_id" /> <TextField source="media_id" sortable={false} />
<NumberField source="media_length" /> <NumberField source="media_length" sortable={false} />
<TextField source="media_type" /> <TextField source="media_type" sortable={false} />
<TextField source="upload_name" /> <TextField source="upload_name" sortable={false} />
<TextField source="quarantined_by" /> <TextField source="quarantined_by" sortable={false} />
<QuarantineMediaButton label="resources.quarantine_media.action.name" /> <BooleanField source="safe_from_quarantine" sortable={false} />
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
<DeleteButton mutationMode="pessimistic" redirect={false} /> <DeleteButton mutationMode="pessimistic" redirect={false} />
</Datagrid> </Datagrid>
</ReferenceManyField> </ReferenceManyField>
+42 -74
View File
@@ -1,6 +1,6 @@
import germanMessages from "ra-language-german"; import germanMessages from "ra-language-german";
const de = { export default {
...germanMessages, ...germanMessages,
synapseadmin: { synapseadmin: {
auth: { auth: {
@@ -10,14 +10,26 @@ const de = {
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", },
action: {
save_and_show: "Speichern und QR Code erzeugen",
save_only: "Nur speichern",
download_pdf: "PDF speichern",
}, },
users: { users: {
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.", invalid_user_id:
tabs: { sso: "SSO" }, "Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver",
}, },
rooms: { rooms: {
details: "Raumdetails", details: "Raumdetails",
room_name: "Raumname",
make_public: "Öffentlicher Raum",
encrypt: "Verschlüsselter Raum",
room_name_required: "Muss angegeben werden",
alias_required_if_public: "Muss für öffentliche Räume angegeben werden.",
alias: "Alias",
alias_too_long:
"Darf zusammen mit der Domain des Homeservers 255 bytes nicht überschreiten",
tabs: { tabs: {
basic: "Allgemein", basic: "Allgemein",
members: "Mitglieder", members: "Mitglieder",
@@ -97,6 +109,7 @@ const de = {
}, },
resources: { resources: {
users: { users: {
backtolist: "Zurück zur Liste",
name: "Benutzer", name: "Benutzer",
email: "E-Mail", email: "E-Mail",
msisdn: "Telefon", msisdn: "Telefon",
@@ -120,12 +133,20 @@ const de = {
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", user_type: "Kontotyp",
user_type: "Benutzertyp", // Devices:
device_id: "Geräte-ID",
display_name: "Gerätename",
last_seen_ts: "Zeitstempel",
last_seen_ip: "IP-Adresse",
role: "Rolle",
},
type: {
default: "Standard",
free: "Basic",
limited: "Eingeschränkt",
}, },
helper: { helper: {
password:
"Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
deactivate: deactivate:
"Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.", "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
erase: "DSGVO konformes Löschen der Benutzerdaten", erase: "DSGVO konformes Löschen der Benutzerdaten",
@@ -133,6 +154,11 @@ const de = {
action: { action: {
erase: "Lösche Benutzerdaten", erase: "Lösche Benutzerdaten",
}, },
roles: {
user: "Nutzer",
mod: "Moderator",
admin: "Administrator",
},
}, },
rooms: { rooms: {
name: "Raum |||| Räume", name: "Raum |||| Räume",
@@ -141,6 +167,8 @@ const de = {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Mitglieder", joined_members: "Mitglieder",
invite_members: "Mitglieder einladen",
invitees: "Einladungen",
joined_local_members: "Lokale Mitglieder", joined_local_members: "Lokale Mitglieder",
joined_local_devices: "Lokale Endgeräte", joined_local_devices: "Lokale Endgeräte",
state_events: "Zustandsereignisse / Komplexität", state_events: "Zustandsereignisse / Komplexität",
@@ -156,10 +184,6 @@ const de = {
topic: "Thema", topic: "Thema",
avatar: "Avatar", 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: {
public: "Öffentlich", public: "Öffentlich",
@@ -244,7 +268,7 @@ const de = {
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: "Schutz vor Quarantäne", safe_from_quarantine: "Geschützt vor Quarantäne",
created_ts: "Erstellt", created_ts: "Erstellt",
last_access_ts: "Letzter Zugriff", last_access_ts: "Letzter Zugriff",
}, },
@@ -262,26 +286,8 @@ const de = {
send_failure: "Beim Versenden ist ein Fehler aufgetreten.", send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
}, },
helper: { helper: {
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.", 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.",
},
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: {
@@ -310,7 +316,8 @@ const de = {
send_failure: "Beim Versenden ist ein Fehler aufgetreten.", send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
}, },
helper: { helper: {
send: 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.', send:
'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: {
@@ -320,15 +327,6 @@ const de = {
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: { room_state: {
name: "Zustandsereignisse", name: "Zustandsereignisse",
fields: { fields: {
@@ -355,31 +353,6 @@ const de = {
send_failure: "Beim Entfernen ist ein Fehler aufgetreten.", send_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
}, },
}, },
destinations: {
name: "Föderation",
fields: {
destination: "Ziel",
failure_ts: "Fehlerzeitpunkt",
retry_last_ts: "Letzter Wiederholungsversuch",
retry_interval: "Wiederholungsintervall",
last_successful_stream_ordering: "letzte erfogreicher Stream",
stream_ordering: "Stream",
},
action: { reconnect: "Neu verbinden" },
},
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,
@@ -400,7 +373,7 @@ const de = {
}, },
}, },
notification: { notification: {
...germanMessages.ra.notification, ...germanMessages.ra.notifiaction,
logged_out: "Abgemeldet", logged_out: "Abgemeldet",
}, },
page: { page: {
@@ -408,10 +381,5 @@ const de = {
empty: "Keine Einträge vorhanden", empty: "Keine Einträge vorhanden",
invite: "", invite: "",
}, },
navigation: {
...germanMessages.ra.navigation,
skip_nav: "Zum Inhalt springen",
},
}, },
}; };
export default de;
+47 -76
View File
@@ -1,6 +1,6 @@
import englishMessages from "ra-language-english"; import englishMessages from "ra-language-english";
const en = { export default {
...englishMessages, ...englishMessages,
synapseadmin: { synapseadmin: {
auth: { auth: {
@@ -10,13 +10,26 @@ const en = {
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", },
action: {
save_and_show: "Create QR code",
save_only: "Save",
download_pdf: "Download PDF",
}, },
users: { users: {
invalid_user_id: "Localpart of a Matrix user-id without homeserver.", invalid_user_id:
tabs: { sso: "SSO" }, "Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver",
}, },
rooms: { rooms: {
details: "Room Details",
room_name: "Room Name",
make_public: "Make room public",
encrypt: "Encrypt room",
room_name_required: "Must be provided",
alias_required_if_public: "Must be provided for a public room",
alias: "Alias",
alias_too_long:
"Must not exceed 255 bytes including the domain of the homeserver.",
tabs: { tabs: {
basic: "Basic", basic: "Basic",
members: "Members", members: "Members",
@@ -96,6 +109,7 @@ const en = {
}, },
resources: { resources: {
users: { users: {
backtolist: "Back to list",
name: "User |||| Users", name: "User |||| Users",
email: "Email", email: "Email",
msisdn: "Phone", msisdn: "Phone",
@@ -119,17 +133,30 @@ const en = {
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", // Devices:
user_type: "User type", device_id: "Device-ID",
display_name: "Device name",
last_seen_ts: "Timestamp",
last_seen_ip: "IP address",
role: "Role",
},
type: {
default: "Standard",
free: "Basic",
limited: "Limited",
}, },
helper: { helper: {
password: "Changing password will log user out of all sessions.",
deactivate: "You must provide a password to re-activate an account.", deactivate: "You must provide a password to re-activate an account.",
erase: "Mark the user as GDPR-erased", erase: "Mark the user as GDPR-erased",
}, },
action: { action: {
erase: "Erase user data", erase: "Erase user data",
}, },
roles: {
user: "User",
mod: "Moderator",
admin: "Administrator",
},
}, },
rooms: { rooms: {
name: "Room |||| Rooms", name: "Room |||| Rooms",
@@ -138,6 +165,8 @@ const en = {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Members", joined_members: "Members",
invite_members: "Invite Members",
invitees: "Invitations",
joined_local_members: "Local members", joined_local_members: "Local members",
joined_local_devices: "Local devices", joined_local_devices: "Local devices",
state_events: "State events / Complexity", state_events: "State events / Complexity",
@@ -153,10 +182,6 @@ const en = {
topic: "Topic", topic: "Topic",
avatar: "Avatar", 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: {
public: "Public", public: "Public",
@@ -176,12 +201,10 @@ const en = {
}, },
unencrypted: "Unencrypted", unencrypted: "Unencrypted",
}, },
action: { erase: {
erase: { title: "Delete room",
title: "Delete room", content:
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!",
"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: {
@@ -197,7 +220,7 @@ const en = {
event_json: { event_json: {
origin: "origin server", origin: "origin server",
origin_server_ts: "time of send", origin_server_ts: "time of send",
type: "event type", type: "event typ",
content: { content: {
msgtype: "content type", msgtype: "content type",
body: "content", body: "content",
@@ -237,7 +260,7 @@ const en = {
name: "Media", name: "Media",
fields: { fields: {
media_id: "Media ID", media_id: "Media ID",
media_length: "File Size (in Bytes)", media_length: "Lenght",
media_type: "Type", media_type: "Type",
upload_name: "File name", upload_name: "File name",
quarantined_by: "Quarantined by", quarantined_by: "Quarantined by",
@@ -259,26 +282,8 @@ const en = {
send_failure: "An error has occurred.", send_failure: "An error has occurred.",
}, },
helper: { helper: {
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.", 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.",
},
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: {
@@ -307,7 +312,8 @@ const en = {
send_failure: "An error has occurred.", send_failure: "An error has occurred.",
}, },
helper: { helper: {
send: 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.', send:
'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: {
@@ -317,15 +323,6 @@ const en = {
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: { room_state: {
name: "State events", name: "State events",
fields: { fields: {
@@ -345,38 +342,12 @@ const en = {
title: title:
"Delete room from directory |||| Delete %{smart_count} rooms from directory", "Delete room from directory |||| Delete %{smart_count} rooms from directory",
content: 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?", "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", erase: "Delete from room directory",
create: "Publish in room directory", create: "Publish in room directory",
send_success: "Room successfully published.", send_success: "Room successfully published.",
send_failure: "An error has occurred.", send_failure: "An error has occurred.",
}, },
}, },
destinations: {
name: "Federation",
fields: {
destination: "Destination",
failure_ts: "Failure timestamp",
retry_last_ts: "Last retry timestamp",
retry_interval: "Retry interval",
last_successful_stream_ordering: "Last successful stream",
stream_ordering: "Stream",
},
action: { reconnect: "Reconnect" },
},
},
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;
-377
View File
@@ -1,377 +0,0 @@
import frenchMessages from "ra-language-french";
const fr = {
...frenchMessages,
synapseadmin: {
auth: {
base_url: "URL du serveur daccueil",
welcome: "Bienvenue sur Synapse-admin",
server_version: "Version du serveur Synapse",
username_error:
"Veuillez entrer un nom d'utilisateur complet : « @utilisateur:domaine »",
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
url_error: "L'URL du serveur Matrix n'est pas valide",
sso_sign_in: "Se connecter avec lauthentification unique",
},
users: {
invalid_user_id:
"Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.",
tabs: { sso: "Authentification unique" },
},
rooms: {
tabs: {
basic: "Informations de base",
members: "Membres",
detail: "Détails",
permission: "Permissions",
},
},
reports: { tabs: { basic: "Informations de base", detail: "Détails" } },
},
import_users: {
error: {
at_entry: "Pour l'entrée %{entry} : %{message}",
error: "Erreur",
required_field: "Le champ requis « %{field} » est manquant",
invalid_value:
"Valeur non valide à la ligne %{row}. Le champ « %{field} » ne peut être que « true » ou « false »",
unreasonably_big:
"Refus de charger un fichier trop volumineux de %{size} mégaoctets",
already_in_progress: "Un import est déjà en cours",
id_exits: "L'identifiant %{id} déjà présent",
},
title: "Importer des utilisateurs à partir d'un fichier CSV",
goToPdf: "Voir le PDF",
cards: {
importstats: {
header: "Importer des utilisateurs",
users_total:
"%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs",
admin_count:
"%{smart_count} administrateur |||| %{smart_count} administrateurs",
},
conflicts: {
header: "Stratégie de résolution des conflits",
mode: {
stop: "S'arrêter en cas de conflit",
skip: "Afficher l'erreur et ignorer le conflit",
},
},
ids: {
header: "Identifiants",
all_ids_present: "Identifiants présents pour chaque entrée",
count_ids_present:
"%{smart_count} entrée avec identifiant |||| %{smart_count} entrées avec identifiant",
mode: {
ignore:
"Ignorer les identifiants dans le ficher CSV et en créer de nouveaux",
update: "Mettre à jour les enregistrements existants",
},
},
passwords: {
header: "Mots de passe",
all_passwords_present: "Mots de passe présents pour chaque entrée",
count_passwords_present:
"%{smart_count} entrée avec mot de passe |||| %{smart_count} entrées avec mot de passe",
use_passwords: "Utiliser les mots de passe provenant du fichier CSV",
},
upload: {
header: "Fichier CSV en entrée",
explanation:
"Vous pouvez télécharger ici un fichier contenant des valeurs séparées par des virgules qui sera traité pour créer ou mettre à jour des utilisateurs. Le fichier doit inclure les champs « id » et « displayname ». Vous pouvez télécharger et adapter un fichier d'exemple ici : ",
},
startImport: {
simulate_only: "Simuler",
run_import: "Importer",
},
results: {
header: "Résultats de l'import",
total:
"%{smart_count} entrée au total |||| %{smart_count} entrées au total",
successful: "%{smart_count} entrées importées avec succès",
skipped: "%{smart_count} entrées ignorées",
download_skipped: "Télécharger les entrées ignorées",
with_error:
"%{smart_count} entrée avec des erreurs ||| %{smart_count} entrées avec des erreurs",
simulated_only: "L'import était simulé",
},
},
},
resources: {
users: {
name: "Utilisateur |||| Utilisateurs",
email: "Adresse électronique",
msisdn: "Numéro de téléphone",
threepid: "Adresse électronique / Numéro de téléphone",
fields: {
avatar: "Avatar",
id: "Identifiant",
name: "Nom",
is_guest: "Visiteur",
admin: "Administrateur du serveur",
deactivated: "Désactivé",
guests: "Afficher les visiteurs",
show_deactivated: "Afficher les utilisateurs désactivés",
user_id: "Rechercher un utilisateur",
displayname: "Nom d'affichage",
password: "Mot de passe",
avatar_url: "URL de l'avatar",
avatar_src: "Avatar",
medium: "Type",
threepids: "Identifiants tiers",
address: "Adresse",
creation_ts_ms: "Date de création",
consent_version: "Version du consentement",
auth_provider: "Fournisseur d'identité",
},
helper: {
deactivate:
"Vous devrez fournir un mot de passe pour réactiver le compte.",
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
},
action: {
erase: "Effacer les données de l'utilisateur",
},
},
rooms: {
name: "Salon |||| Salons",
fields: {
room_id: "Identifiant du salon",
name: "Nom",
canonical_alias: "Alias",
joined_members: "Membres",
joined_local_members: "Membres locaux",
joined_local_devices: "Appareils locaux",
state_events: "Événements d'État / Complexité",
version: "Version",
is_encrypted: "Chiffré",
encryption: "Chiffrement",
federatable: "Fédérable",
public: "Visible dans le répertoire des salons",
creator: "Créateur",
join_rules: "Règles d'adhésion",
guest_access: "Accès des visiteurs",
history_visibility: "Visibilité de l'historique",
topic: "Sujet",
avatar: "Avatar",
},
helper: {
forward_extremities:
"Les extrémités avant sont les événements feuilles à la fin d'un graphe orienté acyclique (DAG) dans un salon, c'est-à-dire les événements qui n'ont pas de descendants. Plus il y en a dans un salon, plus la résolution d'état que Synapse doit effectuer est importante (indice : c'est une opération coûteuse). Bien que Synapse dispose d'un algorithme pour éviter qu'un trop grand nombre de ces événements n'existent en même temps dans un salon, des bogues peuvent parfois les faire réapparaître. Si un salon présente plus de 10 extrémités avant, cela vaut la peine d'y prêter attention et éventuellement de les supprimer en utilisant les requêtes SQL mentionnées dans la discussion traitant du problème https://github.com/matrix-org/synapse/issues/1760.",
},
enums: {
join_rules: {
public: "Public",
knock: "Sur demande",
invite: "Sur invitation",
private: "Privé",
},
guest_access: {
can_join: "Les visiteurs peuvent rejoindre le salon",
forbidden: "Les visiteurs ne peuvent pas rejoindre le salon",
},
history_visibility: {
invited: "Depuis l'invitation",
joined: "Depuis l'adhésion",
shared: "Depuis le partage",
world_readable: "Tout le monde",
},
unencrypted: "Non chiffré",
},
action: {
erase: {
title: "Supprimer le salon",
content:
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
},
},
},
reports: {
name: "Événement signalé |||| Événements signalés",
fields: {
id: "Identifiant",
received_ts: "Date du rapport",
user_id: "Rapporteur",
name: "Nom du salon",
score: "Score",
reason: "Motif",
event_id: "Identifiant de l'événement",
event_json: {
origin: "Serveur d'origine",
origin_server_ts: "Date d'envoi",
type: "Type d'événement",
content: {
msgtype: "Type de contenu",
body: "Contenu",
format: "Format",
formatted_body: "Contenu mis en forme",
algorithm: "Algorithme",
},
},
},
},
connections: {
name: "Connexions",
fields: {
last_seen: "Date",
ip: "Adresse IP",
user_agent: "Agent utilisateur",
},
},
devices: {
name: "Appareil |||| Appareils",
fields: {
device_id: "Identifiant de l'appareil",
display_name: "Nom de l'appareil",
last_seen_ts: "Date",
last_seen_ip: "Adresse IP",
},
action: {
erase: {
title: "Suppression de %{id}",
content: "Voulez-vous vraiment supprimer l'appareil « %{name} » ?",
success: "Appareil supprimé avec succès",
failure: "Une erreur s'est produite",
},
},
},
users_media: {
name: "Media",
fields: {
media_id: "Identifiant du média",
media_length: "Taille du fichier (en octets)",
media_type: "Type",
upload_name: "Nom du fichier",
quarantined_by: "Mis en quarantaine par",
safe_from_quarantine: "Protection contre la mise en quarantaine",
created_ts: "Date de création",
last_access_ts: "Dernier accès",
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "Dernier accès avant",
size_gt: "Plus grand que (en octets)",
keep_profiles: "Conserver les images de profil",
},
action: {
send: "Supprimer le média",
send_success: "Requête envoyée avec succès",
send_failure: "Une erreur s'est produite",
},
helper: {
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
},
},
protect_media: {
action: {
create: "Protéger",
delete: "Révoquer la protection",
none: "En quarantaine",
send_success: "Le statut de protection a été modifié avec succès",
send_failure: "Une erreur s'est produite",
},
},
quarantine_media: {
action: {
name: "Quarantaine",
create: "Mettre en quarantaine",
delete: "Révoquer la mise en quarantaine",
none: "Protégé contre la mise en quarantaine",
send_success: "Le statut de la quarantaine a été modifié avec succès",
send_failure: "Une erreur s'est produite",
},
},
pushers: {
name: "Émetteur de notifications |||| Émetteurs de notifications",
fields: {
app: "Application",
app_display_name: "Nom d'affichage de l'application",
app_id: "Identifiant de l'application",
device_display_name: "Nom d'affichage de l'appareil",
kind: "Type",
lang: "Langue",
profile_tag: "Profil",
pushkey: "Identifiant de l'émetteur",
data: { url: "URL" },
},
},
servernotices: {
name: "Annonces du serveur",
send: "Envoyer des « Annonces du serveur »",
fields: {
body: "Message",
},
action: {
send: "Envoyer une annonce",
send_success: "Annonce envoyée avec succès",
send_failure: "Une erreur s'est produite",
},
helper: {
send: "Envoie une annonce au nom du serveur aux utilisateurs sélectionnés. La fonction « Annonces du serveur » doit être activée sur le serveur.",
},
},
user_media_statistics: {
name: "Médias des utilisateurs",
fields: {
media_count: "Nombre de médias",
media_length: "Taille des médias",
},
},
forward_extremities: {
name: "Extrémités avant",
fields: {
id: "Identifiant de l'événement",
received_ts: "Date de réception",
depth: "Profondeur",
state_group: "Groupe d'état",
},
},
room_state: {
name: "Événements d'état",
fields: {
type: "Type",
content: "Contenu",
origin_server_ts: "Date d'envoi",
sender: "Expéditeur",
},
},
room_directory: {
name: "Répertoire des salons",
fields: {
world_readable:
"Tout utilisateur peut avoir un aperçu du salon, sans en devenir membre",
guest_can_join: "Les visiteurs peuvent rejoindre le salon",
},
action: {
title:
"Supprimer un salon du répertoire |||| Supprimer %{smart_count} salons du répertoire",
content:
"Voulez-vous vraiment supprimer ce salon du répertoire ? |||| Voulez-vous vraiment supprimer ces %{smart_count} salons du répertoire ?",
erase: "Supprimer du répertoire des salons",
create: "Publier dans le répertoire des salons",
send_success: "Salon publié avec succès",
send_failure: "Une erreur s'est produite",
},
},
},
registration_tokens: {
name: "Jetons d'inscription",
fields: {
token: "Jeton",
valid: "Jeton valide",
uses_allowed: "Nombre d'inscription autorisées",
pending: "Nombre d'inscription en cours",
completed: "Nombre d'inscription accomplie",
expiry_time: "Date d'expiration",
length: "Longueur",
},
helper: {
length:
"Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
},
},
};
export default fr;
+6 -6
View File
@@ -1,6 +1,6 @@
import chineseMessages from "ra-language-chinese"; import chineseMessages from "ra-language-chinese";
const zh = { export default {
...chineseMessages, ...chineseMessages,
synapseadmin: { synapseadmin: {
auth: { auth: {
@@ -10,12 +10,10 @@ const zh = {
username_error: "请输入完整有效的用户 ID: '@user:domain'", username_error: "请输入完整有效的用户 ID: '@user:domain'",
protocol_error: "URL 需要以'http://'或'https://'作为起始", protocol_error: "URL 需要以'http://'或'https://'作为起始",
url_error: "不是一个有效的 Matrix 服务器地址", url_error: "不是一个有效的 Matrix 服务器地址",
sso_sign_in: "使用 SSO 登录",
}, },
users: { users: {
invalid_user_id: invalid_user_id:
"必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver", "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
tabs: { sso: "SSO" },
}, },
rooms: { rooms: {
tabs: { tabs: {
@@ -100,6 +98,7 @@ const zh = {
}, },
resources: { resources: {
users: { users: {
backtolist: "回到列表",
name: "用户", name: "用户",
email: "邮箱", email: "邮箱",
msisdn: "电话", msisdn: "电话",
@@ -246,7 +245,8 @@ const zh = {
send_failure: "出现了一个错误。", send_failure: "出现了一个错误。",
}, },
helper: { helper: {
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。", send:
"这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
}, },
}, },
pushers: { pushers: {
@@ -275,7 +275,8 @@ const zh = {
send_failure: "出现了一个错误。", send_failure: "出现了一个错误。",
}, },
helper: { helper: {
send: '向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。', send:
'向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
}, },
}, },
user_media_statistics: { user_media_statistics: {
@@ -287,4 +288,3 @@ const zh = {
}, },
}, },
}; };
export default zh;
+3
View File
@@ -1,3 +1,6 @@
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();
+8 -23
View File
@@ -2,37 +2,21 @@ 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, loginToken }) => { login: ({ base_url, username, password }) => {
// 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({
Object.assign( type: "m.login.password",
{ user: username,
device_id: localStorage.getItem("device_id"), password: password,
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);
@@ -64,6 +48,7 @@ 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();
+70 -141
View File
@@ -25,6 +25,16 @@ const mxcUrlToHttp = mxcUrl => {
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`; return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
}; };
const powerLevelToRole = powerLevel =>
powerLevel < 100 ? (powerLevel < 50 ? "user" : "mod") : "admin";
const POWER_LEVELS = {
admin: 100,
mod: 50,
user: 0,
};
const roleToPowerLevel = role => POWER_LEVELS[role] || 0;
const resourceMap = { const resourceMap = {
users: { users: {
path: "/_synapse/admin/v2/users", path: "/_synapse/admin/v2/users",
@@ -35,22 +45,19 @@ const resourceMap = {
is_guest: !!u.is_guest, is_guest: !!u.is_guest,
admin: !!u.admin, admin: !!u.admin,
deactivated: !!u.deactivated, deactivated: !!u.deactivated,
displayname: u.display_name || u.displayname,
// need timestamp in milliseconds // need timestamp in milliseconds
creation_ts_ms: u.creation_ts * 1000, creation_ts_ms: u.creation_ts * 1000,
}), }),
data: "users", data: "users",
total: json => json.total, total: json => json.total,
create: data => ({ create: data => ({
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent( endpoint: `/_synapse/admin/v2/users/${data.id}`,
data.id
)}:${localStorage.getItem("home_server")}`,
body: data, body: data,
method: "PUT", method: "PUT",
}), }),
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent( endpoint: `/_synapse/admin/v1/deactivate/${params.id}`,
params.id
)}`,
body: { erase: true }, body: { erase: true },
method: "POST", method: "POST",
}), }),
@@ -67,12 +74,45 @@ const resourceMap = {
public: !!r.public, public: !!r.public,
}), }),
data: "rooms", data: "rooms",
total: json => { total: json => json.total_rooms,
return json.total_rooms; create: data => ({
endpoint: "/_synapse/admin/v1/rooms",
body: {
owner: data.owner,
name: data.name,
room_alias_name: data.canonical_alias,
visibility: data.public ? "public" : "private",
invite:
Array.isArray(data.invitees) && data.invitees.length > 0
? data.invitees
: undefined,
initial_state: data.encrypt
? [
{
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
},
]
: undefined,
},
method: "POST",
}),
transformBeforeUpdate: data => {
return {
...data,
member_roles: (data.member_roles || []).map(member => ({
member_id: member.member_id,
power_level: roleToPowerLevel(member.role),
})),
};
}, },
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v2/rooms/${params.id}`, endpoint: `/_synapse/admin/v1/rooms/${params.id}/delete`,
body: { block: false }, body: { block: false },
method: "POST",
}), }),
}, },
reports: { reports: {
@@ -94,12 +134,10 @@ const resourceMap = {
return json.total; return json.total;
}, },
reference: id => ({ reference: id => ({
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`, endpoint: `/_synapse/admin/v2/users/${id}/devices`,
}), }),
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent( endpoint: `/_synapse/admin/v2/users/${params.user_id}/devices/${params.id}`,
params.user_id
)}/devices/${params.id}`,
}), }),
}, },
connections: { connections: {
@@ -141,7 +179,7 @@ const resourceMap = {
id: p.pushkey, id: p.pushkey,
}), }),
reference: id => ({ reference: id => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`, endpoint: `/_synapse/admin/v1/users/${id}/pushers`,
}), }),
data: "pushers", data: "pushers",
total: json => { total: json => {
@@ -153,9 +191,7 @@ const resourceMap = {
id: jr, id: jr,
}), }),
reference: id => ({ reference: id => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent( endpoint: `/_synapse/admin/v1/users/${id}/joined_rooms`,
id
)}/joined_rooms`,
}), }),
data: "joined_rooms", data: "joined_rooms",
total: json => { total: json => {
@@ -168,7 +204,7 @@ const resourceMap = {
id: um.media_id, id: um.media_id,
}), }),
reference: id => ({ reference: id => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`, endpoint: `/_synapse/admin/v1/users/${id}/media`,
}), }),
data: "media", data: "media",
total: json => { total: json => {
@@ -190,32 +226,6 @@ 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 => ({
@@ -241,22 +251,6 @@ 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: { room_directory: {
path: "/_matrix/client/r0/publicRooms", path: "/_matrix/client/r0/publicRooms",
map: rd => ({ map: rd => ({
@@ -281,59 +275,11 @@ const resourceMap = {
method: "PUT", method: "PUT",
}), }),
}, },
destinations: {
path: "/_synapse/admin/v1/federation/destinations",
map: dst => ({
...dst,
id: dst.destination,
}),
data: "destinations",
total: json => {
return json.total;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
method: "POST",
}),
},
destination_rooms: {
map: dstroom => ({
...dstroom,
id: dstroom.room_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
}),
data: "rooms",
total: json => {
return json.total;
},
},
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) {
// Filtering out null properties // Filtering out null properties
// to reset user_type from user, it must be null if (value === null) {
if (value === null && key !== "user_type") {
return undefined; return undefined;
} }
return value; return value;
@@ -350,15 +296,7 @@ function getSearchOrder(order) {
const dataProvider = { const dataProvider = {
getList: (resource, params) => { getList: (resource, params) => {
console.log("getList " + resource); console.log("getList " + resource);
const { const { user_id, name, guests, deactivated, search_term } = params.filter;
user_id,
name,
guests,
deactivated,
search_term,
destination,
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;
@@ -368,10 +306,8 @@ const dataProvider = {
user_id: user_id, user_id: user_id,
search_term: search_term, search_term: search_term,
name: name, name: name,
destination: destination,
guests: guests, guests: guests,
deactivated: deactivated, deactivated: deactivated,
valid: valid,
order_by: field, order_by: field,
dir: getSearchOrder(order), dir: getSearchOrder(order),
}; };
@@ -390,18 +326,16 @@ const dataProvider = {
}, },
getOne: (resource, params) => { getOne: (resource, params) => {
console.log("getOne " + resource); console.log("getOne " + resource, params);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then( return jsonClient(`${endpoint_url}/${params.id}`).then(({ json }) => ({
({ json }) => ({ data: res.map(json),
data: res.map(json), }));
})
);
}, },
getMany: (resource, params) => { getMany: (resource, params) => {
@@ -413,9 +347,7 @@ const dataProvider = {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return Promise.all( return Promise.all(
params.ids.map(id => params.ids.map(id => jsonClient(`${endpoint_url}/${id}`))
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
)
).then(responses => ({ ).then(responses => ({
data: responses.map(({ json }) => res.map(json)), data: responses.map(({ json }) => res.map(json)),
total: responses.length, total: responses.length,
@@ -425,13 +357,10 @@ 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");
@@ -455,10 +384,13 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const transform = res.transformBeforeUpdate || (x => x);
const data = transform(params.data);
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, { return jsonClient(`${endpoint_url}/${params.data.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(data, filterNullValues),
}).then(({ json }) => ({ }).then(({ json }) => ({
data: res.map(json), data: res.map(json),
})); }));
@@ -473,13 +405,10 @@ const dataProvider = {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return Promise.all( return Promise.all(
params.ids.map( params.ids.map(id => jsonClient(`${endpoint_url}/${id}`), {
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`), method: "PUT",
{ body: JSON.stringify(params.data, filterNullValues),
method: "PUT", })
body: JSON.stringify(params.data, filterNullValues),
}
)
).then(responses => ({ ).then(responses => ({
data: responses.map(({ json }) => json), data: responses.map(({ json }) => json),
})); }));
+7909 -7278
View File
File diff suppressed because it is too large Load Diff