Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd71a08fe9 | |||
| 137a5828df | |||
| c6ed88176d | |||
| b3a611e7ad | |||
| de03e23524 | |||
| a200a8932b | |||
| 41656748bb | |||
| 42a4decc2a | |||
| 74f77e6988 | |||
| 8501f19a03 | |||
| 6d30af9976 | |||
| 2e59190bd0 | |||
| a647c91f4f | |||
| 21ba5c9862 | |||
| 0a701df5d2 | |||
| 347d1c3114 | |||
| 8982487eee | |||
| 7fa835f973 | |||
| 5767817733 | |||
| e43f732bde | |||
| c0f4a92f1a | |||
| e70c680e8a | |||
| 3b4da1d3c2 | |||
| 3b2aa776eb | |||
| ee8050f697 | |||
| ca5fde9190 | |||
| 561daf7737 | |||
| f2526dc00e | |||
| 3cce1a61d3 | |||
| af3b5439a5 | |||
| 59afa49be0 | |||
| bfd38f50ea | |||
| 37ed5c4156 | |||
| 2eb16eb747 | |||
| bd6b6eef85 | |||
| 43f1b82d33 | |||
| 24afbd2953 | |||
| a74a6166a3 | |||
| e23e9ccd82 | |||
| e787b0a940 | |||
| 67066a1ba7 | |||
| 08a7d5c0f6 | |||
| 472b13ec26 | |||
| b901c9e0ff | |||
| 0b153ddcbb | |||
| 784c284723 | |||
| e5f73ea8b4 | |||
| 96551259c5 | |||
| b2fa533ef0 | |||
| 7aa0f9f50d | |||
| 5aa90c25f7 | |||
| 38d58db08d | |||
| e637df232a | |||
| d97c633cd0 | |||
| b02396c61f | |||
| 95de50b925 | |||
| 1a150a10fd | |||
| 158e7dbe98 | |||
| a642f11503 | |||
| 888a3f001b | |||
| 4b0845bee8 | |||
| f449e3277a | |||
| 3303f253b4 | |||
| 0250954ee7 | |||
| efed8b2774 | |||
| c891afa611 | |||
| 38541b8f02 | |||
| 0f4c382c18 | |||
| b90d4ef00f | |||
| 3fb33facc5 | |||
| c4a68ff1d5 | |||
| c4f0fa48ec | |||
| 26ed63d65e | |||
| b9e81b2278 | |||
| f6f437b17a | |||
| 91af8f1c04 | |||
| abc9d5154e | |||
| 8228d7d2c2 | |||
| 4adc20f80d | |||
| a5c7d7dd22 | |||
| dc5c2c1d68 | |||
| 42b3252353 | |||
| 1a17d3e69b | |||
| 79ef38ee6b | |||
| 0ff4b30d71 | |||
| 6c4ff6c791 |
@@ -0,0 +1,20 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
ignore:
|
||||||
|
# Major updates for react-admin have breaking changes
|
||||||
|
- dependency-name: "react-admin"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -10,11 +10,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 16
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ name: Create docker image(s) and push to docker hub
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
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:
|
tags:
|
||||||
- '[0-9]+\.[0-9]+\.[0-9]+'
|
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@@ -11,26 +17,35 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push
|
- name: Calculate docker image tag
|
||||||
uses: docker/build-push-action@v2
|
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:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
push: true
|
||||||
tags: awesometechnologies/synapse-admin:latest
|
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
|
||||||
- name: Update repo description
|
platforms: linux/amd64,linux/arm64
|
||||||
uses: peter-evans/dockerhub-description@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
repository: awesometechnologies/synapse-admin
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Build and Deploy Edge version to GH Pages
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🛎️
|
||||||
|
uses: actions/checkout@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
|
||||||
@@ -8,12 +8,15 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "14"
|
node-version: "16"
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: |
|
- run: |
|
||||||
@@ -21,7 +24,7 @@ jobs:
|
|||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
cp -r build synapse-admin-$version
|
cp -r build synapse-admin-$version
|
||||||
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
||||||
- uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
|
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
|
||||||
with:
|
with:
|
||||||
files: dist/*.tar.gz
|
files: dist/*.tar.gz
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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
@@ -6,6 +6,6 @@
|
|||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"jsxBracketSameLine": false,
|
"bracketSameLine": false,
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
|
dist: focal
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- lts/*
|
- 17
|
||||||
|
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|||||||
+6
-2
@@ -1,11 +1,15 @@
|
|||||||
# Builder
|
# Builder
|
||||||
FROM node:current as builder
|
FROM node:lts as builder
|
||||||
|
|
||||||
|
ARG PUBLIC_URL=/
|
||||||
|
ARG REACT_APP_SERVER
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src
|
COPY . /src
|
||||||
|
RUN yarn --network-timeout 500000 add @mui/icons-material
|
||||||
RUN yarn --network-timeout=100000 install
|
RUN yarn --network-timeout=100000 install
|
||||||
RUN yarn build
|
RUN PUBLIC_URL=$PUBLIC_URL REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
||||||
|
|
||||||
|
|
||||||
# App
|
# App
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
[](https://travis-ci.org/Awesome-Technologies/synapse-admin)
|
[](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
|
||||||
|
[](https://app.travis-ci.com/github/Awesome-Technologies/synapse-admin)
|
||||||
[](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
|
[](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
|
||||||
|
[](https://awesome-technologies.github.io/synapse-admin/)
|
||||||
|
[](https://hub.docker.com/r/awesometechnologies/synapse-admin)
|
||||||
|
[](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/).
|
||||||
|
|
||||||
It needs at least Synapse v1.38.0 for all functions to work as expected!
|
## Usage
|
||||||
|
|
||||||
|
### 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://matrix-org.github.io/synapse/develop/admin_api/version_api.html).
|
||||||
|
|
||||||
After entering the URL on the login page of synapse-admin the server version appears below the input field.
|
After entering the URL on the login page of synapse-admin the server version appears below the input field.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
You need access to the following endpoints:
|
You need access to the following endpoints:
|
||||||
|
|
||||||
- `/_matrix`
|
- `/_matrix`
|
||||||
@@ -19,15 +29,25 @@ You need access to the following endpoints:
|
|||||||
|
|
||||||
See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints)
|
See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints)
|
||||||
|
|
||||||
## Step-By-Step install:
|
### Use without install
|
||||||
|
|
||||||
|
You can use the current version of Synapse Admin without own installation direct
|
||||||
|
via [GitHub Pages](https://awesome-technologies.github.io/synapse-admin/).
|
||||||
|
|
||||||
|
**Note:**
|
||||||
|
If you want to use the deployment, you have to make sure that the admin endpoints (`/_synapse/admin`) are accessible for your browser.
|
||||||
|
**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
|
||||||
|
|
||||||
You have three options:
|
You have three options:
|
||||||
|
|
||||||
1. Download the tarball and serve with any webserver
|
1. [Download the tarball and serve with any webserver](#steps-for-1)
|
||||||
2. Download the source code from github and run using nodejs
|
2. [Download the source code from github and run using nodejs](#steps-for-2)
|
||||||
3. Run the Docker container
|
3. [Run the Docker container](#steps-for-3)
|
||||||
|
|
||||||
Steps for 1):
|
#### Steps for 1)
|
||||||
|
|
||||||
- make sure you have a webserver installed that can serve static files (any webserver like nginx or apache will do)
|
- 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
|
- configure a vhost for synapse admin on your webserver
|
||||||
@@ -36,7 +56,7 @@ Steps for 1):
|
|||||||
- move or symlink the `synapse-admin-x.x.x` into your vhosts root dir
|
- move or symlink the `synapse-admin-x.x.x` into your vhosts root dir
|
||||||
- open the url of the vhost in your browser
|
- open the url of the vhost in your browser
|
||||||
|
|
||||||
Steps for 2):
|
#### 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`
|
||||||
@@ -49,7 +69,7 @@ Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver
|
|||||||
or by editing it in the [.env](.env) file. See also the
|
or by editing it in the [.env](.env) file. See also the
|
||||||
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
|
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
|
||||||
|
|
||||||
Steps for 3):
|
#### 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`
|
||||||
|
|
||||||
@@ -66,6 +86,9 @@ Steps for 3):
|
|||||||
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
|
||||||
|
|||||||
+6
-1
@@ -12,10 +12,15 @@ 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
|
||||||
|
|||||||
+13
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "AMP/2021.08",
|
"version": "0.8.5",
|
||||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -10,29 +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.1.1",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@testing-library/user-event": "^13.1.8",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"eslint": "^7.25.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.1.2",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^2.2.0",
|
"prettier": "^2.2.0",
|
||||||
"ra-test": "^3.15.0"
|
"ra-test": "^3.15.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@progress/kendo-drawing": "^1.6.0",
|
"@emotion/react": "^11.7.1",
|
||||||
"@progress/kendo-react-pdf": "^3.10.1",
|
"@emotion/styled": "^11.6.0",
|
||||||
"babel-preset-jest": "^24.9.0",
|
"@mui/icons-material": "^5.3.1",
|
||||||
|
"@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": "^3.13.4",
|
"ra-language-german": "^3.13.4",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-admin": "^3.15.0",
|
"react-admin": "^3.19.7",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-scripts": "^4.0.0"
|
"react-scripts": "^5.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 358 KiB |
@@ -9,32 +9,6 @@
|
|||||||
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/
|
||||||
|
|||||||
+33
-18
@@ -4,27 +4,36 @@ 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, RoomCreate, RoomShow, RoomEdit } from "./components/rooms";
|
import { RoomList, RoomShow } from "./components/rooms";
|
||||||
import { ReportList, ReportShow } from "./components/EventReports";
|
import { ReportList, ReportShow } from "./components/EventReports";
|
||||||
import ImportFeature from "./components/ImportFeature";
|
|
||||||
import LoginPage from "./components/LoginPage";
|
import LoginPage from "./components/LoginPage";
|
||||||
import UserIcon from "@material-ui/icons/Group";
|
import ConfirmationNumberIcon from "@mui/icons-material/ConfirmationNumber";
|
||||||
import EqualizerIcon from "@material-ui/icons/Equalizer";
|
import CloudQueueIcon from "@mui/icons-material/CloudQueue";
|
||||||
|
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 "@material-ui/icons/ViewList";
|
import RoomIcon from "@mui/icons-material/ViewList";
|
||||||
import ReportIcon from "@material-ui/icons/Warning";
|
import ReportIcon from "@mui/icons-material/Warning";
|
||||||
import FolderSharedIcon from "@material-ui/icons/FolderShared";
|
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||||
|
import { DestinationList, DestinationShow } from "./components/destinations";
|
||||||
|
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(
|
||||||
@@ -40,8 +49,7 @@ const App = () => (
|
|||||||
dataProvider={dataProvider}
|
dataProvider={dataProvider}
|
||||||
i18nProvider={i18nProvider}
|
i18nProvider={i18nProvider}
|
||||||
customRoutes={[
|
customRoutes={[
|
||||||
<Route key="csvImport" path="/importcsv" component={ImportFeature} />,
|
<Route key="userImport" path="/import_users" component={ImportFeature} />,
|
||||||
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Resource
|
<Resource
|
||||||
@@ -51,14 +59,7 @@ const App = () => (
|
|||||||
edit={UserEdit}
|
edit={UserEdit}
|
||||||
icon={UserIcon}
|
icon={UserIcon}
|
||||||
/>
|
/>
|
||||||
<Resource
|
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} />
|
||||||
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}
|
||||||
@@ -75,6 +76,19 @@ 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" />
|
||||||
@@ -84,6 +98,7 @@ const App = () => (
|
|||||||
<Resource name="servernotices" />
|
<Resource name="servernotices" />
|
||||||
<Resource name="forward_extremities" />
|
<Resource name="forward_extremities" />
|
||||||
<Resource name="room_state" />
|
<Resource name="room_state" />
|
||||||
|
<Resource name="destination_rooms" />
|
||||||
</Admin>
|
</Admin>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,17 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import PageviewIcon from "@material-ui/icons/Pageview";
|
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||||
import ViewListIcon from "@material-ui/icons/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
|
||||||
|
const date_format = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
const ReportPagination = props => (
|
const ReportPagination = props => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
@@ -33,14 +42,7 @@ export const ReportShow = props => {
|
|||||||
<DateField
|
<DateField
|
||||||
source="received_ts"
|
source="received_ts"
|
||||||
showTime
|
showTime
|
||||||
options={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
sortable={true}
|
sortable={true}
|
||||||
/>
|
/>
|
||||||
<ReferenceField source="user_id" reference="users">
|
<ReferenceField source="user_id" reference="users">
|
||||||
@@ -68,18 +70,10 @@ 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={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
sortable={true}
|
sortable={true}
|
||||||
/>
|
/>
|
||||||
<ReferenceField source="sender" reference="users">
|
<ReferenceField source="sender" reference="users">
|
||||||
@@ -116,14 +110,7 @@ export const ReportList = ({ ...props }) => {
|
|||||||
<DateField
|
<DateField
|
||||||
source="received_ts"
|
source="received_ts"
|
||||||
showTime
|
showTime
|
||||||
options={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
sortable={true}
|
sortable={true}
|
||||||
/>
|
/>
|
||||||
<TextField sortable={false} source="user_id" />
|
<TextField sortable={false} source="user_id" />
|
||||||
|
|||||||
@@ -6,21 +6,20 @@ 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 "@material-ui/icons/GetApp";
|
import GetAppIcon from "@mui/icons-material/GetApp";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardActions,
|
CardActions,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
FormControlLabel,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Container,
|
||||||
|
FormControlLabel,
|
||||||
NativeSelect,
|
NativeSelect,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
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";
|
||||||
import ShowUserPdf from "./ShowUserPdf";
|
|
||||||
|
|
||||||
const LOGGING = true;
|
const LOGGING = true;
|
||||||
|
|
||||||
@@ -60,8 +59,6 @@ const FilePicker = props => {
|
|||||||
|
|
||||||
const [progress, setProgress] = useState(null);
|
const [progress, setProgress] = useState(null);
|
||||||
|
|
||||||
const [pdfRecords, setPdfRecords] = useState(null);
|
|
||||||
|
|
||||||
const [importResults, setImportResults] = useState(null);
|
const [importResults, setImportResults] = useState(null);
|
||||||
const [skippedRecords, setSkippedRecords] = useState(null);
|
const [skippedRecords, setSkippedRecords] = useState(null);
|
||||||
|
|
||||||
@@ -69,23 +66,17 @@ const FilePicker = props => {
|
|||||||
const [passwordMode, setPasswordMode] = useState(true);
|
const [passwordMode, setPasswordMode] = useState(true);
|
||||||
const [useridMode, setUseridMode] = useState("ignore");
|
const [useridMode, setUseridMode] = useState("ignore");
|
||||||
|
|
||||||
const [showingPdf, setShowingPdf] = useState(false);
|
|
||||||
|
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
const onFileChange = async e => {
|
const onFileChange = async e => {
|
||||||
if (progress !== null) {
|
if (progress !== null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (LOGGING) console.log("onFileChange was called");
|
|
||||||
setValues(null);
|
setValues(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setStats(null);
|
setStats(null);
|
||||||
setPdfRecords(null);
|
|
||||||
|
|
||||||
setImportResults(null);
|
setImportResults(null);
|
||||||
const file = e.target.files ? e.target.files[0] : null;
|
const file = e.target.files ? e.target.files[0] : null;
|
||||||
/* Let's refuse some unreasonably big files instead of freezing
|
/* Let's refuse some unreasonably big files instead of freezing
|
||||||
@@ -135,11 +126,6 @@ const FilePicker = props => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (eF.length !== 0) {
|
if (eF.length !== 0) {
|
||||||
if (LOGGING) {
|
|
||||||
console.log(meta.fields);
|
|
||||||
console.log(eF);
|
|
||||||
console.log(oF);
|
|
||||||
}
|
|
||||||
setError(
|
setError(
|
||||||
translate("import_users.error.required_field", { field: eF[0] })
|
translate("import_users.error.required_field", { field: eF[0] })
|
||||||
);
|
);
|
||||||
@@ -240,9 +226,6 @@ const FilePicker = props => {
|
|||||||
setProgress,
|
setProgress,
|
||||||
setError
|
setError
|
||||||
);
|
);
|
||||||
|
|
||||||
setPdfRecords(results.recordsForPdf);
|
|
||||||
|
|
||||||
setImportResults(results);
|
setImportResults(results);
|
||||||
// offer CSV download of skipped or errored records
|
// offer CSV download of skipped or errored records
|
||||||
// (so that the user doesn't have to filter out successful
|
// (so that the user doesn't have to filter out successful
|
||||||
@@ -268,8 +251,6 @@ const FilePicker = props => {
|
|||||||
let skippedRecords = [];
|
let skippedRecords = [];
|
||||||
let erroredRecords = [];
|
let erroredRecords = [];
|
||||||
let succeededRecords = [];
|
let succeededRecords = [];
|
||||||
let recordsForPdf = [];
|
|
||||||
|
|
||||||
let changeStats = {
|
let changeStats = {
|
||||||
toAdmin: 0,
|
toAdmin: 0,
|
||||||
toGuest: 0,
|
toGuest: 0,
|
||||||
@@ -384,14 +365,6 @@ const FilePicker = props => {
|
|||||||
await dataProvider.create("users", { data: recordData });
|
await dataProvider.create("users", { data: recordData });
|
||||||
}
|
}
|
||||||
succeededRecords.push(recordData);
|
succeededRecords.push(recordData);
|
||||||
|
|
||||||
if (recordData.password !== undefined) {
|
|
||||||
recordsForPdf.push({
|
|
||||||
id: recordData.id,
|
|
||||||
password: recordData.password,
|
|
||||||
displayname: recordData.displayname,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -416,7 +389,6 @@ const FilePicker = props => {
|
|||||||
erroredRecords,
|
erroredRecords,
|
||||||
succeededRecords,
|
succeededRecords,
|
||||||
totalRecordCount: entriesCount,
|
totalRecordCount: entriesCount,
|
||||||
recordsForPdf,
|
|
||||||
changeStats,
|
changeStats,
|
||||||
wasDryRun: dryRun,
|
wasDryRun: dryRun,
|
||||||
};
|
};
|
||||||
@@ -646,10 +618,6 @@ const FilePicker = props => {
|
|||||||
<br />,
|
<br />,
|
||||||
]
|
]
|
||||||
: ""}
|
: ""}
|
||||||
{translate(
|
|
||||||
"import_users.cards.results.for_print",
|
|
||||||
importResults.recordsForPdf.length
|
|
||||||
)}
|
|
||||||
<br />
|
<br />
|
||||||
{importResults.wasDryRun && [
|
{importResults.wasDryRun && [
|
||||||
translate("import_users.cards.results.simulated_only"),
|
translate("import_users.cards.results.simulated_only"),
|
||||||
@@ -687,43 +655,20 @@ const FilePicker = props => {
|
|||||||
</CardActions>
|
</CardActions>
|
||||||
);
|
);
|
||||||
|
|
||||||
let pdfDisplay =
|
let allCards = [];
|
||||||
pdfRecords && showingPdf && pdfRecords.length ? (
|
if (uploadCard) allCards.push(uploadCard);
|
||||||
<ShowUserPdf records={pdfRecords} />
|
if (errorCards) allCards.push(errorCards);
|
||||||
) : null;
|
if (conflictCards) allCards.push(conflictCards);
|
||||||
|
if (statsCards) allCards.push(...statsCards);
|
||||||
|
if (startImportCard) allCards.push(startImportCard);
|
||||||
|
if (resultsCard) allCards.push(resultsCard);
|
||||||
|
|
||||||
let pdfActions = pdfRecords ? (
|
let cardContainer = <Card>{allCards}</Card>;
|
||||||
<CardActions>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
onClick={e => {
|
|
||||||
setShowingPdf(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{translate("import_users.goToPdf")}
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
if (pdfRecords && showingPdf) {
|
return [
|
||||||
return <Card>{pdfDisplay}</Card>;
|
<Title defaultTitle={translate("import_users.title")} />,
|
||||||
} else {
|
cardContainer,
|
||||||
let allCards = [];
|
];
|
||||||
if (uploadCard) allCards.push(uploadCard);
|
|
||||||
if (errorCards) allCards.push(errorCards);
|
|
||||||
if (conflictCards) allCards.push(conflictCards);
|
|
||||||
if (statsCards) allCards.push(...statsCards);
|
|
||||||
if (startImportCard) allCards.push(startImportCard);
|
|
||||||
if (resultsCard) allCards.push(resultsCard);
|
|
||||||
if (pdfActions) allCards.push(pdfActions);
|
|
||||||
|
|
||||||
let cardContainer = <Card>{allCards}</Card>;
|
|
||||||
|
|
||||||
return [
|
|
||||||
<Title defaultTitle={translate("import_users.title")} />,
|
|
||||||
cardContainer,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImportFeature = FilePicker;
|
export const ImportFeature = FilePicker;
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import LockIcon from "@material-ui/icons/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
main: {
|
main: {
|
||||||
@@ -78,11 +78,48 @@ const LoginPage = ({ theme }) => {
|
|||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||||
var locale = useLocale();
|
var locale = useLocale();
|
||||||
const setLocale = useSetLocale();
|
const setLocale = useSetLocale();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = localStorage.getItem("base_url");
|
||||||
const cfg_base_url = process.env.REACT_APP_SERVER;
|
const 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 } = {},
|
||||||
@@ -132,11 +169,19 @@ 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,
|
||||||
"warning"
|
{ type: "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;
|
||||||
@@ -188,6 +233,31 @@ const LoginPage = ({ theme }) => {
|
|||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
setServerVersion("");
|
setServerVersion("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set SSO Url
|
||||||
|
const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`;
|
||||||
|
let supportPass = false,
|
||||||
|
supportSSO = false;
|
||||||
|
fetchUtils
|
||||||
|
.fetchJson(authMethodUrl, { method: "GET" })
|
||||||
|
.then(({ json }) => {
|
||||||
|
json.flows.forEach(f => {
|
||||||
|
if (f.type === "m.login.password") {
|
||||||
|
supportPass = true;
|
||||||
|
} else if (f.type === "m.login.sso") {
|
||||||
|
supportSSO = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setSupportPassAuth(supportPass);
|
||||||
|
if (supportSSO) {
|
||||||
|
setSSOBaseUrl(formData.base_url);
|
||||||
|
} else {
|
||||||
|
setSSOBaseUrl("");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(_ => {
|
||||||
|
setSSOBaseUrl("");
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[formData.base_url]
|
[formData.base_url]
|
||||||
);
|
);
|
||||||
@@ -199,8 +269,8 @@ const LoginPage = ({ theme }) => {
|
|||||||
autoFocus
|
autoFocus
|
||||||
name="username"
|
name="username"
|
||||||
component={renderInput}
|
component={renderInput}
|
||||||
label={translate("ra.auth.username")}
|
label="ra.auth.username"
|
||||||
disabled={loading}
|
disabled={loading || !supportPassAuth}
|
||||||
onBlur={handleUsernameChange}
|
onBlur={handleUsernameChange}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -210,9 +280,9 @@ const LoginPage = ({ theme }) => {
|
|||||||
<PasswordInput
|
<PasswordInput
|
||||||
name="password"
|
name="password"
|
||||||
component={renderInput}
|
component={renderInput}
|
||||||
label={translate("ra.auth.password")}
|
label="ra.auth.password"
|
||||||
type="password"
|
type="password"
|
||||||
disabled={loading}
|
disabled={loading || !supportPassAuth}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
@@ -221,7 +291,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
name="base_url"
|
name="base_url"
|
||||||
component={renderInput}
|
component={renderInput}
|
||||||
label={translate("synapseadmin.auth.base_url")}
|
label="synapseadmin.auth.base_url"
|
||||||
disabled={cfg_base_url || loading}
|
disabled={cfg_base_url || loading}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -261,6 +331,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
>
|
>
|
||||||
<MenuItem value="de">Deutsch</MenuItem>
|
<MenuItem value="de">Deutsch</MenuItem>
|
||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem value="en">English</MenuItem>
|
||||||
|
<MenuItem value="fr">Français</MenuItem>
|
||||||
<MenuItem value="zh">简体中文</MenuItem>
|
<MenuItem value="zh">简体中文</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -273,13 +344,24 @@ const LoginPage = ({ theme }) => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={loading}
|
disabled={loading || !supportPassAuth}
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
{loading && <CircularProgress size={25} thickness={2} />}
|
{loading && <CircularProgress size={25} thickness={2} />}
|
||||||
{translate("ra.auth.sign_in")}
|
{translate("ra.auth.sign_in")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleSSO}
|
||||||
|
disabled={loading || ssoBaseUrl === ""}
|
||||||
|
className={classes.button}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{loading && <CircularProgress size={25} thickness={2} />}
|
||||||
|
{translate("synapseadmin.auth.sso_sign_in")}
|
||||||
|
</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
<Notification />
|
<Notification />
|
||||||
|
|||||||
@@ -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 "@material-ui/core";
|
import { useMediaQuery } from "@mui/material";
|
||||||
import { MenuItemLink, getResources } from "react-admin";
|
import { MenuItemLink, getResources } from "react-admin";
|
||||||
import DefaultIcon from "@material-ui/icons/ViewList";
|
import DefaultIcon from "@mui/icons-material/ViewList";
|
||||||
import LabelIcon from "@material-ui/icons/Label";
|
import LabelIcon from "@mui/icons-material/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"));
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
BooleanInput,
|
||||||
|
Create,
|
||||||
|
Datagrid,
|
||||||
|
DateField,
|
||||||
|
DateTimeInput,
|
||||||
|
Edit,
|
||||||
|
Filter,
|
||||||
|
List,
|
||||||
|
maxValue,
|
||||||
|
number,
|
||||||
|
NumberField,
|
||||||
|
NumberInput,
|
||||||
|
regex,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
TextField,
|
||||||
|
Toolbar,
|
||||||
|
} from "react-admin";
|
||||||
|
|
||||||
|
const date_format = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
|
||||||
|
const validateUsesAllowed = [number()];
|
||||||
|
const validateLength = [number(), maxValue(64)];
|
||||||
|
|
||||||
|
const dateParser = v => {
|
||||||
|
const d = new Date(v);
|
||||||
|
if (isNaN(d)) return 0;
|
||||||
|
return d.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateFormatter = v => {
|
||||||
|
if (v === undefined || v === null) return;
|
||||||
|
const d = new Date(v);
|
||||||
|
|
||||||
|
const pad = "00";
|
||||||
|
const year = d.getFullYear().toString();
|
||||||
|
const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
|
||||||
|
const day = (pad + d.getDate().toString()).slice(-2);
|
||||||
|
const hour = (pad + d.getHours().toString()).slice(-2);
|
||||||
|
const minute = (pad + d.getMinutes().toString()).slice(-2);
|
||||||
|
|
||||||
|
// target format yyyy-MM-ddThh:mm
|
||||||
|
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RegistrationTokenFilter = props => (
|
||||||
|
<Filter {...props}>
|
||||||
|
<BooleanInput source="valid" alwaysOn />
|
||||||
|
</Filter>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RegistrationTokenList = props => {
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
filters={<RegistrationTokenFilter />}
|
||||||
|
filterDefaultValues={{ valid: true }}
|
||||||
|
pagination={false}
|
||||||
|
perPage={500}
|
||||||
|
>
|
||||||
|
<Datagrid rowClick="edit">
|
||||||
|
<TextField source="token" sortable={false} />
|
||||||
|
<NumberField source="uses_allowed" sortable={false} />
|
||||||
|
<NumberField source="pending" sortable={false} />
|
||||||
|
<NumberField source="completed" sortable={false} />
|
||||||
|
<DateField
|
||||||
|
source="expiry_time"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RegistrationTokenCreate = props => (
|
||||||
|
<Create {...props}>
|
||||||
|
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}>
|
||||||
|
<TextInput
|
||||||
|
source="token"
|
||||||
|
autoComplete="off"
|
||||||
|
validate={validateToken}
|
||||||
|
resettable
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
source="length"
|
||||||
|
validate={validateLength}
|
||||||
|
helperText="resources.registration_tokens.helper.length"
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
source="uses_allowed"
|
||||||
|
validate={validateUsesAllowed}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
<DateTimeInput source="expiry_time" parse={dateParser} />
|
||||||
|
</SimpleForm>
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RegistrationTokenEdit = props => {
|
||||||
|
return (
|
||||||
|
<Edit {...props}>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="token" disabled />
|
||||||
|
<NumberInput source="pending" disabled />
|
||||||
|
<NumberInput source="completed" disabled />
|
||||||
|
<NumberInput
|
||||||
|
source="uses_allowed"
|
||||||
|
validate={validateUsesAllowed}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
<DateTimeInput
|
||||||
|
source="expiry_time"
|
||||||
|
parse={dateParser}
|
||||||
|
format={dateFormatter}
|
||||||
|
/>
|
||||||
|
</SimpleForm>
|
||||||
|
</Edit>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import Avatar from "@material-ui/core/Avatar";
|
import { Avatar, Chip } from "@mui/material";
|
||||||
import { Chip } from "@material-ui/core";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import FolderSharedIcon from "@material-ui/icons/FolderShared";
|
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
@@ -19,6 +18,7 @@ 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"
|
||||||
undoable={false}
|
mutationMode="pessimistic"
|
||||||
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,7 +87,9 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
|
|||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: error =>
|
onFailure: error =>
|
||||||
notify("resources.room_directory.action.send_failure", "error"),
|
notify("resources.room_directory.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -103,7 +105,8 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomDirectorySaveButton = ({ record }) => {
|
export const RoomDirectorySaveButton = props => {
|
||||||
|
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");
|
||||||
@@ -119,7 +122,9 @@ export const RoomDirectorySaveButton = ({ record }) => {
|
|||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: error =>
|
onFailure: error =>
|
||||||
notify("resources.room_directory.action.send_failure", "error"),
|
notify("resources.room_directory.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -177,7 +182,6 @@ export const FilterableRoomDirectoryList = ({
|
|||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const translate = useTranslate();
|
|
||||||
const filter = roomDirectoryFilters;
|
const filter = 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;
|
||||||
@@ -191,53 +195,53 @@ export const FilterableRoomDirectoryList = ({
|
|||||||
filters={<RoomDirectoryFilter />}
|
filters={<RoomDirectoryFilter />}
|
||||||
perPage={100}
|
perPage={100}
|
||||||
>
|
>
|
||||||
<Datagrid>
|
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}>
|
||||||
<AvatarField
|
<AvatarField
|
||||||
source="avatar_src"
|
source="avatar_src"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
className={classes.small}
|
className={classes.small}
|
||||||
label={translate("resources.rooms.fields.avatar")}
|
label="resources.rooms.fields.avatar"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
source="name"
|
source="name"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
label={translate("resources.rooms.fields.name")}
|
label="resources.rooms.fields.name"
|
||||||
/>
|
/>
|
||||||
{roomIdFilter && (
|
{roomIdFilter && (
|
||||||
<TextField
|
<TextField
|
||||||
source="room_id"
|
source="room_id"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
label={translate("resources.rooms.fields.room_id")}
|
label="resources.rooms.fields.room_id"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canonicalAliasFilter && (
|
{canonicalAliasFilter && (
|
||||||
<TextField
|
<TextField
|
||||||
source="canonical_alias"
|
source="canonical_alias"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
label={translate("resources.rooms.fields.canonical_alias")}
|
label="resources.rooms.fields.canonical_alias"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{topicFilter && (
|
{topicFilter && (
|
||||||
<TextField
|
<TextField
|
||||||
source="topic"
|
source="topic"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
label={translate("resources.rooms.fields.topic")}
|
label="resources.rooms.fields.topic"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<NumberField
|
<NumberField
|
||||||
source="num_joined_members"
|
source="num_joined_members"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
label={translate("resources.rooms.fields.joined_members")}
|
label="resources.rooms.fields.joined_members"
|
||||||
/>
|
/>
|
||||||
<BooleanField
|
<BooleanField
|
||||||
source="world_readable"
|
source="world_readable"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
label={translate("resources.room_directory.fields.world_readable")}
|
label="resources.room_directory.fields.world_readable"
|
||||||
/>
|
/>
|
||||||
<BooleanField
|
<BooleanField
|
||||||
source="guest_can_join"
|
source="guest_can_join"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
label={translate("resources.room_directory.fields.guest_can_join")}
|
label="resources.room_directory.fields.guest_can_join"
|
||||||
/>
|
/>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -9,22 +9,28 @@ import {
|
|||||||
useCreate,
|
useCreate,
|
||||||
useMutation,
|
useMutation,
|
||||||
useNotify,
|
useNotify,
|
||||||
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import MessageIcon from "@material-ui/icons/Message";
|
import MessageIcon from "@mui/icons-material/Message";
|
||||||
import IconCancel from "@material-ui/icons/Cancel";
|
import IconCancel from "@mui/icons-material/Cancel";
|
||||||
import Dialog from "@material-ui/core/Dialog";
|
import {
|
||||||
import DialogContent from "@material-ui/core/DialogContent";
|
Dialog,
|
||||||
import DialogContentText from "@material-ui/core/DialogContentText";
|
DialogContent,
|
||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
DialogContentText,
|
||||||
|
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 label="resources.servernotices.action.send" />
|
<SaveButton
|
||||||
|
label="resources.servernotices.action.send"
|
||||||
|
disabled={props.pristine}
|
||||||
|
/>
|
||||||
<Button label="ra.action.cancel" onClick={onClose}>
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
<IconCancel />
|
<IconCancel />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -61,7 +67,8 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServerNoticeButton = ({ record }) => {
|
export const ServerNoticeButton = props => {
|
||||||
|
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");
|
||||||
@@ -78,7 +85,9 @@ export const ServerNoticeButton = ({ record }) => {
|
|||||||
handleDialogClose();
|
handleDialogClose();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onFailure: () =>
|
||||||
notify("resources.servernotices.action.send_failure", "error"),
|
notify("resources.servernotices.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -124,7 +133,9 @@ export const ServerNoticeBulkButton = ({ selectedIds }) => {
|
|||||||
handleDialogClose();
|
handleDialogClose();
|
||||||
},
|
},
|
||||||
onFailure: error =>
|
onFailure: error =>
|
||||||
notify("resources.servernotices.action.send_failure", "error"),
|
notify("resources.servernotices.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
import React, { useRef } 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";
|
|
||||||
import { string, any } from "prop-types";
|
|
||||||
|
|
||||||
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";
|
|
||||||
const origUrlString = "user=" + username + "&password=" + password;
|
|
||||||
|
|
||||||
var urlString = xor(origUrlString, magicString); // xor with magic string
|
|
||||||
if (origUrlString !== xor(urlString, magicString)) {
|
|
||||||
console.error(
|
|
||||||
"xoring this url string with magicString twice gave different results:",
|
|
||||||
origUrlString,
|
|
||||||
urlString,
|
|
||||||
xor(urlString, magicString)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
urlString = btoa(urlString); // to base64
|
|
||||||
|
|
||||||
return serverUrl + "/#" + urlString;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserPdfPage.propTypes = {
|
|
||||||
classes: any,
|
|
||||||
displayname: string,
|
|
||||||
qrCode: any,
|
|
||||||
serverUrl: string,
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
function UserPdfPage({
|
|
||||||
classes,
|
|
||||||
displayname,
|
|
||||||
qrCode,
|
|
||||||
serverUrl,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ShowUserPdf = props => {
|
|
||||||
const classes = useStyles();
|
|
||||||
const userPdf = useRef(null);
|
|
||||||
|
|
||||||
const exportPDF = () => {
|
|
||||||
userPdf.current.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
let userRecords;
|
|
||||||
|
|
||||||
if (props.records) {
|
|
||||||
userRecords = props.records;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
props.location &&
|
|
||||||
props.location.state &&
|
|
||||||
props.location.state.id &&
|
|
||||||
props.location.state.password
|
|
||||||
) {
|
|
||||||
userRecords = [
|
|
||||||
{
|
|
||||||
id: props.location.state.id,
|
|
||||||
password: props.location.state.password,
|
|
||||||
displayname: props.location.state.displayname,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Title title="PDF" />
|
|
||||||
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
|
|
||||||
|
|
||||||
<PDFExport
|
|
||||||
paperSize={"A4"}
|
|
||||||
fileName="Users.pdf"
|
|
||||||
title=""
|
|
||||||
subject=""
|
|
||||||
keywords=""
|
|
||||||
ref={userPdf}
|
|
||||||
//ref={r => (resume = r)}
|
|
||||||
>
|
|
||||||
{userRecords.map(record => {
|
|
||||||
if (record.id && record.password) {
|
|
||||||
const username = record.id.substring(1, record.id.indexOf(":"));
|
|
||||||
const serverUrl =
|
|
||||||
"https://" + record.id.substring(record.id.indexOf(":") + 1);
|
|
||||||
const qrString = calculateQrString(
|
|
||||||
serverUrl,
|
|
||||||
username,
|
|
||||||
record.password
|
|
||||||
);
|
|
||||||
const qrCode = <QRCode value={qrString} size={128} />;
|
|
||||||
return (
|
|
||||||
<UserPdfPage
|
|
||||||
classes={classes}
|
|
||||||
displayname={record.displayname}
|
|
||||||
qrCode={qrCode}
|
|
||||||
serverUrl={serverUrl}
|
|
||||||
username={username}
|
|
||||||
password={record.password}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
/* Skip empty PDF pages */
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</PDFExport>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShowUserPdf;
|
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+12
-17
@@ -1,14 +1,15 @@
|
|||||||
import React, { Fragment, useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
useMutation,
|
useDelete,
|
||||||
useNotify,
|
useNotify,
|
||||||
Confirm,
|
Confirm,
|
||||||
|
useRecordContext,
|
||||||
useRefresh,
|
useRefresh,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import ActionDelete from "@material-ui/icons/Delete";
|
import ActionDelete from "@mui/icons-material/Delete";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
import { alpha } from "@mui/material/styles";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
@@ -16,7 +17,7 @@ const useStyles = makeStyles(
|
|||||||
deleteButton: {
|
deleteButton: {
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: fade(theme.palette.error.main, 0.12),
|
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
||||||
// Reset on mouse devices
|
// Reset on mouse devices
|
||||||
"@media (hover: none)": {
|
"@media (hover: none)": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
@@ -28,13 +29,13 @@ const useStyles = makeStyles(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const DeviceRemoveButton = props => {
|
export const DeviceRemoveButton = props => {
|
||||||
const { record } = props;
|
const record = useRecordContext();
|
||||||
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, { loading }] = useMutation();
|
const [removeDevice, { isLoading }] = useDelete("devices");
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
@@ -43,21 +44,15 @@ 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", "error"),
|
notify("resources.devices.action.erase.failure", { type: "error" });
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -74,7 +69,7 @@ export const DeviceRemoveButton = props => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Confirm
|
<Confirm
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
loading={loading}
|
loading={isLoading}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
onClose={handleDialogClose}
|
onClose={handleDialogClose}
|
||||||
title="resources.devices.action.erase.title"
|
title="resources.devices.action.erase.title"
|
||||||
|
|||||||
+33
-20
@@ -1,8 +1,7 @@
|
|||||||
import React, { Fragment, useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
import { alpha } from "@mui/material/styles";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import { Tooltip } from "@material-ui/core";
|
|
||||||
import {
|
import {
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
Button,
|
Button,
|
||||||
@@ -14,26 +13,30 @@ import {
|
|||||||
useCreate,
|
useCreate,
|
||||||
useDelete,
|
useDelete,
|
||||||
useNotify,
|
useNotify,
|
||||||
|
useRecordContext,
|
||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import BlockIcon from "@material-ui/icons/Block";
|
import BlockIcon from "@mui/icons-material/Block";
|
||||||
import ClearIcon from "@material-ui/icons/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
import DeleteSweepIcon from "@material-ui/icons/DeleteSweep";
|
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
||||||
import Dialog from "@material-ui/core/Dialog";
|
import {
|
||||||
import DialogContent from "@material-ui/core/DialogContent";
|
Dialog,
|
||||||
import DialogContentText from "@material-ui/core/DialogContentText";
|
DialogContent,
|
||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
DialogContentText,
|
||||||
import IconCancel from "@material-ui/icons/Cancel";
|
DialogTitle,
|
||||||
import LockIcon from "@material-ui/icons/Lock";
|
Tooltip,
|
||||||
import LockOpenIcon from "@material-ui/icons/LockOpen";
|
} 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: fade(theme.palette.error.main, 0.12),
|
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
||||||
// Reset on mouse devices
|
// Reset on mouse devices
|
||||||
"@media (hover: none)": {
|
"@media (hover: none)": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
@@ -127,7 +130,9 @@ export const DeleteMediaButton = props => {
|
|||||||
handleDialogClose();
|
handleDialogClose();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onFailure: () =>
|
||||||
notify("resources.delete_media.action.send_failure", "error"),
|
notify("resources.delete_media.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -152,7 +157,7 @@ export const DeleteMediaButton = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ProtectMediaButton = props => {
|
export const ProtectMediaButton = props => {
|
||||||
const { record } = props;
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
@@ -170,7 +175,9 @@ export const ProtectMediaButton = props => {
|
|||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onFailure: () =>
|
||||||
notify("resources.protect_media.action.send_failure", "error"),
|
notify("resources.protect_media.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -184,7 +191,9 @@ export const ProtectMediaButton = props => {
|
|||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onFailure: () =>
|
||||||
notify("resources.protect_media.action.send_failure", "error"),
|
notify("resources.protect_media.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -244,7 +253,7 @@ export const ProtectMediaButton = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const QuarantineMediaButton = props => {
|
export const QuarantineMediaButton = props => {
|
||||||
const { record } = props;
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
@@ -262,7 +271,9 @@ export const QuarantineMediaButton = props => {
|
|||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onFailure: () =>
|
||||||
notify("resources.quarantine_media.action.send_failure", "error"),
|
notify("resources.quarantine_media.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -276,7 +287,9 @@ export const QuarantineMediaButton = props => {
|
|||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onFailure: () =>
|
||||||
notify("resources.quarantine_media.action.send_failure", "error"),
|
notify("resources.quarantine_media.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+36
-410
@@ -1,65 +1,39 @@
|
|||||||
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,
|
||||||
Button,
|
|
||||||
Create,
|
|
||||||
Edit,
|
|
||||||
Datagrid,
|
|
||||||
DateField,
|
DateField,
|
||||||
|
Datagrid,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
Filter,
|
Filter,
|
||||||
FormTab,
|
|
||||||
List,
|
List,
|
||||||
NumberField,
|
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,
|
||||||
useDataProvider,
|
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
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 PropTypes from "prop-types";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import {
|
import { Tooltip, Typography, Chip } from "@mui/material";
|
||||||
Tooltip,
|
import FastForwardIcon from "@mui/icons-material/FastForward";
|
||||||
Typography,
|
import HttpsIcon from "@mui/icons-material/Https";
|
||||||
Chip,
|
import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
|
||||||
Drawer,
|
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||||
styled,
|
import UserIcon from "@mui/icons-material/Group";
|
||||||
withStyles,
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
Select,
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
MenuItem,
|
import EventIcon from "@mui/icons-material/Event";
|
||||||
} from "@material-ui/core";
|
|
||||||
import FastForwardIcon from "@material-ui/icons/FastForward";
|
|
||||||
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,
|
||||||
@@ -67,6 +41,15 @@ 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 => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
helper_forward_extremities: {
|
helper_forward_extremities: {
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
@@ -104,368 +87,21 @@ const EncryptionField = ({ source, record = {}, emptyText }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateDisplayName = fieldval => {
|
const RoomTitle = props => {
|
||||||
return fieldval == null
|
const record = useRecordContext();
|
||||||
? "synapseadmin.rooms.room_name_required"
|
|
||||||
: fieldval.length === 0
|
|
||||||
? "synapseadmin.rooms.room_name_required"
|
|
||||||
: 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();
|
const translate = useTranslate();
|
||||||
|
var name = "";
|
||||||
|
if (record) {
|
||||||
|
name = record.name !== "" ? record.name : record.id;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
|
{translate("resources.rooms.name", 1)} {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) {
|
||||||
@@ -523,7 +159,11 @@ export const RoomShow = props => {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />}>
|
<Tab
|
||||||
|
label="synapseadmin.rooms.tabs.members"
|
||||||
|
icon={<UserIcon />}
|
||||||
|
path="members"
|
||||||
|
>
|
||||||
<ReferenceManyField
|
<ReferenceManyField
|
||||||
reference="room_members"
|
reference="room_members"
|
||||||
target="room_id"
|
target="room_id"
|
||||||
@@ -621,14 +261,7 @@ export const RoomShow = props => {
|
|||||||
<DateField
|
<DateField
|
||||||
source="origin_server_ts"
|
source="origin_server_ts"
|
||||||
showTime
|
showTime
|
||||||
options={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
sortable={false}
|
sortable={false}
|
||||||
/>
|
/>
|
||||||
<TextField source="content" sortable={false} />
|
<TextField source="content" sortable={false} />
|
||||||
@@ -661,14 +294,7 @@ export const RoomShow = props => {
|
|||||||
<DateField
|
<DateField
|
||||||
source="received_ts"
|
source="received_ts"
|
||||||
showTime
|
showTime
|
||||||
options={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
sortable={false}
|
sortable={false}
|
||||||
/>
|
/>
|
||||||
<NumberField source="depth" sortable={false} />
|
<NumberField source="depth" sortable={false} />
|
||||||
@@ -689,7 +315,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"
|
||||||
undoable={false}
|
mutationMode="pessimistic"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -729,7 +355,7 @@ const RoomFilter = ({ ...props }) => {
|
|||||||
|
|
||||||
const RoomNameField = props => {
|
const RoomNameField = props => {
|
||||||
const { source } = props;
|
const { source } = props;
|
||||||
const record = useRecordContext(props);
|
const record = useRecordContext();
|
||||||
return (
|
return (
|
||||||
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
|
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
|
||||||
);
|
);
|
||||||
|
|||||||
+123
-161
@@ -1,24 +1,22 @@
|
|||||||
import React, { cloneElement, Fragment } from "react";
|
import React, { cloneElement, Fragment } from "react";
|
||||||
import Avatar from "@material-ui/core/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||||
import ContactMailIcon from "@material-ui/icons/ContactMail";
|
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
||||||
import DevicesIcon from "@material-ui/icons/Devices";
|
import DevicesIcon from "@mui/icons-material/Devices";
|
||||||
import GetAppIcon from "@material-ui/icons/GetApp";
|
import GetAppIcon from "@mui/icons-material/GetApp";
|
||||||
import NotificationsIcon from "@material-ui/icons/Notifications";
|
import NotificationsIcon from "@mui/icons-material/Notifications";
|
||||||
import PermMediaIcon from "@material-ui/icons/PermMedia";
|
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||||
import PersonPinIcon from "@material-ui/icons/PersonPin";
|
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
||||||
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
|
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,
|
||||||
Button,
|
Button,
|
||||||
CreateButton,
|
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
Create,
|
Create,
|
||||||
Edit,
|
Edit,
|
||||||
ExportButton,
|
|
||||||
List,
|
List,
|
||||||
Filter,
|
Filter,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
@@ -31,31 +29,34 @@ import {
|
|||||||
PasswordInput,
|
PasswordInput,
|
||||||
TextField,
|
TextField,
|
||||||
TextInput,
|
TextInput,
|
||||||
SearchInput,
|
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
ReferenceManyField,
|
ReferenceManyField,
|
||||||
SelectField,
|
SearchInput,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
|
maxLength,
|
||||||
regex,
|
regex,
|
||||||
|
required,
|
||||||
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
CreateButton,
|
||||||
|
ExportButton,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
sanitizeListRestProps,
|
sanitizeListRestProps,
|
||||||
NumberField,
|
NumberField,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import SaveQrButton from "./SaveQrButton";
|
import { Link } from "react-router-dom";
|
||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
||||||
import { DeviceRemoveButton } from "./devices";
|
import { DeviceRemoveButton } from "./devices";
|
||||||
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
|
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
const redirect = () => {
|
const redirect = () => {
|
||||||
return {
|
return {
|
||||||
pathname: "/importcsv",
|
pathname: "/import_users",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,6 +72,25 @@ 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,
|
||||||
@@ -144,7 +164,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"
|
||||||
undoable={false}
|
mutationMode="pessimistic"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -175,53 +195,29 @@ export const UserList = props => {
|
|||||||
<TextField source="displayname" />
|
<TextField source="displayname" />
|
||||||
<BooleanField source="is_guest" />
|
<BooleanField source="is_guest" />
|
||||||
<BooleanField source="admin" />
|
<BooleanField source="admin" />
|
||||||
<SelectField
|
|
||||||
source="user_type"
|
|
||||||
choices={[
|
|
||||||
{ id: null, name: "resources.users.type.default" },
|
|
||||||
{ id: "free", name: "resources.users.type.free" },
|
|
||||||
{ id: "limited", name: "resources.users.type.limited" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<BooleanField source="deactivated" />
|
<BooleanField source="deactivated" />
|
||||||
|
<DateField
|
||||||
|
source="creation_ts"
|
||||||
|
label="resources.users.fields.creation_ts_ms"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
/>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// redirect to the related Author show page
|
|
||||||
const redirectToPdf = (basePath, id, data) => {
|
|
||||||
return {
|
|
||||||
pathname: "/showpdf",
|
|
||||||
state: {
|
|
||||||
id: data.id,
|
|
||||||
displayname: data.displayname,
|
|
||||||
password: data.password,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserCreateToolbar = props => (
|
|
||||||
<Toolbar {...props}>
|
|
||||||
<SaveQrButton
|
|
||||||
label="synapseadmin.action.save_and_show"
|
|
||||||
redirect={redirectToPdf}
|
|
||||||
submitOnEnter={true}
|
|
||||||
/>
|
|
||||||
<SaveButton
|
|
||||||
label="synapseadmin.action.save_only"
|
|
||||||
redirect="list"
|
|
||||||
submitOnEnter={false}
|
|
||||||
variant="text"
|
|
||||||
/>
|
|
||||||
</Toolbar>
|
|
||||||
);
|
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/appendices#user-identifiers
|
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||||||
const validateUser = regex(
|
// here only local part of user_id
|
||||||
/^@[a-z0-9._=\-/]+:.*/,
|
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
|
||||||
"synapseadmin.users.invalid_user_id"
|
// localStorage.getItem("home_server").length is not valid here
|
||||||
);
|
const validateUser = [
|
||||||
|
required(),
|
||||||
|
maxLength(253),
|
||||||
|
regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const validateAddress = [required(), maxLength(255)];
|
||||||
|
|
||||||
export function generateRandomUser() {
|
export function generateRandomUser() {
|
||||||
const homeserver = localStorage.getItem("home_server");
|
const homeserver = localStorage.getItem("home_server");
|
||||||
@@ -264,65 +260,78 @@ 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 (
|
||||||
<Toolbar {...props}>
|
<TopToolbar>
|
||||||
<SaveQrButton
|
{!userStatus && <ServerNoticeButton record={data} />}
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
<ServerNoticeButton />
|
</TopToolbar>
|
||||||
</Toolbar>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserCreate = props => (
|
export const UserCreate = props => (
|
||||||
<Create record={generateRandomUser()} {...props}>
|
<Create {...props}>
|
||||||
<SimpleForm toolbar={<UserCreateToolbar />}>
|
<SimpleForm>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" validate={maxLength(256)} />
|
||||||
<PasswordInput source="password" autoComplete="new-password" />
|
<PasswordInput
|
||||||
<BooleanInput source="admin" />
|
source="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
validate={maxLength(512)}
|
||||||
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
source="user_type"
|
source="user_type"
|
||||||
choices={[
|
choices={choices_type}
|
||||||
{ id: null, name: "resources.users.type.default" },
|
translateChoice={false}
|
||||||
{ id: "free", name: "resources.users.type.free" },
|
allowEmpty={true}
|
||||||
{ id: "limited", name: "resources.users.type.limited" },
|
resettable
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
<BooleanInput source="admin" />
|
||||||
<ArrayInput source="threepids">
|
<ArrayInput source="threepids">
|
||||||
<SimpleFormIterator>
|
<SimpleFormIterator disableReordering>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
source="medium"
|
source="medium"
|
||||||
choices={[
|
choices={choices_medium}
|
||||||
{ id: "email", name: "resources.users.email" },
|
validate={required()}
|
||||||
{ 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 = ({ record }) => {
|
const UserTitle = props => {
|
||||||
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
@@ -338,7 +347,7 @@ export const UserEdit = props => {
|
|||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Edit {...props} title={<UserTitle />}>
|
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||||
<FormTab
|
<FormTab
|
||||||
label={translate("resources.users.name", { smart_count: 1 })}
|
label={translate("resources.users.name", { smart_count: 1 })}
|
||||||
@@ -351,33 +360,24 @@ export const UserEdit = props => {
|
|||||||
/>
|
/>
|
||||||
<TextInput source="id" disabled />
|
<TextInput source="id" disabled />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
<PasswordInput source="password" autoComplete="new-password" />
|
<PasswordInput
|
||||||
|
source="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
helperText="resources.users.helper.password"
|
||||||
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
source="user_type"
|
source="user_type"
|
||||||
choices={[
|
choices={choices_type}
|
||||||
{ id: null, name: "resources.users.type.default" },
|
translateChoice={false}
|
||||||
{ id: "free", name: "resources.users.type.free" },
|
allowEmpty={true}
|
||||||
{ id: "limited", name: "resources.users.type.limited" },
|
resettable
|
||||||
]}
|
|
||||||
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
|
<DateField source="creation_ts_ms" showTime options={date_format} />
|
||||||
source="creation_ts_ms"
|
|
||||||
showTime
|
|
||||||
options={{
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField source="consent_version" />
|
<TextField source="consent_version" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
@@ -387,14 +387,8 @@ export const UserEdit = props => {
|
|||||||
path="threepid"
|
path="threepid"
|
||||||
>
|
>
|
||||||
<ArrayInput source="threepids">
|
<ArrayInput source="threepids">
|
||||||
<SimpleFormIterator>
|
<SimpleFormIterator disableReordering>
|
||||||
<SelectInput
|
<SelectInput source="medium" choices={choices_medium} />
|
||||||
source="medium"
|
|
||||||
choices={[
|
|
||||||
{ id: "email", name: "resources.users.email" },
|
|
||||||
{ id: "msisdn", name: "resources.users.msisdn" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<TextInput source="address" />
|
<TextInput source="address" />
|
||||||
</SimpleFormIterator>
|
</SimpleFormIterator>
|
||||||
</ArrayInput>
|
</ArrayInput>
|
||||||
@@ -405,16 +399,16 @@ export const UserEdit = props => {
|
|||||||
icon={<AssignmentIndIcon />}
|
icon={<AssignmentIndIcon />}
|
||||||
path="sso"
|
path="sso"
|
||||||
>
|
>
|
||||||
<ArrayField source="external_ids" label={false}>
|
<ArrayInput source="external_ids" label={false}>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<SimpleFormIterator disableReordering>
|
||||||
<TextField source="auth_provider" sortable={false} />
|
<TextInput source="auth_provider" validate={required()} />
|
||||||
<TextField
|
<TextInput
|
||||||
source="external_id"
|
source="external_id"
|
||||||
label="resources.users.fields.id"
|
label="resources.users.fields.id"
|
||||||
sortable={false}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Datagrid>
|
</SimpleFormIterator>
|
||||||
</ArrayField>
|
</ArrayInput>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab
|
<FormTab
|
||||||
@@ -434,14 +428,7 @@ export const UserEdit = props => {
|
|||||||
<DateField
|
<DateField
|
||||||
source="last_seen_ts"
|
source="last_seen_ts"
|
||||||
showTime
|
showTime
|
||||||
options={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
sortable={false}
|
sortable={false}
|
||||||
/>
|
/>
|
||||||
<DeviceRemoveButton />
|
<DeviceRemoveButton />
|
||||||
@@ -469,14 +456,7 @@ export const UserEdit = props => {
|
|||||||
<DateField
|
<DateField
|
||||||
source="last_seen"
|
source="last_seen"
|
||||||
showTime
|
showTime
|
||||||
options={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
sortable={false}
|
sortable={false}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -503,29 +483,11 @@ export const UserEdit = props => {
|
|||||||
sort={{ field: "created_ts", order: "DESC" }}
|
sort={{ field: "created_ts", order: "DESC" }}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<DateField
|
<DateField source="created_ts" showTime options={date_format} />
|
||||||
source="created_ts"
|
|
||||||
showTime
|
|
||||||
options={{
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DateField
|
<DateField
|
||||||
source="last_access_ts"
|
source="last_access_ts"
|
||||||
showTime
|
showTime
|
||||||
options={{
|
options={date_format}
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<TextField source="media_id" />
|
<TextField source="media_id" />
|
||||||
<NumberField source="media_length" />
|
<NumberField source="media_length" />
|
||||||
|
|||||||
+31
-38
@@ -10,27 +10,14 @@ 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:
|
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
||||||
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver",
|
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO" },
|
||||||
},
|
},
|
||||||
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",
|
||||||
@@ -105,14 +92,11 @@ const de = {
|
|||||||
with_error:
|
with_error:
|
||||||
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
|
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
|
||||||
simulated_only: "Import-Vorgang war nur simuliert",
|
simulated_only: "Import-Vorgang war nur simuliert",
|
||||||
for_print:
|
|
||||||
"%{smart_count} Eintrag zum Drucken verfügbar |||| %{smart_count} Einträge zum Drucken verfügbar",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
backtolist: "Zurück zur Liste",
|
|
||||||
name: "Benutzer",
|
name: "Benutzer",
|
||||||
email: "E-Mail",
|
email: "E-Mail",
|
||||||
msisdn: "Telefon",
|
msisdn: "Telefon",
|
||||||
@@ -137,20 +121,11 @@ const de = {
|
|||||||
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",
|
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",
|
||||||
@@ -158,11 +133,6 @@ 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",
|
||||||
@@ -171,8 +141,6 @@ 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",
|
||||||
@@ -387,6 +355,31 @@ 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,
|
||||||
@@ -407,7 +400,7 @@ const de = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
notification: {
|
notification: {
|
||||||
...germanMessages.ra.notifiaction,
|
...germanMessages.ra.notification,
|
||||||
logged_out: "Abgemeldet",
|
logged_out: "Abgemeldet",
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
|
|||||||
+37
-43
@@ -10,27 +10,13 @@ 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:
|
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
||||||
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver",
|
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO" },
|
||||||
},
|
},
|
||||||
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",
|
||||||
@@ -105,14 +91,11 @@ const en = {
|
|||||||
with_error:
|
with_error:
|
||||||
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
||||||
simulated_only: "Run was only simulated",
|
simulated_only: "Run was only simulated",
|
||||||
for_print:
|
|
||||||
"%{smart_count} entry available for printing |||| %{smart_count} entries available for printing",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
backtolist: "Back to list",
|
|
||||||
name: "User |||| Users",
|
name: "User |||| Users",
|
||||||
email: "Email",
|
email: "Email",
|
||||||
msisdn: "Phone",
|
msisdn: "Phone",
|
||||||
@@ -137,30 +120,16 @@ const en = {
|
|||||||
creation_ts_ms: "Creation timestamp",
|
creation_ts_ms: "Creation timestamp",
|
||||||
consent_version: "Consent version",
|
consent_version: "Consent version",
|
||||||
auth_provider: "Provider",
|
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",
|
||||||
@@ -169,8 +138,6 @@ 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",
|
||||||
@@ -209,10 +176,12 @@ const en = {
|
|||||||
},
|
},
|
||||||
unencrypted: "Unencrypted",
|
unencrypted: "Unencrypted",
|
||||||
},
|
},
|
||||||
erase: {
|
action: {
|
||||||
title: "Delete room",
|
erase: {
|
||||||
content:
|
title: "Delete room",
|
||||||
"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!",
|
content:
|
||||||
|
"Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: {
|
reports: {
|
||||||
@@ -228,7 +197,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 typ",
|
type: "event type",
|
||||||
content: {
|
content: {
|
||||||
msgtype: "content type",
|
msgtype: "content type",
|
||||||
body: "content",
|
body: "content",
|
||||||
@@ -376,13 +345,38 @@ 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;
|
export default en;
|
||||||
|
|||||||
+377
@@ -0,0 +1,377 @@
|
|||||||
|
import frenchMessages from "ra-language-french";
|
||||||
|
|
||||||
|
const fr = {
|
||||||
|
...frenchMessages,
|
||||||
|
synapseadmin: {
|
||||||
|
auth: {
|
||||||
|
base_url: "URL du serveur d’accueil",
|
||||||
|
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 l’authentification unique",
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
invalid_user_id:
|
||||||
|
"Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
||||||
|
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;
|
||||||
+2
-1
@@ -10,10 +10,12 @@ 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: {
|
||||||
@@ -98,7 +100,6 @@ const zh = {
|
|||||||
},
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
backtolist: "回到列表",
|
|
||||||
name: "用户",
|
name: "用户",
|
||||||
email: "邮箱",
|
email: "邮箱",
|
||||||
msisdn: "电话",
|
msisdn: "电话",
|
||||||
|
|||||||
@@ -2,20 +2,31 @@ import { fetchUtils } from "react-admin";
|
|||||||
|
|
||||||
const authProvider = {
|
const authProvider = {
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
login: ({ base_url, username, password }) => {
|
login: ({ base_url, username, password, loginToken }) => {
|
||||||
// force homeserver for protection in case the form is manipulated
|
// force homeserver for protection in case the form is manipulated
|
||||||
base_url = process.env.REACT_APP_SERVER || base_url;
|
base_url = process.env.REACT_APP_SERVER || base_url;
|
||||||
|
|
||||||
console.log("login ");
|
console.log("login ");
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(
|
||||||
type: "m.login.password",
|
Object.assign(
|
||||||
user: username,
|
{
|
||||||
password: password,
|
device_id: localStorage.getItem("device_id"),
|
||||||
device_id: localStorage.getItem("device_id"),
|
initial_device_display_name: "Synapse Admin",
|
||||||
initial_device_display_name: "Synapse Admin",
|
},
|
||||||
}),
|
loginToken
|
||||||
|
? {
|
||||||
|
type: "m.login.token",
|
||||||
|
token: loginToken,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "m.login.password",
|
||||||
|
user: username,
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// use the base_url from login instead of the well_known entry from the
|
// use the base_url from login instead of the well_known entry from the
|
||||||
|
|||||||
+96
-66
@@ -25,13 +25,6 @@ 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 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",
|
||||||
@@ -42,19 +35,22 @@ 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/${data.id}`,
|
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(
|
||||||
|
data.id
|
||||||
|
)}:${localStorage.getItem("home_server")}`,
|
||||||
body: data,
|
body: data,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
}),
|
}),
|
||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/deactivate/${params.id}`,
|
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
|
||||||
|
params.id
|
||||||
|
)}`,
|
||||||
body: { erase: true },
|
body: { erase: true },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
@@ -71,43 +67,11 @@ const resourceMap = {
|
|||||||
public: !!r.public,
|
public: !!r.public,
|
||||||
}),
|
}),
|
||||||
data: "rooms",
|
data: "rooms",
|
||||||
total: json => json.total_rooms,
|
total: json => {
|
||||||
create: data => ({
|
return json.total_rooms;
|
||||||
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/v1/rooms/${params.id}`,
|
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
|
||||||
body: { block: false },
|
body: { block: false },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -130,10 +94,12 @@ const resourceMap = {
|
|||||||
return json.total;
|
return json.total;
|
||||||
},
|
},
|
||||||
reference: id => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/${id}/devices`,
|
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
|
||||||
}),
|
}),
|
||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/${params.user_id}/devices/${params.id}`,
|
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
|
||||||
|
params.user_id
|
||||||
|
)}/devices/${params.id}`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
@@ -175,7 +141,7 @@ const resourceMap = {
|
|||||||
id: p.pushkey,
|
id: p.pushkey,
|
||||||
}),
|
}),
|
||||||
reference: id => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/users/${id}/pushers`,
|
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
|
||||||
}),
|
}),
|
||||||
data: "pushers",
|
data: "pushers",
|
||||||
total: json => {
|
total: json => {
|
||||||
@@ -187,7 +153,9 @@ const resourceMap = {
|
|||||||
id: jr,
|
id: jr,
|
||||||
}),
|
}),
|
||||||
reference: id => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/users/${id}/joined_rooms`,
|
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
|
||||||
|
id
|
||||||
|
)}/joined_rooms`,
|
||||||
}),
|
}),
|
||||||
data: "joined_rooms",
|
data: "joined_rooms",
|
||||||
total: json => {
|
total: json => {
|
||||||
@@ -200,7 +168,7 @@ const resourceMap = {
|
|||||||
id: um.media_id,
|
id: um.media_id,
|
||||||
}),
|
}),
|
||||||
reference: id => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/users/${id}/media`,
|
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
|
||||||
}),
|
}),
|
||||||
data: "media",
|
data: "media",
|
||||||
total: json => {
|
total: json => {
|
||||||
@@ -313,11 +281,59 @@ 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
|
||||||
if (value === null) {
|
// to reset user_type from user, it must be null
|
||||||
|
if (value === null && key !== "user_type") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
@@ -334,7 +350,15 @@ function getSearchOrder(order) {
|
|||||||
const dataProvider = {
|
const dataProvider = {
|
||||||
getList: (resource, params) => {
|
getList: (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const { user_id, name, guests, deactivated, search_term } = params.filter;
|
const {
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
guests,
|
||||||
|
deactivated,
|
||||||
|
search_term,
|
||||||
|
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;
|
||||||
@@ -344,8 +368,10 @@ 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),
|
||||||
};
|
};
|
||||||
@@ -364,16 +390,18 @@ const dataProvider = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getOne: (resource, params) => {
|
getOne: (resource, params) => {
|
||||||
console.log("getOne " + resource, params);
|
console.log("getOne " + resource);
|
||||||
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}/${params.id}`).then(({ json }) => ({
|
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then(
|
||||||
data: res.map(json),
|
({ json }) => ({
|
||||||
}));
|
data: res.map(json),
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getMany: (resource, params) => {
|
getMany: (resource, params) => {
|
||||||
@@ -385,7 +413,9 @@ const dataProvider = {
|
|||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
params.ids.map(id => jsonClient(`${endpoint_url}/${id}`))
|
params.ids.map(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 +455,10 @@ 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}/${params.data.id}`, {
|
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
}).then(({ json }) => ({
|
}).then(({ json }) => ({
|
||||||
data: res.map(json),
|
data: res.map(json),
|
||||||
}));
|
}));
|
||||||
@@ -446,10 +473,13 @@ const dataProvider = {
|
|||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
params.ids.map(id => jsonClient(`${endpoint_url}/${id}`), {
|
params.ids.map(
|
||||||
method: "PUT",
|
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
|
||||||
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),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user