Compare commits
3 Commits
master
...
fix_docker
Author | SHA1 | Date | |
---|---|---|---|
|
dd71a08fe9 | ||
|
137a5828df | ||
|
c6ed88176d |
8
.github/workflows/build-test.yml
vendored
8
.github/workflows/build-test.yml
vendored
@ -10,12 +10,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: 16
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn --immutable
|
run: yarn --frozen-lockfile
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
56
.github/workflows/docker-release.yml
vendored
56
.github/workflows/docker-release.yml
vendored
@ -1,5 +1,4 @@
|
|||||||
name: Create docker image(s) and push to docker hub and ghcr.io
|
name: Create docker image(s) and push to docker hub
|
||||||
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -14,50 +13,39 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
name: Push Docker image to multiple registries
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
fetch-tags: true
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Calculate docker image tag
|
||||||
- name: Login to GHCR
|
id: set-tag
|
||||||
uses: docker/login-action@v3
|
run: |
|
||||||
with:
|
case "${GITHUB_REF}" in
|
||||||
registry: ghcr.io
|
refs/heads/master|refs/heads/main)
|
||||||
username: ${{ github.actor }}
|
tag=latest
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
;;
|
||||||
|
refs/tags/*)
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
tag=${GITHUB_REF#refs/tags/}
|
||||||
id: meta
|
;;
|
||||||
uses: docker/metadata-action@v5
|
*)
|
||||||
with:
|
tag=${GITHUB_SHA}
|
||||||
images: |
|
;;
|
||||||
awesometechnologies/synapse-admin
|
esac
|
||||||
ghcr.io/${{ github.repository }}
|
echo "::set-output name=tag::$tag"
|
||||||
|
|
||||||
- name: Build and Push Tag
|
- name: Build and Push Tag
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
12
.github/workflows/edge_ghpage.yml
vendored
12
.github/workflows/edge_ghpage.yml
vendored
@ -10,19 +10,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout 🛎️
|
- name: Checkout 🛎️
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
fetch-tags: true
|
node-version: "16"
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
- name: Install and Build 🔧
|
- name: Install and Build 🔧
|
||||||
run: |
|
run: |
|
||||||
yarn install --immutable
|
yarn install
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.5.0
|
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||||
with:
|
with:
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
folder: build
|
folder: build
|
||||||
|
12
.github/workflows/github-release.yml
vendored
12
.github/workflows/github-release.yml
vendored
@ -13,20 +13,18 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
fetch-tags: true
|
node-version: "16"
|
||||||
- uses: actions/setup-node@v4
|
- run: yarn install
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
- run: yarn install --immutable
|
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: |
|
- run: |
|
||||||
version=`git describe --dirty --tags || echo unknown`
|
version=`git describe --dirty --tags || echo unknown`
|
||||||
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@3198ee18f814cdf787321b4a32a26ddbf37acc52
|
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
|
||||||
with:
|
with:
|
||||||
files: dist/*.tar.gz
|
files: dist/*.tar.gz
|
||||||
env:
|
env:
|
||||||
|
10
.github/workflows/test-docker-image.yml
vendored
10
.github/workflows/test-docker-image.yml
vendored
@ -17,13 +17,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
echo "::set-output name=tag::$tag"
|
echo "::set-output name=tag::$tag"
|
||||||
- name: Build and Push Tag
|
- name: Build and Push Tag
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
|
6
.travis.yml
Normal file
6
.travis.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
dist: focal
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 17
|
||||||
|
|
||||||
|
cache: yarn
|
@ -1,13 +1,15 @@
|
|||||||
# Builder
|
# Builder
|
||||||
FROM node:lts as builder
|
FROM node:lts as builder
|
||||||
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
|
||||||
|
ARG PUBLIC_URL=/
|
||||||
ARG REACT_APP_SERVER
|
ARG REACT_APP_SERVER
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src
|
COPY . /src
|
||||||
RUN yarn --network-timeout=300000 install --immutable
|
RUN yarn --network-timeout 500000 add @mui/icons-material
|
||||||
RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
RUN yarn --network-timeout=100000 install
|
||||||
|
RUN PUBLIC_URL=$PUBLIC_URL REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
||||||
|
|
||||||
|
|
||||||
# App
|
# App
|
||||||
|
@ -13,10 +13,10 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
|
|||||||
|
|
||||||
### Supported Synapse
|
### Supported Synapse
|
||||||
|
|
||||||
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.52.0 for all functions to work as expected!
|
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://element-hq.github.io/synapse/latest/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.
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ You need access to the following endpoints:
|
|||||||
- `/_matrix`
|
- `/_matrix`
|
||||||
- `/_synapse/admin`
|
- `/_synapse/admin`
|
||||||
|
|
||||||
See also [Synapse administration endpoints](https://element-hq.github.io/synapse/latest/reverse_proxy.html#synapse-administration-endpoints)
|
See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints)
|
||||||
|
|
||||||
### Use without install
|
### Use without install
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ services:
|
|||||||
# 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.
|
||||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||||
# default is .
|
# default is /
|
||||||
# - PUBLIC_URL=/synapse-admin
|
# - PUBLIC_URL=/synapse-admin
|
||||||
# You can use a fixed homeserver, so that the user can no longer
|
# You can use a fixed homeserver, so that the user can no longer
|
||||||
# define it himself
|
# define it himself
|
||||||
|
38
package.json
38
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "0.9.2",
|
"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,30 +10,30 @@
|
|||||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^15.0.2",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^3.2.5"
|
"prettier": "^2.2.0",
|
||||||
|
"ra-test": "^3.15.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mui/icons-material": "^5.15.15",
|
"@emotion/react": "^11.7.1",
|
||||||
"@mui/material": "^5.15.15",
|
"@emotion/styled": "^11.6.0",
|
||||||
"@mui/styles": "^5.15.15",
|
"@mui/icons-material": "^5.3.1",
|
||||||
"papaparse": "^5.4.1",
|
"@mui/material": "^5.4.0",
|
||||||
|
"papaparse": "^5.2.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
"ra-language-chinese": "^2.0.10",
|
"ra-language-chinese": "^2.0.10",
|
||||||
"ra-language-french": "^4.16.15",
|
"ra-language-french": "^4.2.0",
|
||||||
"ra-language-german": "^3.13.4",
|
"ra-language-german": "^3.13.4",
|
||||||
"ra-language-italian": "^3.13.1",
|
"react": "^17.0.0",
|
||||||
"ra-language-farsi": "^4.2.0",
|
"react-admin": "^3.19.7",
|
||||||
"ra-language-russian": "^4.14.2",
|
"react-dom": "^17.0.2",
|
||||||
"react": "^18.0.0",
|
|
||||||
"react-admin": "^4.16.15",
|
|
||||||
"react-dom": "^18.0.0",
|
|
||||||
"react-scripts": "^5.0.1"
|
"react-scripts": "^5.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
id,displayname,password,is_guest,admin,deactivated
|
id,displayname,password,is_guest,admin,deactivated
|
||||||
testuser22,Jane Doe,secretpassword,false,true,false
|
@testuser22:example.org,Jane Doe,secretpassword,false,true,false
|
||||||
,John Doe,,false,false,false
|
,John Doe,,false,false,false
|
||||||
|
|
@ -1,3 +1,2 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /
|
|
||||||
|
105
src/App.js
Normal file
105
src/App.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Admin, Resource, resolveBrowserLocale } from "react-admin";
|
||||||
|
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||||
|
import authProvider from "./synapse/authProvider";
|
||||||
|
import dataProvider from "./synapse/dataProvider";
|
||||||
|
import { UserList, UserCreate, UserEdit } from "./components/users";
|
||||||
|
import { RoomList, RoomShow } from "./components/rooms";
|
||||||
|
import { ReportList, ReportShow } from "./components/EventReports";
|
||||||
|
import LoginPage from "./components/LoginPage";
|
||||||
|
import ConfirmationNumberIcon from "@mui/icons-material/ConfirmationNumber";
|
||||||
|
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 RoomIcon from "@mui/icons-material/ViewList";
|
||||||
|
import ReportIcon from "@mui/icons-material/Warning";
|
||||||
|
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 { Route } from "react-router-dom";
|
||||||
|
import germanMessages from "./i18n/de";
|
||||||
|
import englishMessages from "./i18n/en";
|
||||||
|
import frenchMessages from "./i18n/fr";
|
||||||
|
import chineseMessages from "./i18n/zh";
|
||||||
|
|
||||||
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
|
const messages = {
|
||||||
|
de: germanMessages,
|
||||||
|
en: englishMessages,
|
||||||
|
fr: frenchMessages,
|
||||||
|
zh: chineseMessages,
|
||||||
|
};
|
||||||
|
const i18nProvider = polyglotI18nProvider(
|
||||||
|
locale => (messages[locale] ? messages[locale] : messages.en),
|
||||||
|
resolveBrowserLocale()
|
||||||
|
);
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<Admin
|
||||||
|
disableTelemetry
|
||||||
|
loginPage={LoginPage}
|
||||||
|
authProvider={authProvider}
|
||||||
|
dataProvider={dataProvider}
|
||||||
|
i18nProvider={i18nProvider}
|
||||||
|
customRoutes={[
|
||||||
|
<Route key="userImport" path="/import_users" component={ImportFeature} />,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Resource
|
||||||
|
name="users"
|
||||||
|
list={UserList}
|
||||||
|
create={UserCreate}
|
||||||
|
edit={UserEdit}
|
||||||
|
icon={UserIcon}
|
||||||
|
/>
|
||||||
|
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} />
|
||||||
|
<Resource
|
||||||
|
name="user_media_statistics"
|
||||||
|
list={UserMediaStatsList}
|
||||||
|
icon={EqualizerIcon}
|
||||||
|
/>
|
||||||
|
<Resource
|
||||||
|
name="reports"
|
||||||
|
list={ReportList}
|
||||||
|
show={ReportShow}
|
||||||
|
icon={ReportIcon}
|
||||||
|
/>
|
||||||
|
<Resource
|
||||||
|
name="room_directory"
|
||||||
|
list={RoomDirectoryList}
|
||||||
|
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="devices" />
|
||||||
|
<Resource name="room_members" />
|
||||||
|
<Resource name="users_media" />
|
||||||
|
<Resource name="joined_rooms" />
|
||||||
|
<Resource name="pushers" />
|
||||||
|
<Resource name="servernotices" />
|
||||||
|
<Resource name="forward_extremities" />
|
||||||
|
<Resource name="room_state" />
|
||||||
|
<Resource name="destination_rooms" />
|
||||||
|
</Admin>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default App;
|
72
src/App.jsx
72
src/App.jsx
@ -1,72 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Admin,
|
|
||||||
CustomRoutes,
|
|
||||||
Resource,
|
|
||||||
resolveBrowserLocale,
|
|
||||||
} from "react-admin";
|
|
||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
|
||||||
import authProvider from "./synapse/authProvider";
|
|
||||||
import dataProvider from "./synapse/dataProvider";
|
|
||||||
import users from "./components/users";
|
|
||||||
import rooms from "./components/rooms";
|
|
||||||
import userMediaStats from "./components/statistics";
|
|
||||||
import reports from "./components/EventReports";
|
|
||||||
import roomDirectory from "./components/RoomDirectory";
|
|
||||||
import destinations from "./components/destinations";
|
|
||||||
import LoginPage from "./components/LoginPage";
|
|
||||||
import { ImportFeature } from "./components/ImportFeature";
|
|
||||||
import { Route } from "react-router-dom";
|
|
||||||
import germanMessages from "./i18n/de";
|
|
||||||
import englishMessages from "./i18n/en";
|
|
||||||
import frenchMessages from "./i18n/fr";
|
|
||||||
import chineseMessages from "./i18n/zh";
|
|
||||||
import italianMessages from "./i18n/it";
|
|
||||||
import russianMessages from "./i18n/ru";
|
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
|
||||||
const messages = {
|
|
||||||
de: germanMessages,
|
|
||||||
en: englishMessages,
|
|
||||||
fr: frenchMessages,
|
|
||||||
it: italianMessages,
|
|
||||||
zh: chineseMessages,
|
|
||||||
ru: russianMessages
|
|
||||||
};
|
|
||||||
const i18nProvider = polyglotI18nProvider(
|
|
||||||
locale => (messages[locale] ? messages[locale] : messages.en),
|
|
||||||
resolveBrowserLocale()
|
|
||||||
);
|
|
||||||
|
|
||||||
const App = () => (
|
|
||||||
<Admin
|
|
||||||
disableTelemetry
|
|
||||||
requireAuth
|
|
||||||
loginPage={LoginPage}
|
|
||||||
authProvider={authProvider}
|
|
||||||
dataProvider={dataProvider}
|
|
||||||
i18nProvider={i18nProvider}
|
|
||||||
>
|
|
||||||
<CustomRoutes>
|
|
||||||
<Route path="/import_users" element={<ImportFeature />} />
|
|
||||||
</CustomRoutes>
|
|
||||||
<Resource {...users} />
|
|
||||||
<Resource {...rooms} />
|
|
||||||
<Resource {...userMediaStats} />
|
|
||||||
<Resource {...reports} />
|
|
||||||
<Resource {...roomDirectory} />
|
|
||||||
<Resource {...destinations} />
|
|
||||||
<Resource name="connections" />
|
|
||||||
<Resource name="devices" />
|
|
||||||
<Resource name="room_members" />
|
|
||||||
<Resource name="users_media" />
|
|
||||||
<Resource name="joined_rooms" />
|
|
||||||
<Resource name="pushers" />
|
|
||||||
<Resource name="servernotices" />
|
|
||||||
<Resource name="forward_extremities" />
|
|
||||||
<Resource name="room_state" />
|
|
||||||
<Resource name="destination_rooms" />
|
|
||||||
</Admin>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default App;
|
|
9
src/App.test.js
Normal file
9
src/App.test.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
describe("App", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
render(<App />);
|
||||||
|
});
|
||||||
|
});
|
@ -1,10 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
describe("App", () => {
|
|
||||||
it("renders", async () => {
|
|
||||||
render(<App />);
|
|
||||||
await screen.findAllByText("Welcome to Synapse-admin");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,22 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import get from "lodash/get";
|
|
||||||
import { Avatar } from "@mui/material";
|
|
||||||
import { useRecordContext } from "react-admin";
|
|
||||||
|
|
||||||
const AvatarField = ({ source, ...rest }) => {
|
|
||||||
const record = useRecordContext(rest);
|
|
||||||
const src = get(record, source)?.toString();
|
|
||||||
const { alt, classes, sizes, sx, variant } = rest;
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
alt={alt}
|
|
||||||
classes={classes}
|
|
||||||
sizes={sizes}
|
|
||||||
src={src}
|
|
||||||
sx={sx}
|
|
||||||
variant={variant}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AvatarField;
|
|
@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { RecordContextProvider } from "react-admin";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import AvatarField from "./AvatarField";
|
|
||||||
|
|
||||||
describe("AvatarField", () => {
|
|
||||||
it("shows image", () => {
|
|
||||||
const value = {
|
|
||||||
avatar: "foo",
|
|
||||||
};
|
|
||||||
render(
|
|
||||||
<RecordContextProvider value={value}>
|
|
||||||
<AvatarField source="avatar" />
|
|
||||||
</RecordContextProvider>
|
|
||||||
);
|
|
||||||
expect(screen.getByRole("img").getAttribute("src")).toBe("foo");
|
|
||||||
});
|
|
||||||
});
|
|
@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
DeleteButton,
|
|
||||||
List,
|
List,
|
||||||
NumberField,
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
@ -11,12 +10,9 @@ import {
|
|||||||
Tab,
|
Tab,
|
||||||
TabbedShowLayout,
|
TabbedShowLayout,
|
||||||
TextField,
|
TextField,
|
||||||
TopToolbar,
|
|
||||||
useRecordContext,
|
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import PageviewIcon from "@mui/icons-material/Pageview";
|
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||||
import ReportIcon from "@mui/icons-material/Warning";
|
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
|
||||||
const date_format = {
|
const date_format = {
|
||||||
@ -28,14 +24,14 @@ const date_format = {
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReportPagination = () => (
|
const ReportPagination = props => (
|
||||||
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ReportShow = props => {
|
export const ReportShow = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Show {...props} actions={<ReportShowActions />}>
|
<Show {...props}>
|
||||||
<TabbedShowLayout>
|
<TabbedShowLayout>
|
||||||
<Tab
|
<Tab
|
||||||
label={translate("synapseadmin.reports.tabs.basic", {
|
label={translate("synapseadmin.reports.tabs.basic", {
|
||||||
@ -83,7 +79,6 @@ export const ReportShow = props => {
|
|||||||
<ReferenceField source="sender" reference="users">
|
<ReferenceField source="sender" reference="users">
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<TextField source="sender" label="Sender (raw user ID)" />
|
|
||||||
<TextField source="event_id" />
|
<TextField source="event_id" />
|
||||||
<TextField source="event_json.origin" />
|
<TextField source="event_json.origin" />
|
||||||
<TextField source="event_json.type" />
|
<TextField source="event_json.type" />
|
||||||
@ -94,7 +89,7 @@ export const ReportShow = props => {
|
|||||||
<TextField source="event_json.content.algorithm" />
|
<TextField source="event_json.content.algorithm" />
|
||||||
<TextField
|
<TextField
|
||||||
source="event_json.content.device_id"
|
source="event_json.content.device_id"
|
||||||
label="resources.devices.fields.device_id"
|
label="resources.users.fields.device_id"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabbedShowLayout>
|
</TabbedShowLayout>
|
||||||
@ -102,28 +97,15 @@ export const ReportShow = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReportShowActions = () => {
|
export const ReportList = ({ ...props }) => {
|
||||||
const record = useRecordContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
|
||||||
<DeleteButton
|
|
||||||
record={record}
|
|
||||||
mutationMode="pessimistic"
|
|
||||||
confirmTitle="resources.reports.action.erase.title"
|
|
||||||
confirmContent="resources.reports.action.erase.content"
|
|
||||||
/>
|
|
||||||
</TopToolbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ReportList = props => (
|
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
pagination={<ReportPagination />}
|
pagination={<ReportPagination />}
|
||||||
sort={{ field: "received_ts", order: "DESC" }}
|
sort={{ field: "received_ts", order: "DESC" }}
|
||||||
|
bulkActionButtons={false}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick="show" bulkActionButtons={false}>
|
<Datagrid rowClick="show">
|
||||||
<TextField source="id" sortable={false} />
|
<TextField source="id" sortable={false} />
|
||||||
<DateField
|
<DateField
|
||||||
source="received_ts"
|
source="received_ts"
|
||||||
@ -136,13 +118,5 @@ export const ReportList = props => (
|
|||||||
<TextField sortable={false} source="score" />
|
<TextField sortable={false} source="score" />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
const resource = {
|
|
||||||
name: "reports",
|
|
||||||
icon: ReportIcon,
|
|
||||||
list: ReportList,
|
|
||||||
show: ReportShow,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default resource;
|
|
@ -1,6 +1,12 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useDataProvider, useNotify, Title } from "react-admin";
|
import {
|
||||||
|
Button as ReactAdminButton,
|
||||||
|
useDataProvider,
|
||||||
|
useNotify,
|
||||||
|
Title,
|
||||||
|
} from "react-admin";
|
||||||
import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
|
import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
|
||||||
|
import GetAppIcon from "@mui/icons-material/GetApp";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@ -17,6 +23,19 @@ import { generateRandomUser } from "./users";
|
|||||||
|
|
||||||
const LOGGING = true;
|
const LOGGING = true;
|
||||||
|
|
||||||
|
export const ImportButton = ({ label, variant = "text" }) => {
|
||||||
|
return (
|
||||||
|
<ReactAdminButton
|
||||||
|
color="primary"
|
||||||
|
component="span"
|
||||||
|
variant={variant}
|
||||||
|
label={label}
|
||||||
|
>
|
||||||
|
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
|
||||||
|
</ReactAdminButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const expectedFields = ["id", "displayname"].sort();
|
const expectedFields = ["id", "displayname"].sort();
|
||||||
const optionalFields = [
|
const optionalFields = [
|
||||||
"user_type",
|
"user_type",
|
||||||
@ -32,7 +51,7 @@ function TranslatableOption({ value, text }) {
|
|||||||
return <option value={value}>{translate(text)}</option>;
|
return <option value={value}>{translate(text)}</option>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilePicker = () => {
|
const FilePicker = props => {
|
||||||
const [values, setValues] = useState(null);
|
const [values, setValues] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
@ -191,7 +210,7 @@ const FilePicker = () => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runImport = async _e => {
|
const runImport = async e => {
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
notify("import_users.errors.already_in_progress");
|
notify("import_users.errors.already_in_progress");
|
||||||
return;
|
return;
|
||||||
@ -307,7 +326,7 @@ const FilePicker = () => {
|
|||||||
let retries = 0;
|
let retries = 0;
|
||||||
const submitRecord = recordData => {
|
const submitRecord = recordData => {
|
||||||
return dataProvider.getOne("users", { id: recordData.id }).then(
|
return dataProvider.getOne("users", { id: recordData.id }).then(
|
||||||
async _alreadyExists => {
|
async alreadyExists => {
|
||||||
if (LOGGING) console.log("already existed");
|
if (LOGGING) console.log("already existed");
|
||||||
|
|
||||||
if (useridMode === "update" || conflictMode === "skip") {
|
if (useridMode === "update" || conflictMode === "skip") {
|
||||||
@ -332,7 +351,7 @@ const FilePicker = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async _okToSubmit => {
|
async okToSubmit => {
|
||||||
if (LOGGING)
|
if (LOGGING)
|
||||||
console.log(
|
console.log(
|
||||||
"OK to create record " +
|
"OK to create record " +
|
375
src/components/LoginPage.js
Normal file
375
src/components/LoginPage.js
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
fetchUtils,
|
||||||
|
FormDataConsumer,
|
||||||
|
Notification,
|
||||||
|
useLogin,
|
||||||
|
useNotify,
|
||||||
|
useLocale,
|
||||||
|
useSetLocale,
|
||||||
|
useTranslate,
|
||||||
|
PasswordInput,
|
||||||
|
TextInput,
|
||||||
|
} from "react-admin";
|
||||||
|
import { Form, useForm } from "react-final-form";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardActions,
|
||||||
|
CircularProgress,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
main: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
minHeight: "calc(100vh - 1em)",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
background: "url(./images/floating-cogs.svg)",
|
||||||
|
backgroundColor: "#f9f9f9",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
minWidth: "30em",
|
||||||
|
marginTop: "6em",
|
||||||
|
marginBottom: "6em",
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
margin: "1em",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
marginTop: "1em",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
padding: "0 1em 1em 1em",
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
marginTop: "1em",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
padding: "0 1em 1em 1em",
|
||||||
|
},
|
||||||
|
serverVersion: {
|
||||||
|
color: "#9e9e9e",
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
marginBottom: "1em",
|
||||||
|
marginLeft: "0.5em",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const LoginPage = ({ theme }) => {
|
||||||
|
const classes = useStyles({ theme });
|
||||||
|
const login = useLogin();
|
||||||
|
const notify = useNotify();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||||
|
var locale = useLocale();
|
||||||
|
const setLocale = useSetLocale();
|
||||||
|
const translate = useTranslate();
|
||||||
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
const cfg_base_url = process.env.REACT_APP_SERVER;
|
||||||
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
|
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||||
|
|
||||||
|
if (loginToken) {
|
||||||
|
const ssoToken = loginToken[1];
|
||||||
|
console.log("SSO token is", ssoToken);
|
||||||
|
// Prevent further requests
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
window.location.href.replace(loginToken[0], "#").split("#")[0]
|
||||||
|
);
|
||||||
|
const baseUrl = localStorage.getItem("sso_base_url");
|
||||||
|
localStorage.removeItem("sso_base_url");
|
||||||
|
if (baseUrl) {
|
||||||
|
const auth = {
|
||||||
|
base_url: baseUrl,
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
loginToken: ssoToken,
|
||||||
|
};
|
||||||
|
console.log("Base URL is:", baseUrl);
|
||||||
|
console.log("SSO Token is:", ssoToken);
|
||||||
|
console.log("Let's try token login...");
|
||||||
|
login(auth).catch(error => {
|
||||||
|
alert(
|
||||||
|
typeof error === "string"
|
||||||
|
? error
|
||||||
|
: typeof error === "undefined" || !error.message
|
||||||
|
? "ra.auth.sign_in_error"
|
||||||
|
: error.message
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderInput = ({
|
||||||
|
meta: { touched, error } = {},
|
||||||
|
input: { ...inputProps },
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<TextField
|
||||||
|
error={!!(touched && error)}
|
||||||
|
helperText={touched && error}
|
||||||
|
{...inputProps}
|
||||||
|
{...props}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const validate = values => {
|
||||||
|
const errors = {};
|
||||||
|
if (!values.username) {
|
||||||
|
errors.username = translate("ra.validation.required");
|
||||||
|
}
|
||||||
|
if (!values.password) {
|
||||||
|
errors.password = translate("ra.validation.required");
|
||||||
|
}
|
||||||
|
if (!values.base_url) {
|
||||||
|
errors.base_url = translate("ra.validation.required");
|
||||||
|
} else {
|
||||||
|
if (!values.base_url.match(/^(http|https):\/\//)) {
|
||||||
|
errors.base_url = translate("synapseadmin.auth.protocol_error");
|
||||||
|
} else if (
|
||||||
|
!values.base_url.match(
|
||||||
|
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
errors.base_url = translate("synapseadmin.auth.url_error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = auth => {
|
||||||
|
setLoading(true);
|
||||||
|
login(auth).catch(error => {
|
||||||
|
setLoading(false);
|
||||||
|
notify(
|
||||||
|
typeof error === "string"
|
||||||
|
? error
|
||||||
|
: typeof error === "undefined" || !error.message
|
||||||
|
? "ra.auth.sign_in_error"
|
||||||
|
: error.message,
|
||||||
|
{ 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 usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/;
|
||||||
|
if (!username) return null;
|
||||||
|
const res = username.match(usernameRegex);
|
||||||
|
if (res) return res[1];
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserData = ({ formData }) => {
|
||||||
|
const form = useForm();
|
||||||
|
const [serverVersion, setServerVersion] = useState("");
|
||||||
|
|
||||||
|
const handleUsernameChange = _ => {
|
||||||
|
if (formData.base_url || cfg_base_url) return;
|
||||||
|
// check if username is a full qualified userId then set base_url accordially
|
||||||
|
const home_server = extractHomeServer(formData.username);
|
||||||
|
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
|
||||||
|
if (home_server) {
|
||||||
|
// fetch .well-known entry to get base_url
|
||||||
|
fetchUtils
|
||||||
|
.fetchJson(wellKnownUrl, { method: "GET" })
|
||||||
|
.then(({ json }) => {
|
||||||
|
form.change("base_url", json["m.homeserver"].base_url);
|
||||||
|
})
|
||||||
|
.catch(_ => {
|
||||||
|
// if there is no .well-known entry, try the home server name
|
||||||
|
form.change("base_url", `https://${home_server}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
_ => {
|
||||||
|
if (
|
||||||
|
!formData.base_url ||
|
||||||
|
!formData.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+$/)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const versionUrl = `${formData.base_url}/_synapse/admin/v1/server_version`;
|
||||||
|
fetchUtils
|
||||||
|
.fetchJson(versionUrl, { method: "GET" })
|
||||||
|
.then(({ json }) => {
|
||||||
|
setServerVersion(
|
||||||
|
`${translate("synapseadmin.auth.server_version")} ${
|
||||||
|
json["server_version"]
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(_ => {
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={classes.input}>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
name="username"
|
||||||
|
component={renderInput}
|
||||||
|
label="ra.auth.username"
|
||||||
|
disabled={loading || !supportPassAuth}
|
||||||
|
onBlur={handleUsernameChange}
|
||||||
|
resettable
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.input}>
|
||||||
|
<PasswordInput
|
||||||
|
name="password"
|
||||||
|
component={renderInput}
|
||||||
|
label="ra.auth.password"
|
||||||
|
type="password"
|
||||||
|
disabled={loading || !supportPassAuth}
|
||||||
|
resettable
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.input}>
|
||||||
|
<TextInput
|
||||||
|
name="base_url"
|
||||||
|
component={renderInput}
|
||||||
|
label="synapseadmin.auth.base_url"
|
||||||
|
disabled={cfg_base_url || loading}
|
||||||
|
resettable
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.serverVersion}>{serverVersion}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
initialValues={{ base_url: cfg_base_url || base_url }}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validate={validate}
|
||||||
|
render={({ handleSubmit }) => (
|
||||||
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
|
<div className={classes.main}>
|
||||||
|
<Card className={classes.card}>
|
||||||
|
<div className={classes.avatar}>
|
||||||
|
<Avatar className={classes.icon}>
|
||||||
|
<LockIcon />
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
<div className={classes.hint}>
|
||||||
|
{translate("synapseadmin.auth.welcome")}
|
||||||
|
</div>
|
||||||
|
<div className={classes.form}>
|
||||||
|
<div className={classes.input}>
|
||||||
|
<Select
|
||||||
|
value={locale}
|
||||||
|
onChange={e => {
|
||||||
|
setLocale(e.target.value);
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<MenuItem value="de">Deutsch</MenuItem>
|
||||||
|
<MenuItem value="en">English</MenuItem>
|
||||||
|
<MenuItem value="fr">Français</MenuItem>
|
||||||
|
<MenuItem value="zh">简体中文</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<FormDataConsumer>
|
||||||
|
{formDataProps => <UserData {...formDataProps} />}
|
||||||
|
</FormDataConsumer>
|
||||||
|
</div>
|
||||||
|
<CardActions className={classes.actions}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
disabled={loading || !supportPassAuth}
|
||||||
|
className={classes.button}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{loading && <CircularProgress size={25} thickness={2} />}
|
||||||
|
{translate("ra.auth.sign_in")}
|
||||||
|
</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>
|
||||||
|
</Card>
|
||||||
|
<Notification />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
@ -1,329 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormDataConsumer,
|
|
||||||
Notification,
|
|
||||||
required,
|
|
||||||
useLogin,
|
|
||||||
useNotify,
|
|
||||||
useLocaleState,
|
|
||||||
useTranslate,
|
|
||||||
PasswordInput,
|
|
||||||
TextInput,
|
|
||||||
} from "react-admin";
|
|
||||||
import { useFormContext } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardActions,
|
|
||||||
CircularProgress,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
|
||||||
import {
|
|
||||||
getServerVersion,
|
|
||||||
getSupportedFeatures,
|
|
||||||
getSupportedLoginFlows,
|
|
||||||
getWellKnownUrl,
|
|
||||||
isValidBaseUrl,
|
|
||||||
splitMxid,
|
|
||||||
} from "../synapse/synapse";
|
|
||||||
|
|
||||||
const FormBox = styled(Box)(({ theme }) => ({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
minHeight: "calc(100vh - 1rem)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
background: "url(./images/floating-cogs.svg)",
|
|
||||||
backgroundColor: "#f9f9f9",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
|
|
||||||
[`& .card`]: {
|
|
||||||
width: "30rem",
|
|
||||||
marginTop: "6rem",
|
|
||||||
marginBottom: "6rem",
|
|
||||||
},
|
|
||||||
[`& .avatar`]: {
|
|
||||||
margin: "1rem",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
[`& .icon`]: {
|
|
||||||
backgroundColor: theme.palette.grey[500],
|
|
||||||
},
|
|
||||||
[`& .hint`]: {
|
|
||||||
marginTop: "1em",
|
|
||||||
marginBottom: "1em",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: theme.palette.grey[600],
|
|
||||||
},
|
|
||||||
[`& .form`]: {
|
|
||||||
padding: "0 1rem 1rem 1rem",
|
|
||||||
},
|
|
||||||
[`& .select`]: {
|
|
||||||
marginBottom: "2rem",
|
|
||||||
},
|
|
||||||
[`& .actions`]: {
|
|
||||||
padding: "0 1rem 1rem 1rem",
|
|
||||||
},
|
|
||||||
[`& .serverVersion`]: {
|
|
||||||
color: theme.palette.grey[500],
|
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
marginLeft: "0.5rem",
|
|
||||||
},
|
|
||||||
[`& .matrixVersions`]: {
|
|
||||||
color: theme.palette.grey[500],
|
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
marginLeft: "0.5rem",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const LoginPage = () => {
|
|
||||||
const login = useLogin();
|
|
||||||
const notify = useNotify();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
|
||||||
const [locale, setLocale] = useLocaleState();
|
|
||||||
const translate = useTranslate();
|
|
||||||
const base_url = localStorage.getItem("base_url");
|
|
||||||
const cfg_base_url = process.env.REACT_APP_SERVER;
|
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
|
||||||
|
|
||||||
if (loginToken) {
|
|
||||||
const ssoToken = loginToken[1];
|
|
||||||
console.log("SSO token is", ssoToken);
|
|
||||||
// Prevent further requests
|
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
"",
|
|
||||||
window.location.href.replace(loginToken[0], "#").split("#")[0]
|
|
||||||
);
|
|
||||||
const baseUrl = localStorage.getItem("sso_base_url");
|
|
||||||
localStorage.removeItem("sso_base_url");
|
|
||||||
if (baseUrl) {
|
|
||||||
const auth = {
|
|
||||||
base_url: baseUrl,
|
|
||||||
username: null,
|
|
||||||
password: null,
|
|
||||||
loginToken: ssoToken,
|
|
||||||
};
|
|
||||||
console.log("Base URL is:", baseUrl);
|
|
||||||
console.log("SSO Token is:", ssoToken);
|
|
||||||
console.log("Let's try token login...");
|
|
||||||
login(auth).catch(error => {
|
|
||||||
alert(
|
|
||||||
typeof error === "string"
|
|
||||||
? error
|
|
||||||
: typeof error === "undefined" || !error.message
|
|
||||||
? "ra.auth.sign_in_error"
|
|
||||||
: error.message
|
|
||||||
);
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateBaseUrl = value => {
|
|
||||||
if (!value.match(/^(http|https):\/\//)) {
|
|
||||||
return translate("synapseadmin.auth.protocol_error");
|
|
||||||
} else if (
|
|
||||||
!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)
|
|
||||||
) {
|
|
||||||
return translate("synapseadmin.auth.url_error");
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = auth => {
|
|
||||||
setLoading(true);
|
|
||||||
login(auth).catch(error => {
|
|
||||||
setLoading(false);
|
|
||||||
notify(
|
|
||||||
typeof error === "string"
|
|
||||||
? error
|
|
||||||
: typeof error === "undefined" || !error.message
|
|
||||||
? "ra.auth.sign_in_error"
|
|
||||||
: error.message,
|
|
||||||
{ 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 UserData = ({ formData }) => {
|
|
||||||
const form = useFormContext();
|
|
||||||
const [serverVersion, setServerVersion] = useState("");
|
|
||||||
const [matrixVersions, setMatrixVersions] = useState("");
|
|
||||||
|
|
||||||
const handleUsernameChange = _ => {
|
|
||||||
if (formData.base_url || cfg_base_url) return;
|
|
||||||
// check if username is a full qualified userId then set base_url accordingly
|
|
||||||
const domain = splitMxid(formData.username)?.domain;
|
|
||||||
if (domain) {
|
|
||||||
getWellKnownUrl(domain).then(url => form.setValue("base_url", url));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isValidBaseUrl(formData.base_url)) return;
|
|
||||||
|
|
||||||
getServerVersion(formData.base_url)
|
|
||||||
.then(serverVersion =>
|
|
||||||
setServerVersion(
|
|
||||||
`${translate("synapseadmin.auth.server_version")} ${serverVersion}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.catch(() => setServerVersion(""));
|
|
||||||
|
|
||||||
getSupportedFeatures(formData.base_url)
|
|
||||||
.then(features =>
|
|
||||||
setMatrixVersions(
|
|
||||||
`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.catch(() => setMatrixVersions(""));
|
|
||||||
|
|
||||||
// Set SSO Url
|
|
||||||
getSupportedLoginFlows(formData.base_url)
|
|
||||||
.then(loginFlows => {
|
|
||||||
const supportPass =
|
|
||||||
loginFlows.find(f => f.type === "m.login.password") !== undefined;
|
|
||||||
const supportSSO =
|
|
||||||
loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
|
||||||
setSupportPassAuth(supportPass);
|
|
||||||
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
|
||||||
})
|
|
||||||
.catch(() => setSSOBaseUrl(""));
|
|
||||||
}, [formData.base_url]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box>
|
|
||||||
<TextInput
|
|
||||||
autoFocus
|
|
||||||
name="username"
|
|
||||||
label="ra.auth.username"
|
|
||||||
disabled={loading || !supportPassAuth}
|
|
||||||
onBlur={handleUsernameChange}
|
|
||||||
resettable
|
|
||||||
fullWidth
|
|
||||||
className="input"
|
|
||||||
validate={required()}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<PasswordInput
|
|
||||||
name="password"
|
|
||||||
label="ra.auth.password"
|
|
||||||
type="password"
|
|
||||||
disabled={loading || !supportPassAuth}
|
|
||||||
resettable
|
|
||||||
fullWidth
|
|
||||||
className="input"
|
|
||||||
validate={required()}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<TextInput
|
|
||||||
name="base_url"
|
|
||||||
label="synapseadmin.auth.base_url"
|
|
||||||
disabled={cfg_base_url || loading}
|
|
||||||
resettable
|
|
||||||
fullWidth
|
|
||||||
className="input"
|
|
||||||
validate={[required(), validateBaseUrl]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Typography className="serverVersion">{serverVersion}</Typography>
|
|
||||||
<Typography className="matrixVersions">{matrixVersions}</Typography>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
defaultValues={{ base_url: cfg_base_url || base_url }}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
mode="onTouched"
|
|
||||||
>
|
|
||||||
<FormBox>
|
|
||||||
<Card className="card">
|
|
||||||
<Box className="avatar">
|
|
||||||
{loading ? (
|
|
||||||
<CircularProgress size={25} thickness={2} />
|
|
||||||
) : (
|
|
||||||
<Avatar className="icon">
|
|
||||||
<LockIcon />
|
|
||||||
</Avatar>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
|
|
||||||
<Box className="form">
|
|
||||||
<Select
|
|
||||||
value={locale}
|
|
||||||
onChange={e => {
|
|
||||||
setLocale(e.target.value);
|
|
||||||
}}
|
|
||||||
fullWidth
|
|
||||||
disabled={loading}
|
|
||||||
className="select"
|
|
||||||
>
|
|
||||||
<MenuItem value="de">Deutsch</MenuItem>
|
|
||||||
<MenuItem value="en">English</MenuItem>
|
|
||||||
<MenuItem value="fr">Français</MenuItem>
|
|
||||||
<MenuItem value="it">Italiano</MenuItem>
|
|
||||||
<MenuItem value="zh">简体中文</MenuItem>
|
|
||||||
<MenuItem value="fa">Persian(فارسی)</MenuItem>
|
|
||||||
<MenuItem value="ru">Русский</MenuItem>
|
|
||||||
</Select>
|
|
||||||
<FormDataConsumer>
|
|
||||||
{formDataProps => <UserData {...formDataProps} />}
|
|
||||||
</FormDataConsumer>
|
|
||||||
<CardActions className="actions">
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
type="submit"
|
|
||||||
color="primary"
|
|
||||||
disabled={loading || !supportPassAuth}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{translate("ra.auth.sign_in")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={handleSSO}
|
|
||||||
disabled={loading || ssoBaseUrl === ""}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{translate("synapseadmin.auth.sso_sign_in")}
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</FormBox>
|
|
||||||
<Notification />
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginPage;
|
|
@ -1,14 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { AdminContext } from "react-admin";
|
import { TestContext } from "ra-test";
|
||||||
import LoginPage from "./LoginPage";
|
import LoginPage from "./LoginPage";
|
||||||
|
|
||||||
describe("LoginForm", () => {
|
describe("LoginForm", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
render(
|
render(
|
||||||
<AdminContext>
|
<TestContext>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AdminContext>
|
</TestContext>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
39
src/components/Menu.js
Normal file
39
src/components/Menu.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// in src/Menu.js
|
||||||
|
import * as React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
import { MenuItemLink, getResources } from "react-admin";
|
||||||
|
import DefaultIcon from "@mui/icons-material/ViewList";
|
||||||
|
import LabelIcon from "@mui/icons-material/Label";
|
||||||
|
|
||||||
|
const Menu = ({ onMenuClick, logout }) => {
|
||||||
|
const isXSmall = useMediaQuery(theme => theme.breakpoints.down("xs"));
|
||||||
|
const open = useSelector(state => state.admin.ui.sidebarOpen);
|
||||||
|
const resources = useSelector(getResources);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{resources.map(resource => (
|
||||||
|
<MenuItemLink
|
||||||
|
key={resource.name}
|
||||||
|
to={`/${resource.name}`}
|
||||||
|
primaryText={
|
||||||
|
(resource.options && resource.options.label) || resource.name
|
||||||
|
}
|
||||||
|
leftIcon={resource.icon ? <resource.icon /> : <DefaultIcon />}
|
||||||
|
onClick={onMenuClick}
|
||||||
|
sidebarIsOpen={open}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<MenuItemLink
|
||||||
|
to="/custom-route"
|
||||||
|
primaryText="Miscellaneous"
|
||||||
|
leftIcon={<LabelIcon />}
|
||||||
|
onClick={onMenuClick}
|
||||||
|
sidebarIsOpen={open}
|
||||||
|
/>
|
||||||
|
{isXSmall && logout}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Menu;
|
@ -6,19 +6,18 @@ import {
|
|||||||
DateField,
|
DateField,
|
||||||
DateTimeInput,
|
DateTimeInput,
|
||||||
Edit,
|
Edit,
|
||||||
|
Filter,
|
||||||
List,
|
List,
|
||||||
maxValue,
|
maxValue,
|
||||||
number,
|
number,
|
||||||
NumberField,
|
NumberField,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
regex,
|
regex,
|
||||||
SaveButton,
|
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
TextInput,
|
TextInput,
|
||||||
TextField,
|
TextField,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
|
|
||||||
|
|
||||||
const date_format = {
|
const date_format = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@ -54,12 +53,17 @@ const dateFormatter = v => {
|
|||||||
return `${year}-${month}-${day}T${hour}:${minute}`;
|
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
|
const RegistrationTokenFilter = props => (
|
||||||
|
<Filter {...props}>
|
||||||
|
<BooleanInput source="valid" alwaysOn />
|
||||||
|
</Filter>
|
||||||
|
);
|
||||||
|
|
||||||
export const RegistrationTokenList = props => (
|
export const RegistrationTokenList = props => {
|
||||||
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={registrationTokenFilters}
|
filters={<RegistrationTokenFilter />}
|
||||||
filterDefaultValues={{ valid: true }}
|
filterDefaultValues={{ valid: true }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
perPage={500}
|
perPage={500}
|
||||||
@ -77,18 +81,12 @@ export const RegistrationTokenList = props => (
|
|||||||
/>
|
/>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const RegistrationTokenCreate = props => (
|
export const RegistrationTokenCreate = props => (
|
||||||
<Create {...props} redirect="list">
|
<Create {...props}>
|
||||||
<SimpleForm
|
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}>
|
||||||
toolbar={
|
|
||||||
<Toolbar>
|
|
||||||
{/* It is possible to create tokens per default without input. */}
|
|
||||||
<SaveButton alwaysEnable />
|
|
||||||
</Toolbar>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
source="token"
|
source="token"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -111,7 +109,8 @@ export const RegistrationTokenCreate = props => (
|
|||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RegistrationTokenEdit = props => (
|
export const RegistrationTokenEdit = props => {
|
||||||
|
return (
|
||||||
<Edit {...props}>
|
<Edit {...props}>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="token" disabled />
|
<TextInput source="token" disabled />
|
||||||
@ -129,14 +128,5 @@ export const RegistrationTokenEdit = props => (
|
|||||||
/>
|
/>
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
|
||||||
const resource = {
|
|
||||||
name: "registration_tokens",
|
|
||||||
icon: RegistrationTokenIcon,
|
|
||||||
list: RegistrationTokenList,
|
|
||||||
edit: RegistrationTokenEdit,
|
|
||||||
create: RegistrationTokenCreate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default resource;
|
|
260
src/components/RoomDirectory.js
Normal file
260
src/components/RoomDirectory.js
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import React, { Fragment } from "react";
|
||||||
|
import { Avatar, Chip } from "@mui/material";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import {
|
||||||
|
BooleanField,
|
||||||
|
BulkDeleteButton,
|
||||||
|
Button,
|
||||||
|
Datagrid,
|
||||||
|
DeleteButton,
|
||||||
|
Filter,
|
||||||
|
List,
|
||||||
|
NumberField,
|
||||||
|
Pagination,
|
||||||
|
TextField,
|
||||||
|
useCreate,
|
||||||
|
useMutation,
|
||||||
|
useNotify,
|
||||||
|
useTranslate,
|
||||||
|
useRecordContext,
|
||||||
|
useRefresh,
|
||||||
|
useUnselectAll,
|
||||||
|
} from "react-admin";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
small: {
|
||||||
|
height: "40px",
|
||||||
|
width: "40px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const RoomDirectoryPagination = props => (
|
||||||
|
<Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RoomDirectoryDeleteButton = props => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeleteButton
|
||||||
|
{...props}
|
||||||
|
label="resources.room_directory.action.erase"
|
||||||
|
redirect={false}
|
||||||
|
mutationMode="pessimistic"
|
||||||
|
confirmTitle={translate("resources.room_directory.action.title", {
|
||||||
|
smart_count: 1,
|
||||||
|
})}
|
||||||
|
confirmContent={translate("resources.room_directory.action.content", {
|
||||||
|
smart_count: 1,
|
||||||
|
})}
|
||||||
|
resource="room_directory"
|
||||||
|
icon={<FolderSharedIcon />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoomDirectoryBulkDeleteButton = props => (
|
||||||
|
<BulkDeleteButton
|
||||||
|
{...props}
|
||||||
|
label="resources.room_directory.action.erase"
|
||||||
|
mutationMode="pessimistic"
|
||||||
|
confirmTitle="resources.room_directory.action.title"
|
||||||
|
confirmContent="resources.room_directory.action.content"
|
||||||
|
resource="room_directory"
|
||||||
|
icon={<FolderSharedIcon />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
|
||||||
|
const notify = useNotify();
|
||||||
|
const refresh = useRefresh();
|
||||||
|
const unselectAll = useUnselectAll();
|
||||||
|
const [createMany, { loading }] = useMutation();
|
||||||
|
|
||||||
|
const handleSend = values => {
|
||||||
|
createMany(
|
||||||
|
{
|
||||||
|
type: "createMany",
|
||||||
|
resource: "room_directory",
|
||||||
|
payload: { ids: selectedIds, data: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ data }) => {
|
||||||
|
notify("resources.room_directory.action.send_success");
|
||||||
|
unselectAll("rooms");
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onFailure: error =>
|
||||||
|
notify("resources.room_directory.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label="resources.room_directory.action.create"
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<FolderSharedIcon />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoomDirectorySaveButton = props => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
const notify = useNotify();
|
||||||
|
const refresh = useRefresh();
|
||||||
|
const [create, { loading }] = useCreate("room_directory");
|
||||||
|
|
||||||
|
const handleSend = values => {
|
||||||
|
create(
|
||||||
|
{
|
||||||
|
payload: { data: { id: record.id } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ data }) => {
|
||||||
|
notify("resources.room_directory.action.send_success");
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onFailure: error =>
|
||||||
|
notify("resources.room_directory.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label="resources.room_directory.action.create"
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<FolderSharedIcon />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RoomDirectoryBulkActionButtons = props => (
|
||||||
|
<Fragment>
|
||||||
|
<RoomDirectoryBulkDeleteButton {...props} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AvatarField = ({ source, className, record = {} }) => (
|
||||||
|
<Avatar src={record[source]} className={className} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const RoomDirectoryFilter = ({ ...props }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
return (
|
||||||
|
<Filter {...props}>
|
||||||
|
<Chip
|
||||||
|
label={translate("resources.rooms.fields.room_id")}
|
||||||
|
source="room_id"
|
||||||
|
defaultValue={false}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={translate("resources.rooms.fields.topic")}
|
||||||
|
source="topic"
|
||||||
|
defaultValue={false}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={translate("resources.rooms.fields.canonical_alias")}
|
||||||
|
source="canonical_alias"
|
||||||
|
defaultValue={false}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
</Filter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterableRoomDirectoryList = ({
|
||||||
|
roomDirectoryFilters,
|
||||||
|
dispatch,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const filter = roomDirectoryFilters;
|
||||||
|
const roomIdFilter = filter && filter.room_id ? true : false;
|
||||||
|
const topicFilter = filter && filter.topic ? true : false;
|
||||||
|
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
pagination={<RoomDirectoryPagination />}
|
||||||
|
bulkActionButtons={<RoomDirectoryBulkActionButtons />}
|
||||||
|
filters={<RoomDirectoryFilter />}
|
||||||
|
perPage={100}
|
||||||
|
>
|
||||||
|
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}>
|
||||||
|
<AvatarField
|
||||||
|
source="avatar_src"
|
||||||
|
sortable={false}
|
||||||
|
className={classes.small}
|
||||||
|
label="resources.rooms.fields.avatar"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
source="name"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.name"
|
||||||
|
/>
|
||||||
|
{roomIdFilter && (
|
||||||
|
<TextField
|
||||||
|
source="room_id"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.room_id"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{canonicalAliasFilter && (
|
||||||
|
<TextField
|
||||||
|
source="canonical_alias"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.canonical_alias"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{topicFilter && (
|
||||||
|
<TextField
|
||||||
|
source="topic"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.topic"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<NumberField
|
||||||
|
source="num_joined_members"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.joined_members"
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
source="world_readable"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.room_directory.fields.world_readable"
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
source="guest_can_join"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.room_directory.fields.guest_can_join"
|
||||||
|
/>
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
roomDirectoryFilters:
|
||||||
|
state.admin.resources.room_directory.list.params.displayedFilters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoomDirectoryList = connect(mapStateToProps)(
|
||||||
|
FilterableRoomDirectoryList
|
||||||
|
);
|
@ -1,206 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
BooleanField,
|
|
||||||
BulkDeleteButton,
|
|
||||||
Button,
|
|
||||||
DatagridConfigurable,
|
|
||||||
ExportButton,
|
|
||||||
DeleteButton,
|
|
||||||
List,
|
|
||||||
NumberField,
|
|
||||||
Pagination,
|
|
||||||
SelectColumnsButton,
|
|
||||||
TextField,
|
|
||||||
TopToolbar,
|
|
||||||
useCreate,
|
|
||||||
useDataProvider,
|
|
||||||
useListContext,
|
|
||||||
useNotify,
|
|
||||||
useTranslate,
|
|
||||||
useRecordContext,
|
|
||||||
useRefresh,
|
|
||||||
useUnselectAll,
|
|
||||||
} from "react-admin";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
|
|
||||||
import AvatarField from "./AvatarField";
|
|
||||||
|
|
||||||
const RoomDirectoryPagination = () => (
|
|
||||||
<Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export const RoomDirectoryUnpublishButton = props => {
|
|
||||||
const translate = useTranslate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DeleteButton
|
|
||||||
{...props}
|
|
||||||
label="resources.room_directory.action.erase"
|
|
||||||
redirect={false}
|
|
||||||
mutationMode="pessimistic"
|
|
||||||
confirmTitle={translate("resources.room_directory.action.title", {
|
|
||||||
smart_count: 1,
|
|
||||||
})}
|
|
||||||
confirmContent={translate("resources.room_directory.action.content", {
|
|
||||||
smart_count: 1,
|
|
||||||
})}
|
|
||||||
resource="room_directory"
|
|
||||||
icon={<RoomDirectoryIcon />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RoomDirectoryBulkUnpublishButton = props => (
|
|
||||||
<BulkDeleteButton
|
|
||||||
{...props}
|
|
||||||
label="resources.room_directory.action.erase"
|
|
||||||
mutationMode="pessimistic"
|
|
||||||
confirmTitle="resources.room_directory.action.title"
|
|
||||||
confirmContent="resources.room_directory.action.content"
|
|
||||||
resource="room_directory"
|
|
||||||
icon={<RoomDirectoryIcon />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const RoomDirectoryBulkPublishButton = props => {
|
|
||||||
const { selectedIds } = useListContext();
|
|
||||||
const notify = useNotify();
|
|
||||||
const refresh = useRefresh();
|
|
||||||
const unselectAllRooms = useUnselectAll("rooms");
|
|
||||||
const dataProvider = useDataProvider();
|
|
||||||
const { mutate, isLoading } = useMutation(
|
|
||||||
() =>
|
|
||||||
dataProvider.createMany("room_directory", {
|
|
||||||
ids: selectedIds,
|
|
||||||
data: {},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
notify("resources.room_directory.action.send_success");
|
|
||||||
unselectAllRooms();
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
onError: () =>
|
|
||||||
notify("resources.room_directory.action.send_failure", {
|
|
||||||
type: "error",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
label="resources.room_directory.action.create"
|
|
||||||
onClick={mutate}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<RoomDirectoryIcon />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RoomDirectoryPublishButton = props => {
|
|
||||||
const record = useRecordContext();
|
|
||||||
const notify = useNotify();
|
|
||||||
const refresh = useRefresh();
|
|
||||||
const [create, { isLoading }] = useCreate();
|
|
||||||
|
|
||||||
const handleSend = () => {
|
|
||||||
create(
|
|
||||||
"room_directory",
|
|
||||||
{ data: { id: record.id } },
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
notify("resources.room_directory.action.send_success");
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
onError: () =>
|
|
||||||
notify("resources.room_directory.action.send_failure", {
|
|
||||||
type: "error",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
label="resources.room_directory.action.create"
|
|
||||||
onClick={handleSend}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<RoomDirectoryIcon />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RoomDirectoryListActions = () => (
|
|
||||||
<TopToolbar>
|
|
||||||
<SelectColumnsButton />
|
|
||||||
<ExportButton />
|
|
||||||
</TopToolbar>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const RoomDirectoryList = () => (
|
|
||||||
<List
|
|
||||||
pagination={<RoomDirectoryPagination />}
|
|
||||||
perPage={100}
|
|
||||||
actions={<RoomDirectoryListActions />}
|
|
||||||
>
|
|
||||||
<DatagridConfigurable
|
|
||||||
rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
|
|
||||||
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
|
||||||
omit={["room_id", "canonical_alias", "topic"]}
|
|
||||||
>
|
|
||||||
<AvatarField
|
|
||||||
source="avatar_src"
|
|
||||||
sortable={false}
|
|
||||||
sx={{ height: "40px", width: "40px" }}
|
|
||||||
label="resources.rooms.fields.avatar"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
source="name"
|
|
||||||
sortable={false}
|
|
||||||
label="resources.rooms.fields.name"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
source="room_id"
|
|
||||||
sortable={false}
|
|
||||||
label="resources.rooms.fields.room_id"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
source="canonical_alias"
|
|
||||||
sortable={false}
|
|
||||||
label="resources.rooms.fields.canonical_alias"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
source="topic"
|
|
||||||
sortable={false}
|
|
||||||
label="resources.rooms.fields.topic"
|
|
||||||
/>
|
|
||||||
<NumberField
|
|
||||||
source="num_joined_members"
|
|
||||||
sortable={false}
|
|
||||||
label="resources.rooms.fields.joined_members"
|
|
||||||
/>
|
|
||||||
<BooleanField
|
|
||||||
source="world_readable"
|
|
||||||
sortable={false}
|
|
||||||
label="resources.room_directory.fields.world_readable"
|
|
||||||
/>
|
|
||||||
<BooleanField
|
|
||||||
source="guest_can_join"
|
|
||||||
sortable={false}
|
|
||||||
label="resources.room_directory.fields.guest_can_join"
|
|
||||||
/>
|
|
||||||
</DatagridConfigurable>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
const resource = {
|
|
||||||
name: "room_directory",
|
|
||||||
icon: RoomDirectoryIcon,
|
|
||||||
list: RoomDirectoryList,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resource;
|
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
@ -7,14 +7,12 @@ import {
|
|||||||
Toolbar,
|
Toolbar,
|
||||||
required,
|
required,
|
||||||
useCreate,
|
useCreate,
|
||||||
useDataProvider,
|
useMutation,
|
||||||
useListContext,
|
|
||||||
useNotify,
|
useNotify,
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import MessageIcon from "@mui/icons-material/Message";
|
import MessageIcon from "@mui/icons-material/Message";
|
||||||
import IconCancel from "@mui/icons-material/Cancel";
|
import IconCancel from "@mui/icons-material/Cancel";
|
||||||
import {
|
import {
|
||||||
@ -24,7 +22,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
|
const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
const ServerNoticeToolbar = props => (
|
const ServerNoticeToolbar = props => (
|
||||||
@ -48,7 +46,12 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
|
|||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{translate("resources.servernotices.helper.send")}
|
{translate("resources.servernotices.helper.send")}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
|
<SimpleForm
|
||||||
|
toolbar={<ServerNoticeToolbar />}
|
||||||
|
submitOnEnter={false}
|
||||||
|
redirect={false}
|
||||||
|
save={onSend}
|
||||||
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
source="body"
|
source="body"
|
||||||
label="resources.servernotices.fields.body"
|
label="resources.servernotices.fields.body"
|
||||||
@ -64,25 +67,24 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServerNoticeButton = () => {
|
export const ServerNoticeButton = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [create, { isloading }] = useCreate();
|
const [create, { loading }] = useCreate("servernotices");
|
||||||
|
|
||||||
const handleDialogOpen = () => setOpen(true);
|
const handleDialogOpen = () => setOpen(true);
|
||||||
const handleDialogClose = () => setOpen(false);
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
const handleSend = values => {
|
const handleSend = values => {
|
||||||
create(
|
create(
|
||||||
"servernotices",
|
{ payload: { data: { id: record.id, ...values } } },
|
||||||
{ data: { id: record.id, ...values } },
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.servernotices.action.send_success");
|
notify("resources.servernotices.action.send_success");
|
||||||
handleDialogClose();
|
handleDialogClose();
|
||||||
},
|
},
|
||||||
onError: () =>
|
onFailure: () =>
|
||||||
notify("resources.servernotices.action.send_failure", {
|
notify("resources.servernotices.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
@ -91,65 +93,67 @@ export const ServerNoticeButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment>
|
||||||
<Button
|
<Button
|
||||||
label="resources.servernotices.send"
|
label="resources.servernotices.send"
|
||||||
onClick={handleDialogOpen}
|
onClick={handleDialogOpen}
|
||||||
disabled={isloading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog
|
<ServerNoticeDialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleDialogClose}
|
onClose={handleDialogClose}
|
||||||
onSubmit={handleSend}
|
onSend={handleSend}
|
||||||
/>
|
/>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServerNoticeBulkButton = () => {
|
export const ServerNoticeBulkButton = ({ selectedIds }) => {
|
||||||
const { selectedIds } = useListContext();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const openDialog = () => setOpen(true);
|
|
||||||
const closeDialog = () => setOpen(false);
|
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const unselectAllUsers = useUnselectAll("users");
|
const unselectAll = useUnselectAll();
|
||||||
const dataProvider = useDataProvider();
|
const [createMany, { loading }] = useMutation();
|
||||||
|
|
||||||
const { mutate: sendNotices, isLoading } = useMutation(
|
const handleDialogOpen = () => setOpen(true);
|
||||||
data =>
|
const handleDialogClose = () => setOpen(false);
|
||||||
dataProvider.createMany("servernotices", {
|
|
||||||
ids: selectedIds,
|
const handleSend = values => {
|
||||||
data: data,
|
createMany(
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
type: "createMany",
|
||||||
notify("resources.servernotices.action.send_success");
|
resource: "servernotices",
|
||||||
unselectAllUsers();
|
payload: { ids: selectedIds, data: values },
|
||||||
closeDialog();
|
|
||||||
},
|
},
|
||||||
onError: () =>
|
{
|
||||||
|
onSuccess: ({ data }) => {
|
||||||
|
notify("resources.servernotices.action.send_success");
|
||||||
|
unselectAll("users");
|
||||||
|
handleDialogClose();
|
||||||
|
},
|
||||||
|
onFailure: error =>
|
||||||
notify("resources.servernotices.action.send_failure", {
|
notify("resources.servernotices.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment>
|
||||||
<Button
|
<Button
|
||||||
label="resources.servernotices.send"
|
label="resources.servernotices.send"
|
||||||
onClick={openDialog}
|
onClick={handleDialogOpen}
|
||||||
disabled={isLoading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog
|
<ServerNoticeDialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={closeDialog}
|
onClose={handleDialogClose}
|
||||||
onSubmit={sendNotices}
|
onSend={handleSend}
|
||||||
/>
|
/>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -3,6 +3,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
|
Filter,
|
||||||
List,
|
List,
|
||||||
Pagination,
|
Pagination,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
@ -19,13 +20,12 @@ import {
|
|||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
import AutorenewIcon from "@material-ui/icons/Autorenew";
|
||||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
import FolderSharedIcon from "@material-ui/icons/FolderShared";
|
||||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
import ViewListIcon from "@material-ui/icons/ViewList";
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
|
||||||
|
|
||||||
const DestinationPagination = () => (
|
const DestinationPagination = props => (
|
||||||
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const date_format = {
|
const date_format = {
|
||||||
@ -37,17 +37,23 @@ const date_format = {
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const destinationRowSx = (record, _index) => ({
|
const destinationRowStyle = (record, index) => ({
|
||||||
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
||||||
});
|
});
|
||||||
|
|
||||||
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
const DestinationFilter = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<Filter {...props}>
|
||||||
|
<SearchInput source="destination" alwaysOn />
|
||||||
|
</Filter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const DestinationReconnectButton = () => {
|
export const DestinationReconnectButton = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [handleReconnect, { isLoading }] = useDelete();
|
const [handleReconnect, { isLoading }] = useDelete("destinations");
|
||||||
|
|
||||||
// Reconnect is not required if no error has occurred. (`failure_ts`)
|
// Reconnect is not required if no error has occurred. (`failure_ts`)
|
||||||
if (!record || !record.failure_ts) return null;
|
if (!record || !record.failure_ts) return null;
|
||||||
@ -57,8 +63,7 @@ export const DestinationReconnectButton = () => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
handleReconnect(
|
handleReconnect(
|
||||||
"destinations",
|
{ payload: { id: record.id } },
|
||||||
{ id: record.id },
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("ra.notification.updated", {
|
notify("ra.notification.updated", {
|
||||||
@ -66,7 +71,7 @@ export const DestinationReconnectButton = () => {
|
|||||||
});
|
});
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onFailure: () => {
|
||||||
notify("ra.message.error", { type: "error" });
|
notify("ra.message.error", { type: "error" });
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -84,13 +89,13 @@ export const DestinationReconnectButton = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DestinationShowActions = () => (
|
const DestinationShowActions = props => (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
<DestinationReconnectButton />
|
<DestinationReconnectButton />
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
const DestinationTitle = () => {
|
const DestinationTitle = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
@ -104,14 +109,14 @@ export const DestinationList = props => {
|
|||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={destinationFilters}
|
filters={<DestinationFilter />}
|
||||||
pagination={<DestinationPagination />}
|
pagination={<DestinationPagination />}
|
||||||
sort={{ field: "destination", order: "ASC" }}
|
sort={{ field: "destination", order: "ASC" }}
|
||||||
|
bulkActionButtons={false}
|
||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
rowSx={destinationRowSx}
|
rowStyle={destinationRowStyle}
|
||||||
rowClick={(id, _resource, _record) => `${id}/show/rooms`}
|
rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`}
|
||||||
bulkActionButtons={false}
|
|
||||||
>
|
>
|
||||||
<TextField source="destination" />
|
<TextField source="destination" />
|
||||||
<DateField source="failure_ts" showTime options={date_format} />
|
<DateField source="failure_ts" showTime options={date_format} />
|
||||||
@ -155,7 +160,7 @@ export const DestinationShow = props => {
|
|||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
rowClick={(id, resource, record) => `/rooms/${id}/show`}
|
rowClick={(id, basePath, record) => `/rooms/${id}/show`}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
source="room_id"
|
source="room_id"
|
||||||
@ -178,12 +183,3 @@ export const DestinationShow = props => {
|
|||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource = {
|
|
||||||
name: "destinations",
|
|
||||||
icon: DestinationsIcon,
|
|
||||||
list: DestinationList,
|
|
||||||
show: DestinationShow,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resource;
|
|
84
src/components/devices.js
Normal file
84
src/components/devices.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React, { Fragment, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
useDelete,
|
||||||
|
useNotify,
|
||||||
|
Confirm,
|
||||||
|
useRecordContext,
|
||||||
|
useRefresh,
|
||||||
|
} from "react-admin";
|
||||||
|
import ActionDelete from "@mui/icons-material/Delete";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
deleteButton: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
||||||
|
// Reset on mouse devices
|
||||||
|
"@media (hover: none)": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "RaDeleteDeviceButton" }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DeviceRemoveButton = props => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
const classes = useStyles(props);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const refresh = useRefresh();
|
||||||
|
const notify = useNotify();
|
||||||
|
|
||||||
|
const [removeDevice, { isLoading }] = useDelete("devices");
|
||||||
|
|
||||||
|
if (!record) return null;
|
||||||
|
|
||||||
|
const handleClick = () => setOpen(true);
|
||||||
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
removeDevice(
|
||||||
|
{ payload: { id: record.id, user_id: record.user_id } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notify("resources.devices.action.erase.success");
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onFailure: () => {
|
||||||
|
notify("resources.devices.action.erase.failure", { type: "error" });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
label="ra.action.remove"
|
||||||
|
onClick={handleClick}
|
||||||
|
className={classnames("ra-delete-button", classes.deleteButton)}
|
||||||
|
>
|
||||||
|
<ActionDelete />
|
||||||
|
</Button>
|
||||||
|
<Confirm
|
||||||
|
isOpen={open}
|
||||||
|
loading={isLoading}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleDialogClose}
|
||||||
|
title="resources.devices.action.erase.title"
|
||||||
|
content="resources.devices.action.erase.content"
|
||||||
|
translateOptions={{
|
||||||
|
id: record.id,
|
||||||
|
name: record.display_name ? record.display_name : record.id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
@ -1,51 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
DeleteButton,
|
|
||||||
useDelete,
|
|
||||||
useNotify,
|
|
||||||
useRecordContext,
|
|
||||||
useRefresh,
|
|
||||||
} from "react-admin";
|
|
||||||
|
|
||||||
export const DeviceRemoveButton = props => {
|
|
||||||
const record = useRecordContext();
|
|
||||||
const refresh = useRefresh();
|
|
||||||
const notify = useNotify();
|
|
||||||
|
|
||||||
const [removeDevice] = useDelete();
|
|
||||||
|
|
||||||
if (!record) return null;
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
removeDevice(
|
|
||||||
"devices",
|
|
||||||
// needs previousData for user_id
|
|
||||||
{ id: record.id, previousData: record },
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
notify("resources.devices.action.erase.success");
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
notify("resources.devices.action.erase.failure", { type: "error" });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DeleteButton
|
|
||||||
{...props}
|
|
||||||
label="ra.action.remove"
|
|
||||||
confirmTitle="resources.devices.action.erase.title"
|
|
||||||
confirmContent="resources.devices.action.erase.content"
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
mutationMode="pessimistic"
|
|
||||||
redirect={false}
|
|
||||||
translateOptions={{
|
|
||||||
id: record.id,
|
|
||||||
name: record.display_name ? record.display_name : record.id,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import {
|
import {
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
Button,
|
Button,
|
||||||
@ -27,9 +30,24 @@ import {
|
|||||||
import IconCancel from "@mui/icons-material/Cancel";
|
import IconCancel from "@mui/icons-material/Cancel";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
import { alpha, useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
deleteButton: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
||||||
|
// Reset on mouse devices
|
||||||
|
"@media (hover: none)": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "RaDeleteDeviceButton" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
const dateParser = v => {
|
const dateParser = v => {
|
||||||
@ -38,7 +56,8 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
|||||||
return d.getTime();
|
return d.getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteMediaToolbar = props => (
|
const DeleteMediaToolbar = props => {
|
||||||
|
return (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<SaveButton
|
<SaveButton
|
||||||
label="resources.delete_media.action.send"
|
label="resources.delete_media.action.send"
|
||||||
@ -49,6 +68,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} loading={loading}>
|
<Dialog open={open} onClose={onClose} loading={loading}>
|
||||||
@ -59,7 +79,12 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
|||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{translate("resources.delete_media.helper.send")}
|
{translate("resources.delete_media.helper.send")}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
<SimpleForm
|
||||||
|
toolbar={<DeleteMediaToolbar />}
|
||||||
|
submitOnEnter={false}
|
||||||
|
redirect={false}
|
||||||
|
save={onSend}
|
||||||
|
>
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
fullWidth
|
fullWidth
|
||||||
source="before_ts"
|
source="before_ts"
|
||||||
@ -88,25 +113,23 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteMediaButton = props => {
|
export const DeleteMediaButton = props => {
|
||||||
const theme = useTheme();
|
const classes = useStyles(props);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [deleteOne, { isLoading }] = useDelete();
|
const [deleteOne, { loading }] = useDelete("delete_media");
|
||||||
|
|
||||||
const openDialog = () => setOpen(true);
|
const handleDialogOpen = () => setOpen(true);
|
||||||
const closeDialog = () => setOpen(false);
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
const deleteMedia = values => {
|
const handleSend = values => {
|
||||||
deleteOne(
|
deleteOne(
|
||||||
"delete_media",
|
{ payload: { ...values } },
|
||||||
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
|
|
||||||
{ meta: values },
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.delete_media.action.send_success");
|
notify("resources.delete_media.action.send_success");
|
||||||
closeDialog();
|
handleDialogClose();
|
||||||
},
|
},
|
||||||
onError: () =>
|
onFailure: () =>
|
||||||
notify("resources.delete_media.action.send_failure", {
|
notify("resources.delete_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
@ -115,54 +138,43 @@ export const DeleteMediaButton = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment>
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
|
||||||
label="resources.delete_media.action.send"
|
label="resources.delete_media.action.send"
|
||||||
onClick={openDialog}
|
onClick={handleDialogOpen}
|
||||||
disabled={isLoading}
|
disabled={loading}
|
||||||
sx={{
|
className={classnames("ra-delete-button", classes.deleteButton)}
|
||||||
color: theme.palette.error.main,
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
|
||||||
// Reset on mouse devices
|
|
||||||
"@media (hover: none)": {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DeleteSweepIcon />
|
<DeleteSweepIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<DeleteMediaDialog
|
<DeleteMediaDialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={closeDialog}
|
onClose={handleDialogClose}
|
||||||
onSubmit={deleteMedia}
|
onSend={handleSend}
|
||||||
/>
|
/>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProtectMediaButton = () => {
|
export const ProtectMediaButton = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [create, { isLoading }] = useCreate();
|
const [create, { loading }] = useCreate("protect_media");
|
||||||
const [deleteOne] = useDelete();
|
const [deleteOne] = useDelete("protect_media");
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
const handleProtect = () => {
|
const handleProtect = () => {
|
||||||
create(
|
create(
|
||||||
"protect_media",
|
{ payload: { data: record } },
|
||||||
{ data: record },
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.protect_media.action.send_success");
|
notify("resources.protect_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: () =>
|
onFailure: () =>
|
||||||
notify("resources.protect_media.action.send_failure", {
|
notify("resources.protect_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
@ -172,14 +184,13 @@ export const ProtectMediaButton = () => {
|
|||||||
|
|
||||||
const handleUnprotect = () => {
|
const handleUnprotect = () => {
|
||||||
deleteOne(
|
deleteOne(
|
||||||
"protect_media",
|
{ payload: { ...record } },
|
||||||
{ id: record.id },
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.protect_media.action.send_success");
|
notify("resources.protect_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: () =>
|
onFailure: () =>
|
||||||
notify("resources.protect_media.action.send_failure", {
|
notify("resources.protect_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
@ -192,7 +203,7 @@ export const ProtectMediaButton = () => {
|
|||||||
Wrapping Tooltip with <div>
|
Wrapping Tooltip with <div>
|
||||||
https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
|
https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
|
||||||
*/
|
*/
|
||||||
<>
|
<Fragment>
|
||||||
{record.quarantined_by && (
|
{record.quarantined_by && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={translate("resources.protect_media.action.none", {
|
title={translate("resources.protect_media.action.none", {
|
||||||
@ -218,7 +229,7 @@ export const ProtectMediaButton = () => {
|
|||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleUnprotect} disabled={isLoading}>
|
<Button onClick={handleUnprotect} disabled={loading}>
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -231,13 +242,13 @@ export const ProtectMediaButton = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleProtect} disabled={isLoading}>
|
<Button onClick={handleProtect} disabled={loading}>
|
||||||
<LockOpenIcon />
|
<LockOpenIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -246,21 +257,20 @@ export const QuarantineMediaButton = props => {
|
|||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [create, { isLoading }] = useCreate();
|
const [create, { loading }] = useCreate("quarantine_media");
|
||||||
const [deleteOne] = useDelete();
|
const [deleteOne] = useDelete("quarantine_media");
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
const handleQuarantaine = () => {
|
const handleQuarantaine = () => {
|
||||||
create(
|
create(
|
||||||
"quarantine_media",
|
{ payload: { data: record } },
|
||||||
{ data: record },
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.quarantine_media.action.send_success");
|
notify("resources.quarantine_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: () =>
|
onFailure: () =>
|
||||||
notify("resources.quarantine_media.action.send_failure", {
|
notify("resources.quarantine_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
@ -270,14 +280,13 @@ export const QuarantineMediaButton = props => {
|
|||||||
|
|
||||||
const handleRemoveQuarantaine = () => {
|
const handleRemoveQuarantaine = () => {
|
||||||
deleteOne(
|
deleteOne(
|
||||||
"quarantine_media",
|
{ payload: { ...record } },
|
||||||
{ id: record.id, previousData: record },
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.quarantine_media.action.send_success");
|
notify("resources.quarantine_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: () =>
|
onFailure: () =>
|
||||||
notify("resources.quarantine_media.action.send_failure", {
|
notify("resources.quarantine_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
@ -286,7 +295,7 @@ export const QuarantineMediaButton = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment>
|
||||||
{record.safe_from_quarantine && (
|
{record.safe_from_quarantine && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={translate("resources.quarantine_media.action.none", {
|
title={translate("resources.quarantine_media.action.none", {
|
||||||
@ -294,7 +303,7 @@ export const QuarantineMediaButton = props => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button {...props} disabled={true}>
|
<Button disabled={true}>
|
||||||
<ClearIcon />
|
<ClearIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -307,11 +316,7 @@ export const QuarantineMediaButton = props => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button onClick={handleRemoveQuarantaine} disabled={loading}>
|
||||||
{...props}
|
|
||||||
onClick={handleRemoveQuarantaine}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<BlockIcon color="error" />
|
<BlockIcon color="error" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -324,12 +329,12 @@ export const QuarantineMediaButton = props => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleQuarantaine} disabled={isLoading}>
|
<Button onClick={handleQuarantaine} disabled={loading}>
|
||||||
<BlockIcon />
|
<BlockIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,20 +1,18 @@
|
|||||||
import React from "react";
|
import React, { Fragment } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
DateField,
|
DateField,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DatagridConfigurable,
|
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
ExportButton,
|
Filter,
|
||||||
FunctionField,
|
|
||||||
List,
|
List,
|
||||||
NumberField,
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
ReferenceManyField,
|
ReferenceManyField,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
SelectColumnsButton,
|
|
||||||
SelectField,
|
SelectField,
|
||||||
Show,
|
Show,
|
||||||
Tab,
|
Tab,
|
||||||
@ -24,8 +22,10 @@ import {
|
|||||||
useRecordContext,
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import get from "lodash/get";
|
||||||
import Box from "@mui/material/Box";
|
import PropTypes from "prop-types";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import { Tooltip, Typography, Chip } from "@mui/material";
|
||||||
import FastForwardIcon from "@mui/icons-material/FastForward";
|
import FastForwardIcon from "@mui/icons-material/FastForward";
|
||||||
import HttpsIcon from "@mui/icons-material/Https";
|
import HttpsIcon from "@mui/icons-material/Https";
|
||||||
import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
|
import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
|
||||||
@ -34,12 +34,11 @@ import UserIcon from "@mui/icons-material/Group";
|
|||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import EventIcon from "@mui/icons-material/Event";
|
import EventIcon from "@mui/icons-material/Event";
|
||||||
import RoomIcon from "@mui/icons-material/ViewList";
|
|
||||||
import {
|
import {
|
||||||
RoomDirectoryBulkUnpublishButton,
|
RoomDirectoryBulkDeleteButton,
|
||||||
RoomDirectoryBulkPublishButton,
|
RoomDirectoryBulkSaveButton,
|
||||||
RoomDirectoryUnpublishButton,
|
RoomDirectoryDeleteButton,
|
||||||
RoomDirectoryPublishButton,
|
RoomDirectorySaveButton,
|
||||||
} from "./RoomDirectory";
|
} from "./RoomDirectory";
|
||||||
|
|
||||||
const date_format = {
|
const date_format = {
|
||||||
@ -51,11 +50,44 @@ const date_format = {
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomPagination = () => (
|
const useStyles = makeStyles(theme => ({
|
||||||
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
helper_forward_extremities: {
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
margin: "0.5em",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const RoomPagination = props => (
|
||||||
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const RoomTitle = () => {
|
const EncryptionField = ({ source, record = {}, emptyText }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const value = get(record, source);
|
||||||
|
let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true";
|
||||||
|
|
||||||
|
if (value === false || value === true) {
|
||||||
|
return (
|
||||||
|
<Typography component="span" variant="body2">
|
||||||
|
<Tooltip title={translate(ariaLabel, { _: ariaLabel })}>
|
||||||
|
{value === true ? (
|
||||||
|
<HttpsIcon data-testid="true" htmlColor="limegreen" />
|
||||||
|
) : (
|
||||||
|
<NoEncryptionIcon data-testid="false" color="error" />
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography component="span" variant="body2">
|
||||||
|
{emptyText}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RoomTitle = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
var name = "";
|
var name = "";
|
||||||
@ -70,18 +102,24 @@ const RoomTitle = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomShowActions = () => {
|
const RoomShowActions = ({ basePath, data, resource }) => {
|
||||||
const record = useRecordContext();
|
|
||||||
var roomDirectoryStatus = "";
|
var roomDirectoryStatus = "";
|
||||||
if (record) {
|
if (data) {
|
||||||
roomDirectoryStatus = record.public;
|
roomDirectoryStatus = data.public;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
|
{roomDirectoryStatus === false && (
|
||||||
{roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
|
<RoomDirectorySaveButton record={data} />
|
||||||
|
)}
|
||||||
|
{roomDirectoryStatus === true && (
|
||||||
|
<RoomDirectoryDeleteButton record={data} />
|
||||||
|
)}
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
basePath={basePath}
|
||||||
|
record={data}
|
||||||
|
resource={resource}
|
||||||
mutationMode="pessimistic"
|
mutationMode="pessimistic"
|
||||||
confirmTitle="resources.rooms.action.erase.title"
|
confirmTitle="resources.rooms.action.erase.title"
|
||||||
confirmContent="resources.rooms.action.erase.content"
|
confirmContent="resources.rooms.action.erase.content"
|
||||||
@ -91,6 +129,7 @@ const RoomShowActions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RoomShow = props => {
|
export const RoomShow = props => {
|
||||||
|
const classes = useStyles({ props });
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||||
@ -98,7 +137,6 @@ export const RoomShow = props => {
|
|||||||
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
|
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
|
||||||
<TextField source="room_id" />
|
<TextField source="room_id" />
|
||||||
<TextField source="name" />
|
<TextField source="name" />
|
||||||
<TextField source="topic" />
|
|
||||||
<TextField source="canonical_alias" />
|
<TextField source="canonical_alias" />
|
||||||
<ReferenceField source="creator" reference="users">
|
<ReferenceField source="creator" reference="users">
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
@ -133,8 +171,7 @@ export const RoomShow = props => {
|
|||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
rowClick={(id, resource, record) => "/users/" + id}
|
rowClick={(id, basePath, record) => "/users/" + id}
|
||||||
bulkActionButtons={false}
|
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
source="id"
|
source="id"
|
||||||
@ -219,7 +256,7 @@ export const RoomShow = props => {
|
|||||||
target="room_id"
|
target="room_id"
|
||||||
addLabel={false}
|
addLabel={false}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<TextField source="type" sortable={false} />
|
<TextField source="type" sortable={false} />
|
||||||
<DateField
|
<DateField
|
||||||
source="origin_server_ts"
|
source="origin_server_ts"
|
||||||
@ -244,20 +281,15 @@ export const RoomShow = props => {
|
|||||||
icon={<FastForwardIcon />}
|
icon={<FastForwardIcon />}
|
||||||
path="forward_extremities"
|
path="forward_extremities"
|
||||||
>
|
>
|
||||||
<Box
|
<div className={classes.helper_forward_extremities}>
|
||||||
sx={{
|
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
margin: "0.5em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{translate("resources.rooms.helper.forward_extremities")}
|
{translate("resources.rooms.helper.forward_extremities")}
|
||||||
</Box>
|
</div>
|
||||||
<ReferenceManyField
|
<ReferenceManyField
|
||||||
reference="forward_extremities"
|
reference="forward_extremities"
|
||||||
target="room_id"
|
target="room_id"
|
||||||
addLabel={false}
|
addLabel={false}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<TextField source="id" sortable={false} />
|
<TextField source="id" sortable={false} />
|
||||||
<DateField
|
<DateField
|
||||||
source="received_ts"
|
source="received_ts"
|
||||||
@ -275,81 +307,104 @@ export const RoomShow = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomBulkActionButtons = () => (
|
const RoomBulkActionButtons = props => (
|
||||||
<>
|
<Fragment>
|
||||||
<RoomDirectoryBulkPublishButton />
|
<RoomDirectoryBulkSaveButton {...props} />
|
||||||
<RoomDirectoryBulkUnpublishButton />
|
<RoomDirectoryBulkDeleteButton {...props} />
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
|
{...props}
|
||||||
confirmTitle="resources.rooms.action.erase.title"
|
confirmTitle="resources.rooms.action.erase.title"
|
||||||
confirmContent="resources.rooms.action.erase.content"
|
confirmContent="resources.rooms.action.erase.content"
|
||||||
mutationMode="pessimistic"
|
mutationMode="pessimistic"
|
||||||
/>
|
/>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
const roomFilters = [<SearchInput source="search_term" alwaysOn />];
|
const RoomFilter = ({ ...props }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
return (
|
||||||
|
<Filter {...props}>
|
||||||
|
<SearchInput source="search_term" alwaysOn />
|
||||||
|
<Chip
|
||||||
|
label={translate("resources.rooms.fields.joined_local_members")}
|
||||||
|
source="joined_local_members"
|
||||||
|
defaultValue={false}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={translate("resources.rooms.fields.state_events")}
|
||||||
|
source="state_events"
|
||||||
|
defaultValue={false}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={translate("resources.rooms.fields.version")}
|
||||||
|
source="version"
|
||||||
|
defaultValue={false}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={translate("resources.rooms.fields.federatable")}
|
||||||
|
source="federatable"
|
||||||
|
defaultValue={false}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
</Filter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const RoomListActions = () => (
|
const RoomNameField = props => {
|
||||||
<TopToolbar>
|
const { source } = props;
|
||||||
<SelectColumnsButton />
|
const record = useRecordContext();
|
||||||
<ExportButton />
|
return (
|
||||||
</TopToolbar>
|
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const RoomList = props => {
|
RoomNameField.propTypes = {
|
||||||
const theme = useTheme();
|
label: PropTypes.string,
|
||||||
|
record: PropTypes.object,
|
||||||
|
source: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterableRoomList = ({ roomFilters, dispatch, ...props }) => {
|
||||||
|
const filter = roomFilters;
|
||||||
|
const localMembersFilter =
|
||||||
|
filter && filter.joined_local_members ? true : false;
|
||||||
|
const stateEventsFilter = filter && filter.state_events ? true : false;
|
||||||
|
const versionFilter = filter && filter.version ? true : false;
|
||||||
|
const federateableFilter = filter && filter.federatable ? true : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
pagination={<RoomPagination />}
|
pagination={<RoomPagination />}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
filters={roomFilters}
|
filters={<RoomFilter />}
|
||||||
actions={<RoomListActions />}
|
|
||||||
>
|
|
||||||
<DatagridConfigurable
|
|
||||||
rowClick="show"
|
|
||||||
bulkActionButtons={<RoomBulkActionButtons />}
|
bulkActionButtons={<RoomBulkActionButtons />}
|
||||||
omit={[
|
|
||||||
"joined_local_members",
|
|
||||||
"state_events",
|
|
||||||
"version",
|
|
||||||
"federatable",
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<BooleanField
|
<Datagrid rowClick="show">
|
||||||
|
<EncryptionField
|
||||||
source="is_encrypted"
|
source="is_encrypted"
|
||||||
sortBy="encryption"
|
sortBy="encryption"
|
||||||
TrueIcon={HttpsIcon}
|
|
||||||
FalseIcon={NoEncryptionIcon}
|
|
||||||
label={<HttpsIcon />}
|
label={<HttpsIcon />}
|
||||||
sx={{
|
|
||||||
[`& [data-testid="true"]`]: { color: theme.palette.success.main },
|
|
||||||
[`& [data-testid="false"]`]: { color: theme.palette.error.main },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FunctionField
|
|
||||||
source="name"
|
|
||||||
render={record =>
|
|
||||||
record["name"] || record["canonical_alias"] || record["id"]
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<RoomNameField source="name" />
|
||||||
<TextField source="joined_members" />
|
<TextField source="joined_members" />
|
||||||
<TextField source="joined_local_members" />
|
{localMembersFilter && <TextField source="joined_local_members" />}
|
||||||
<TextField source="state_events" />
|
{stateEventsFilter && <TextField source="state_events" />}
|
||||||
<TextField source="version" />
|
{versionFilter && <TextField source="version" />}
|
||||||
<BooleanField source="federatable" />
|
{federateableFilter && <BooleanField source="federatable" />}
|
||||||
<BooleanField source="public" />
|
<BooleanField source="public" />
|
||||||
</DatagridConfigurable>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource = {
|
function mapStateToProps(state) {
|
||||||
name: "rooms",
|
return {
|
||||||
icon: RoomIcon,
|
roomFilters: state.admin.resources.rooms.list.params.displayedFilters,
|
||||||
list: RoomList,
|
};
|
||||||
show: RoomShow,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default resource;
|
export const RoomList = connect(mapStateToProps)(FilterableRoomList);
|
81
src/components/statistics.js
Normal file
81
src/components/statistics.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { cloneElement } from "react";
|
||||||
|
import {
|
||||||
|
Datagrid,
|
||||||
|
ExportButton,
|
||||||
|
Filter,
|
||||||
|
List,
|
||||||
|
NumberField,
|
||||||
|
Pagination,
|
||||||
|
sanitizeListRestProps,
|
||||||
|
SearchInput,
|
||||||
|
TextField,
|
||||||
|
TopToolbar,
|
||||||
|
useListContext,
|
||||||
|
} from "react-admin";
|
||||||
|
import { DeleteMediaButton } from "./media";
|
||||||
|
|
||||||
|
const ListActions = props => {
|
||||||
|
const { className, exporter, filters, maxResults, ...rest } = props;
|
||||||
|
const {
|
||||||
|
currentSort,
|
||||||
|
resource,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
showFilter,
|
||||||
|
total,
|
||||||
|
} = useListContext();
|
||||||
|
return (
|
||||||
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
{filters &&
|
||||||
|
cloneElement(filters, {
|
||||||
|
resource,
|
||||||
|
showFilter,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
context: "button",
|
||||||
|
})}
|
||||||
|
<DeleteMediaButton />
|
||||||
|
<ExportButton
|
||||||
|
disabled={total === 0}
|
||||||
|
resource={resource}
|
||||||
|
sort={currentSort}
|
||||||
|
filterValues={filterValues}
|
||||||
|
maxResults={maxResults}
|
||||||
|
/>
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserMediaStatsPagination = props => (
|
||||||
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserMediaStatsFilter = props => (
|
||||||
|
<Filter {...props}>
|
||||||
|
<SearchInput source="search_term" alwaysOn />
|
||||||
|
</Filter>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UserMediaStatsList = props => {
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
actions={<ListActions />}
|
||||||
|
filters={<UserMediaStatsFilter />}
|
||||||
|
pagination={<UserMediaStatsPagination />}
|
||||||
|
sort={{ field: "media_length", order: "DESC" }}
|
||||||
|
bulkActionButtons={false}
|
||||||
|
>
|
||||||
|
<Datagrid rowClick={(id, basePath, record) => "/users/" + id + "/media"}>
|
||||||
|
<TextField source="user_id" label="resources.users.fields.id" />
|
||||||
|
<TextField
|
||||||
|
source="displayname"
|
||||||
|
label="resources.users.fields.displayname"
|
||||||
|
/>
|
||||||
|
<NumberField source="media_count" />
|
||||||
|
<NumberField source="media_length" />
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
@ -1,79 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { cloneElement } from "react";
|
|
||||||
import {
|
|
||||||
Datagrid,
|
|
||||||
ExportButton,
|
|
||||||
List,
|
|
||||||
NumberField,
|
|
||||||
Pagination,
|
|
||||||
sanitizeListRestProps,
|
|
||||||
SearchInput,
|
|
||||||
TextField,
|
|
||||||
TopToolbar,
|
|
||||||
useListContext,
|
|
||||||
} from "react-admin";
|
|
||||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
|
||||||
import { DeleteMediaButton } from "./media";
|
|
||||||
|
|
||||||
const ListActions = props => {
|
|
||||||
const { className, exporter, filters, maxResults, ...rest } = props;
|
|
||||||
const { sort, resource, displayedFilters, filterValues, showFilter, total } =
|
|
||||||
useListContext();
|
|
||||||
return (
|
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
|
||||||
{filters &&
|
|
||||||
cloneElement(filters, {
|
|
||||||
resource,
|
|
||||||
showFilter,
|
|
||||||
displayedFilters,
|
|
||||||
filterValues,
|
|
||||||
context: "button",
|
|
||||||
})}
|
|
||||||
<DeleteMediaButton />
|
|
||||||
<ExportButton
|
|
||||||
disabled={total === 0}
|
|
||||||
resource={resource}
|
|
||||||
sort={sort}
|
|
||||||
filterValues={filterValues}
|
|
||||||
maxResults={maxResults}
|
|
||||||
/>
|
|
||||||
</TopToolbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserMediaStatsPagination = () => (
|
|
||||||
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
|
|
||||||
|
|
||||||
export const UserMediaStatsList = props => (
|
|
||||||
<List
|
|
||||||
{...props}
|
|
||||||
actions={<ListActions />}
|
|
||||||
filters={userMediaStatsFilters}
|
|
||||||
pagination={<UserMediaStatsPagination />}
|
|
||||||
sort={{ field: "media_length", order: "DESC" }}
|
|
||||||
>
|
|
||||||
<Datagrid
|
|
||||||
rowClick={(id, resource, record) => "/users/" + id + "/media"}
|
|
||||||
bulkActionButtons={false}
|
|
||||||
>
|
|
||||||
<TextField source="user_id" label="resources.users.fields.id" />
|
|
||||||
<TextField
|
|
||||||
source="displayname"
|
|
||||||
label="resources.users.fields.displayname"
|
|
||||||
/>
|
|
||||||
<NumberField source="media_count" />
|
|
||||||
<NumberField source="media_length" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
const resource = {
|
|
||||||
name: "user_media_statistics",
|
|
||||||
icon: EqualizerIcon,
|
|
||||||
list: UserMediaStatsList,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resource;
|
|
@ -1,4 +1,5 @@
|
|||||||
import React, { cloneElement } from "react";
|
import React, { cloneElement, Fragment } from "react";
|
||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||||
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
||||||
import DevicesIcon from "@mui/icons-material/Devices";
|
import DevicesIcon from "@mui/icons-material/Devices";
|
||||||
@ -7,7 +8,6 @@ import NotificationsIcon from "@mui/icons-material/Notifications";
|
|||||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||||
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
||||||
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
||||||
import UserIcon from "@mui/icons-material/Group";
|
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
import {
|
import {
|
||||||
ArrayInput,
|
ArrayInput,
|
||||||
@ -18,6 +18,7 @@ import {
|
|||||||
Create,
|
Create,
|
||||||
Edit,
|
Edit,
|
||||||
List,
|
List,
|
||||||
|
Filter,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
SimpleFormIterator,
|
SimpleFormIterator,
|
||||||
@ -48,10 +49,28 @@ import {
|
|||||||
NumberField,
|
NumberField,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import AvatarField from "./AvatarField";
|
|
||||||
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";
|
||||||
|
|
||||||
|
const redirect = () => {
|
||||||
|
return {
|
||||||
|
pathname: "/import_users",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
small: {
|
||||||
|
height: "40px",
|
||||||
|
width: "40px",
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
height: "120px",
|
||||||
|
width: "120px",
|
||||||
|
float: "right",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const choices_medium = [
|
const choices_medium = [
|
||||||
{ id: "email", name: "resources.users.email" },
|
{ id: "email", name: "resources.users.email" },
|
||||||
@ -73,7 +92,7 @@ const date_format = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UserListActions = ({
|
const UserListActions = ({
|
||||||
sort,
|
currentSort,
|
||||||
className,
|
className,
|
||||||
resource,
|
resource,
|
||||||
filters,
|
filters,
|
||||||
@ -82,6 +101,7 @@ const UserListActions = ({
|
|||||||
filterValues,
|
filterValues,
|
||||||
permanentFilter,
|
permanentFilter,
|
||||||
hasCreate, // you can hide CreateButton if hasCreate = false
|
hasCreate, // you can hide CreateButton if hasCreate = false
|
||||||
|
basePath,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
onUnselectItems,
|
onUnselectItems,
|
||||||
showFilter,
|
showFilter,
|
||||||
@ -99,18 +119,18 @@ const UserListActions = ({
|
|||||||
filterValues,
|
filterValues,
|
||||||
context: "button",
|
context: "button",
|
||||||
})}
|
})}
|
||||||
<CreateButton />
|
<CreateButton basePath={basePath} />
|
||||||
<ExportButton
|
<ExportButton
|
||||||
disabled={total === 0}
|
disabled={total === 0}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
sort={sort}
|
sort={currentSort}
|
||||||
filter={{ ...filterValues, ...permanentFilter }}
|
filter={{ ...filterValues, ...permanentFilter }}
|
||||||
exporter={exporter}
|
exporter={exporter}
|
||||||
maxResults={maxResults}
|
maxResults={maxResults}
|
||||||
/>
|
/>
|
||||||
{/* Add your custom actions */}
|
{/* Add your custom actions */}
|
||||||
<Button component={Link} to="/import_users" label="CSV Import">
|
<Button component={Link} to={redirect} label="CSV Import">
|
||||||
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
|
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
|
||||||
</Button>
|
</Button>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
@ -121,44 +141,54 @@ UserListActions.defaultProps = {
|
|||||||
onUnselectItems: () => null,
|
onUnselectItems: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserPagination = () => (
|
const UserPagination = props => (
|
||||||
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const userFilters = [
|
const UserFilter = props => (
|
||||||
<SearchInput source="name" alwaysOn />,
|
<Filter {...props}>
|
||||||
<BooleanInput source="guests" alwaysOn />,
|
<SearchInput source="name" alwaysOn />
|
||||||
|
<BooleanInput source="guests" alwaysOn />
|
||||||
<BooleanInput
|
<BooleanInput
|
||||||
label="resources.users.fields.show_deactivated"
|
label="resources.users.fields.show_deactivated"
|
||||||
source="deactivated"
|
source="deactivated"
|
||||||
alwaysOn
|
alwaysOn
|
||||||
/>,
|
/>
|
||||||
];
|
</Filter>
|
||||||
|
);
|
||||||
|
|
||||||
const UserBulkActionButtons = () => (
|
const UserBulkActionButtons = props => (
|
||||||
<>
|
<Fragment>
|
||||||
<ServerNoticeBulkButton />
|
<ServerNoticeBulkButton {...props} />
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
|
{...props}
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle="resources.users.helper.erase"
|
confirmTitle="resources.users.helper.erase"
|
||||||
mutationMode="pessimistic"
|
mutationMode="pessimistic"
|
||||||
/>
|
/>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UserList = props => (
|
const AvatarField = ({ source, className, record = {} }) => (
|
||||||
|
<Avatar src={record[source]} className={className} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UserList = props => {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={userFilters}
|
filters={<UserFilter />}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
actions={<UserListActions maxResults={10000} />}
|
actions={<UserListActions maxResults={10000} />}
|
||||||
|
bulkActionButtons={<UserBulkActionButtons />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
<Datagrid rowClick="edit">
|
||||||
<AvatarField
|
<AvatarField
|
||||||
source="avatar_src"
|
source="avatar_src"
|
||||||
sx={{ height: "40px", width: "40px" }}
|
className={classes.small}
|
||||||
sortBy="avatar_url"
|
sortBy="avatar_url"
|
||||||
/>
|
/>
|
||||||
<TextField source="id" sortBy="name" />
|
<TextField source="id" sortBy="name" />
|
||||||
@ -174,7 +204,8 @@ export const UserList = props => (
|
|||||||
/>
|
/>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/appendices#user-identifiers
|
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||||||
// here only local part of user_id
|
// here only local part of user_id
|
||||||
@ -231,7 +262,7 @@ export function generateRandomUser() {
|
|||||||
|
|
||||||
const UserEditToolbar = props => (
|
const UserEditToolbar = props => (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<SaveButton disabled={props.pristine} />
|
<SaveButton submitOnEnter={true} disabled={props.pristine} />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -271,6 +302,7 @@ export const UserCreate = props => (
|
|||||||
source="user_type"
|
source="user_type"
|
||||||
choices={choices_type}
|
choices={choices_type}
|
||||||
translateChoice={false}
|
translateChoice={false}
|
||||||
|
allowEmpty={true}
|
||||||
resettable
|
resettable
|
||||||
/>
|
/>
|
||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
@ -298,7 +330,7 @@ export const UserCreate = props => (
|
|||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|
||||||
const UserTitle = () => {
|
const UserTitle = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
@ -312,6 +344,7 @@ const UserTitle = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UserEdit = props => {
|
export const UserEdit = props => {
|
||||||
|
const classes = useStyles();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||||
@ -323,7 +356,7 @@ export const UserEdit = props => {
|
|||||||
<AvatarField
|
<AvatarField
|
||||||
source="avatar_src"
|
source="avatar_src"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
sx={{ height: "120px", width: "120px", float: "right" }}
|
className={classes.large}
|
||||||
/>
|
/>
|
||||||
<TextInput source="id" disabled />
|
<TextInput source="id" disabled />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
@ -336,6 +369,7 @@ export const UserEdit = props => {
|
|||||||
source="user_type"
|
source="user_type"
|
||||||
choices={choices_type}
|
choices={choices_type}
|
||||||
translateChoice={false}
|
translateChoice={false}
|
||||||
|
allowEmpty={true}
|
||||||
resettable
|
resettable
|
||||||
/>
|
/>
|
||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
@ -417,7 +451,7 @@ export const UserEdit = props => {
|
|||||||
source="devices[].sessions[0].connections"
|
source="devices[].sessions[0].connections"
|
||||||
label="resources.connections.name"
|
label="resources.connections.name"
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<TextField source="ip" sortable={false} />
|
<TextField source="ip" sortable={false} />
|
||||||
<DateField
|
<DateField
|
||||||
source="last_seen"
|
source="last_seen"
|
||||||
@ -479,8 +513,7 @@ export const UserEdit = props => {
|
|||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
|
rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}
|
||||||
bulkActionButtons={false}
|
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
source="id"
|
source="id"
|
||||||
@ -510,7 +543,7 @@ export const UserEdit = props => {
|
|||||||
target="user_id"
|
target="user_id"
|
||||||
addLabel={false}
|
addLabel={false}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<TextField source="kind" sortable={false} />
|
<TextField source="kind" sortable={false} />
|
||||||
<TextField source="app_display_name" sortable={false} />
|
<TextField source="app_display_name" sortable={false} />
|
||||||
<TextField source="app_id" sortable={false} />
|
<TextField source="app_id" sortable={false} />
|
||||||
@ -526,13 +559,3 @@ export const UserEdit = props => {
|
|||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource = {
|
|
||||||
name: "users",
|
|
||||||
icon: UserIcon,
|
|
||||||
list: UserList,
|
|
||||||
edit: UserEdit,
|
|
||||||
create: UserCreate,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resource;
|
|
@ -7,7 +7,6 @@ const de = {
|
|||||||
base_url: "Heimserver URL",
|
base_url: "Heimserver URL",
|
||||||
welcome: "Willkommen bei Synapse-admin",
|
welcome: "Willkommen bei Synapse-admin",
|
||||||
server_version: "Synapse Version",
|
server_version: "Synapse Version",
|
||||||
supports_specs: "unterstützt Matrix-Specs",
|
|
||||||
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",
|
||||||
@ -189,7 +188,7 @@ const de = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: {
|
reports: {
|
||||||
name: "Gemeldetes Ereignis |||| Gemeldete Ereignisse",
|
name: "Ereignisbericht |||| Ereignisberichte",
|
||||||
fields: {
|
fields: {
|
||||||
id: "ID",
|
id: "ID",
|
||||||
received_ts: "Meldezeit",
|
received_ts: "Meldezeit",
|
||||||
@ -211,13 +210,6 @@ const de = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Gemeldetes Event löschen",
|
|
||||||
content:
|
|
||||||
"Sind Sie sicher dass Sie das gemeldete Event löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
name: "Verbindungen",
|
name: "Verbindungen",
|
||||||
|
@ -7,7 +7,6 @@ const en = {
|
|||||||
base_url: "Homeserver URL",
|
base_url: "Homeserver URL",
|
||||||
welcome: "Welcome to Synapse-admin",
|
welcome: "Welcome to Synapse-admin",
|
||||||
server_version: "Synapse version",
|
server_version: "Synapse version",
|
||||||
supports_specs: "supports Matrix specs",
|
|
||||||
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",
|
||||||
@ -208,13 +207,6 @@ const en = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Delete reported event",
|
|
||||||
content:
|
|
||||||
"Are you sure you want to delete the reported event? This cannot be undone.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
name: "Connections",
|
name: "Connections",
|
||||||
|
382
src/i18n/fa.js
382
src/i18n/fa.js
@ -1,382 +0,0 @@
|
|||||||
import farsiMessages from "ra-language-farsi";
|
|
||||||
|
|
||||||
const fa = {
|
|
||||||
...farsiMessages,
|
|
||||||
synapseadmin: {
|
|
||||||
auth: {
|
|
||||||
base_url: "آدرس سرور",
|
|
||||||
welcome: "به پنل مدیریت سیناپس خوش آمدید!",
|
|
||||||
server_version: "نسخه",
|
|
||||||
username_error: "لطفاً شناسه کاربر را وارد کنید: '@user:domain'",
|
|
||||||
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
|
|
||||||
url_error: "آدرس وارد شده یک سرور معتبر نیست",
|
|
||||||
sso_sign_in: "با SSO وارد شوید",
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
|
||||||
tabs: { sso: "SSO" },
|
|
||||||
},
|
|
||||||
rooms: {
|
|
||||||
tabs: {
|
|
||||||
basic: "اصلی",
|
|
||||||
members: "اعضا",
|
|
||||||
detail: "جزئیات",
|
|
||||||
permission: "مجوزها",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
|
|
||||||
},
|
|
||||||
import_users: {
|
|
||||||
error: {
|
|
||||||
at_entry: "در هنگام ورود %{entry}: %{message}",
|
|
||||||
error: "Error",
|
|
||||||
required_field: "فیلد الزامی '%{field}' وجود ندارد",
|
|
||||||
invalid_value:
|
|
||||||
"خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
|
|
||||||
unreasonably_big:
|
|
||||||
"از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
|
|
||||||
already_in_progress: "یک بارگذاری از قبل در حال انجام است",
|
|
||||||
id_exits: "شناسه %{id} موجود است",
|
|
||||||
},
|
|
||||||
title: "کاربران را از طریق فایل CSV وارد کنید",
|
|
||||||
goToPdf: "رفتن به PDF",
|
|
||||||
cards: {
|
|
||||||
importstats: {
|
|
||||||
header: "وارد کردن کاربران",
|
|
||||||
users_total:
|
|
||||||
"%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
|
|
||||||
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
|
|
||||||
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
|
|
||||||
},
|
|
||||||
conflicts: {
|
|
||||||
header: "استراتژی متغارض",
|
|
||||||
mode: {
|
|
||||||
stop: "توقف",
|
|
||||||
skip: "نمایش خطا و رد شدن",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ids: {
|
|
||||||
header: "شناسنامه ها",
|
|
||||||
all_ids_present: "شناسه های موجود در هر ورودی",
|
|
||||||
count_ids_present:
|
|
||||||
"%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
|
|
||||||
mode: {
|
|
||||||
ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
|
|
||||||
update: "سوابق موجود را به روز کنید",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
passwords: {
|
|
||||||
header: "رمز عبور",
|
|
||||||
all_passwords_present: "رمزهای عبور موجود در هر ورودی",
|
|
||||||
count_passwords_present:
|
|
||||||
"%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
|
|
||||||
use_passwords: "از پسوردهای CSV استفاده کنید",
|
|
||||||
},
|
|
||||||
upload: {
|
|
||||||
header: "Input CSV file",
|
|
||||||
explanation:
|
|
||||||
"در اینجا می توانید فایلی را با مقادیر جدا شده با کاما بارگذاری کنید که برای ایجاد یا به روز رسانی کاربران پردازش می شود. فایل باید شامل فیلدهای 'id' و 'displayname' باشد. می توانید یک فایل نمونه را از اینجا دانلود و تطبیق دهید: ",
|
|
||||||
},
|
|
||||||
startImport: {
|
|
||||||
simulate_only: "فقط شبیه سازی",
|
|
||||||
run_import: "بارگذاری",
|
|
||||||
},
|
|
||||||
results: {
|
|
||||||
header: "بارگذاری نتایج",
|
|
||||||
total: "%{smart_count} ورودی در کل |||| %{smart_count} ورودی ها در کل",
|
|
||||||
successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
|
|
||||||
skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
|
|
||||||
download_skipped: "دانلود رکوردهای نادیده گرفته شده",
|
|
||||||
with_error:
|
|
||||||
"%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
|
|
||||||
simulated_only: "اجرا فقط شبیه سازی شد",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resources: {
|
|
||||||
users: {
|
|
||||||
name: "کاربر |||| کاربران",
|
|
||||||
email: "ایمیل",
|
|
||||||
msisdn: "شماره تلفن",
|
|
||||||
threepid: "ایمیل / شماره تلفن",
|
|
||||||
fields: {
|
|
||||||
avatar: "آواتار",
|
|
||||||
id: "شناسه کاربر",
|
|
||||||
name: "نام",
|
|
||||||
is_guest: "مهمان",
|
|
||||||
admin: "مدیر سرور",
|
|
||||||
deactivated: "غیرفعال",
|
|
||||||
guests: "نمایش مهمانان",
|
|
||||||
show_deactivated: "نمایش کاربران غیرفعال شده",
|
|
||||||
user_id: "جستجوی کاربر",
|
|
||||||
displayname: "نام نمایشی",
|
|
||||||
password: "رمز عبور",
|
|
||||||
avatar_url: "آواتار سرور",
|
|
||||||
avatar_src: "آواتار",
|
|
||||||
medium: "متوسط",
|
|
||||||
threepids: "سرویس احراز هویت",
|
|
||||||
address: "آدرس",
|
|
||||||
creation_ts_ms: "ساخته شده در",
|
|
||||||
consent_version: "Consent نسخه",
|
|
||||||
auth_provider: "ارائه دهنده",
|
|
||||||
user_type: "نوع کاربر",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
|
||||||
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
|
||||||
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: "پاک کردن اطلاعات کاربر",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rooms: {
|
|
||||||
name: "اتاق |||| اتاق ها",
|
|
||||||
fields: {
|
|
||||||
room_id: "شناسه اتاق",
|
|
||||||
name: "نام",
|
|
||||||
canonical_alias: "نام مستعار",
|
|
||||||
joined_members: "اعضا",
|
|
||||||
joined_local_members: "اعضای محلی",
|
|
||||||
joined_local_devices: "دستگاه های محلی",
|
|
||||||
state_events: "رویدادهای حالت / پیچیدگی",
|
|
||||||
version: "نسخه",
|
|
||||||
is_encrypted: "رمزگذاری شده است",
|
|
||||||
encryption: "رمزگذاری",
|
|
||||||
federatable: "Federatable",
|
|
||||||
public: "قابل مشاهده در فهرست اتاق",
|
|
||||||
creator: "سازنده",
|
|
||||||
join_rules: "به قوانین بپیوندید",
|
|
||||||
guest_access: "دسترسی مهمان",
|
|
||||||
history_visibility: "مشاهده تاریخچه",
|
|
||||||
topic: "موضوع",
|
|
||||||
avatar: "آواتار",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
forward_extremities:
|
|
||||||
"اندام های رو به جلو، رویدادهای برگ در انتهای نمودار غیر چرخه ای جهت دار (DAG) در یک اتاق هستند، رویدادهایی که فرزند ندارند. هر چه تعداد بیشتری در یک اتاق وجود داشته باشد، وضوح حالت بیشتری را که سیناپس باید انجام دهد (نکته: این یک عملیات گران است). در حالی که Synapse کدی برای جلوگیری از وجود تعداد زیادی از این موارد در یک زمان در اتاق دارد، گاهی اوقات باگها میتوانند دوباره ظاهر شوند. اگر اتاقی بیش از 10 انتهای رو به جلو دارد، بهتر است بررسی کنید که کدام اتاق مقصر است و احتمالاً آنها را با استفاده از جستارهای SQL ذکر شده در آن حذف کنید. #1760.",
|
|
||||||
},
|
|
||||||
enums: {
|
|
||||||
join_rules: {
|
|
||||||
public: "عمومی",
|
|
||||||
knock: "در زدن",
|
|
||||||
invite: "دعوت کردن",
|
|
||||||
private: "خصوصی",
|
|
||||||
},
|
|
||||||
guest_access: {
|
|
||||||
can_join: "مهمانان می توانند ملحق شوند",
|
|
||||||
forbidden: "مهمانان نمی توانند ملحق شوند",
|
|
||||||
},
|
|
||||||
history_visibility: {
|
|
||||||
invited: "از آنجایی که دعوت شده است",
|
|
||||||
joined: "از زمانی که پیوست",
|
|
||||||
shared: "از آنجایی که به اشتراک گذاشته شده است",
|
|
||||||
world_readable: "هر کسی",
|
|
||||||
},
|
|
||||||
unencrypted: "رمزگذاری نشده",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "حذف اتاق",
|
|
||||||
content:
|
|
||||||
"آیا مطمئن هستید که می خواهید اتاق را حذف کنید؟ این قابل بازگشت نیست. همه پیام ها و رسانه های مشترک در اتاق از سرور حذف می شوند!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reports: {
|
|
||||||
name: "رویداد گزارش شده |||| رویدادهای گزارش شده",
|
|
||||||
fields: {
|
|
||||||
id: "شناسه",
|
|
||||||
received_ts: "زمان گزارش",
|
|
||||||
user_id: "گوینده",
|
|
||||||
name: "نام اتاق",
|
|
||||||
score: "نمره",
|
|
||||||
reason: "دلیل",
|
|
||||||
event_id: "شناسه رویداد",
|
|
||||||
event_json: {
|
|
||||||
origin: "سرور مبدا",
|
|
||||||
origin_server_ts: "زمان ارسال",
|
|
||||||
type: "نوع رویداد",
|
|
||||||
content: {
|
|
||||||
msgtype: "نوع محتوا",
|
|
||||||
body: "محتوا",
|
|
||||||
format: "قالب",
|
|
||||||
formatted_body: "محتوای قالب بندی شده",
|
|
||||||
algorithm: "الگوریتم",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
connections: {
|
|
||||||
name: "اتصالات",
|
|
||||||
fields: {
|
|
||||||
last_seen: "تاریخ",
|
|
||||||
ip: "آدرس آی پی",
|
|
||||||
user_agent: "نماینده کاربر",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devices: {
|
|
||||||
name: "دستگاه |||| دستگاه ها",
|
|
||||||
fields: {
|
|
||||||
device_id: "شناسه دستگاه",
|
|
||||||
display_name: "نام دستگاه",
|
|
||||||
last_seen_ts: "مهر زمان",
|
|
||||||
last_seen_ip: "آدرس آی پی",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "حذف کردن %{id}",
|
|
||||||
content:
|
|
||||||
'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
|
|
||||||
success: "دستگاه با موفقیت حذف شد.",
|
|
||||||
failure: "خطایی رخ داده است.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users_media: {
|
|
||||||
name: "رسانه ها",
|
|
||||||
fields: {
|
|
||||||
media_id: "شناسه رسانه",
|
|
||||||
media_length: "اندازه فایل (به بایت)",
|
|
||||||
media_type: "نوع",
|
|
||||||
upload_name: "نام فایل",
|
|
||||||
quarantined_by: "قرنطینه شده توسط",
|
|
||||||
safe_from_quarantine: "امان از قرنطینه",
|
|
||||||
created_ts: "ایجاد شده",
|
|
||||||
last_access_ts: "آخرین دسترسی",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete_media: {
|
|
||||||
name: "رسانه ها",
|
|
||||||
fields: {
|
|
||||||
before_ts: "آخرین دسترسی قبل",
|
|
||||||
size_gt: "بزرگتر از آن (به بایت)",
|
|
||||||
keep_profiles: "تصاویر پروفایل را نگه دارید",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
send: "حذف رسانه ها",
|
|
||||||
send_success: "درخواست با موفقیت ارسال شد.",
|
|
||||||
send_failure: "خطایی رخ داده است.",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانههایی که در مخازن رسانه خارجی آپلود شدهاند تأثیری نخواهد گذاشت.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protect_media: {
|
|
||||||
action: {
|
|
||||||
create: "محافظت نشده، حفاظت ایجاد کنید",
|
|
||||||
delete: "محافظت شده، حفاظت را بردارید",
|
|
||||||
none: "در قرنطینه",
|
|
||||||
send_success: "وضعیت حفاظت با موفقیت تغییر کرد.",
|
|
||||||
send_failure: "خطایی رخ داده است.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
quarantine_media: {
|
|
||||||
action: {
|
|
||||||
name: "قرنطینه",
|
|
||||||
create: "به قرنطینه اضافه کنید",
|
|
||||||
delete: "در قرنطینه، غیر قرنطینه",
|
|
||||||
none: "از قرنطینه محافظت می شود",
|
|
||||||
send_success: "وضعیت قرنطینه با موفقیت تغییر کرد.",
|
|
||||||
send_failure: "خطایی رخ داده است.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pushers: {
|
|
||||||
name: "هل دهنده |||| هل دهنده ها",
|
|
||||||
fields: {
|
|
||||||
app: "برنامه",
|
|
||||||
app_display_name: "نام نمایش برنامه",
|
|
||||||
app_id: "شناسه برنامه",
|
|
||||||
device_display_name: "نام نمایشی برنامه",
|
|
||||||
kind: "نوع",
|
|
||||||
lang: "زبان",
|
|
||||||
profile_tag: "برچسب پروفایل",
|
|
||||||
pushkey: "کلید",
|
|
||||||
data: { url: "URL" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
servernotices: {
|
|
||||||
name: "اطلاعیه های سرور",
|
|
||||||
send: "ارسال اعلانات سرور",
|
|
||||||
fields: {
|
|
||||||
body: "پیام",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
send: "ارسال یادداشت",
|
|
||||||
send_success: "اعلان سرور با موفقیت ارسال شد.",
|
|
||||||
send_failure: "خطایی رخ داده است.",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
send: "اعلان سرور را برای کاربران انتخاب شده ارسال می کند. ویژگی 'اعلامیه های سرور' باید در سرور فعال شود.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user_media_statistics: {
|
|
||||||
name: "رسانه کاربران",
|
|
||||||
fields: {
|
|
||||||
media_count: "شمارش رسانه ها",
|
|
||||||
media_length: "طول رسانه",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
forward_extremities: {
|
|
||||||
name: "Forward Extremities",
|
|
||||||
fields: {
|
|
||||||
id: "شناسه رویداد",
|
|
||||||
received_ts: "مهر زمان",
|
|
||||||
depth: "عمق",
|
|
||||||
state_group: "گروه دولتی",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_state: {
|
|
||||||
name: "رویدادهای وضعیت",
|
|
||||||
fields: {
|
|
||||||
type: "نوع",
|
|
||||||
content: "محتوا",
|
|
||||||
origin_server_ts: "زمان ارسال",
|
|
||||||
sender: "فرستنده",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_directory: {
|
|
||||||
name: "راهنمای اتاق",
|
|
||||||
fields: {
|
|
||||||
world_readable: "کاربران مهمان می توانند بدون عضویت مشاهده کنند",
|
|
||||||
guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
title:
|
|
||||||
"اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
|
|
||||||
content:
|
|
||||||
"آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{smart_count} از راهنمای اتاق ها حذف کنید؟",
|
|
||||||
erase: "حذف از فهرست اتاق",
|
|
||||||
create: "انتشار در راهنما اتاق",
|
|
||||||
send_success: "اتاق با موفقیت منتشر شد.",
|
|
||||||
send_failure: "خطایی رخ داده است.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destinations: {
|
|
||||||
name: "سرور های مرتبط",
|
|
||||||
fields: {
|
|
||||||
destination: "آدرس",
|
|
||||||
failure_ts: "زمان شکست",
|
|
||||||
retry_last_ts: "آخرین زمان اتصال",
|
|
||||||
retry_interval: "بازه امتحان مجدد",
|
|
||||||
last_successful_stream_ordering: "آخرین جریان موفق",
|
|
||||||
stream_ordering: "جریان",
|
|
||||||
},
|
|
||||||
action: { reconnect: "دوباره وصل شوید" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
registration_tokens: {
|
|
||||||
name: "توکن های ثبت نام",
|
|
||||||
fields: {
|
|
||||||
token: "توکن",
|
|
||||||
valid: "توکن معتبر",
|
|
||||||
uses_allowed: "موارد استفاده مجاز",
|
|
||||||
pending: "انتظار",
|
|
||||||
completed: "تکمیل شد",
|
|
||||||
expiry_time: "زمان انقضا",
|
|
||||||
length: "طول",
|
|
||||||
},
|
|
||||||
helper: { length: "طول توکن در صورت عدم ارائه توکن." },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default fa;
|
|
385
src/i18n/it.js
385
src/i18n/it.js
@ -1,385 +0,0 @@
|
|||||||
import italianMessages from "ra-language-italian";
|
|
||||||
|
|
||||||
const it = {
|
|
||||||
...italianMessages,
|
|
||||||
synapseadmin: {
|
|
||||||
auth: {
|
|
||||||
base_url: "URL dell'homeserver",
|
|
||||||
welcome: "Benvenuto in Synapse-admin",
|
|
||||||
server_version: "Versione di Synapse",
|
|
||||||
username_error:
|
|
||||||
"Per favore inserisci un ID utente completo: '@utente:dominio'",
|
|
||||||
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
|
||||||
url_error: "URL del server Matrix non valido",
|
|
||||||
sso_sign_in: "Accedi con SSO",
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
invalid_user_id: "ID utente non valido su questo homeserver.",
|
|
||||||
tabs: { sso: "SSO" },
|
|
||||||
},
|
|
||||||
rooms: {
|
|
||||||
tabs: {
|
|
||||||
basic: "Semplice",
|
|
||||||
members: "Membro",
|
|
||||||
detail: "Dettagli",
|
|
||||||
permission: "Permessi",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reports: { tabs: { basic: "Semplice", detail: "Dettagli" } },
|
|
||||||
},
|
|
||||||
import_users: {
|
|
||||||
error: {
|
|
||||||
at_entry: "Alla voce %{entry}: %{message}",
|
|
||||||
error: "Errore",
|
|
||||||
required_field: "Il campo '%{field}' non è presente",
|
|
||||||
invalid_value:
|
|
||||||
"Valore non valido alla riga %{row}. '%{field}' Il campo può essere solo 'true' o 'false'",
|
|
||||||
unreasonably_big:
|
|
||||||
"Impossibile caricare un file così grosso (%{size} megabyte)",
|
|
||||||
already_in_progress: "Un import è attualmente già in caricamento",
|
|
||||||
id_exits: "L'ID %{id} è già presente",
|
|
||||||
},
|
|
||||||
title: "Importa utenti tramite file CSV",
|
|
||||||
goToPdf: "Vai al PDF",
|
|
||||||
cards: {
|
|
||||||
importstats: {
|
|
||||||
header: "Importa utenti",
|
|
||||||
users_total:
|
|
||||||
"%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV",
|
|
||||||
guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti",
|
|
||||||
admin_count:
|
|
||||||
"%{smart_count} amministratore |||| %{smart_count} amministratori",
|
|
||||||
},
|
|
||||||
conflicts: {
|
|
||||||
header: "Strategia di conflitto",
|
|
||||||
mode: {
|
|
||||||
stop: "Stoppa al conflitto",
|
|
||||||
skip: "Mostra l'errore e ignora il conflitto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ids: {
|
|
||||||
header: "ID",
|
|
||||||
all_ids_present: "ID presenti in ogni voce",
|
|
||||||
count_ids_present:
|
|
||||||
"%{smart_count} voce con ID |||| %{smart_count} voci con ID",
|
|
||||||
mode: {
|
|
||||||
ignore: "Ignora gli ID nel file CSV e creane di nuovi",
|
|
||||||
update: "Aggiorna le voci esistenti",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
passwords: {
|
|
||||||
header: "Passwords",
|
|
||||||
all_passwords_present: "Password presenti in ogni voce",
|
|
||||||
count_passwords_present:
|
|
||||||
"%{smart_count} voce con password |||| %{smart_count} voci con password",
|
|
||||||
use_passwords: "Usa le password dal file CSV",
|
|
||||||
},
|
|
||||||
upload: {
|
|
||||||
header: "Input file CSV",
|
|
||||||
explanation:
|
|
||||||
"Qui puoi caricare un file con valori separati da virgole che verrà poi utilizzato per creare o aggiornare gli utenti. Il file deve includere i campi 'id' and 'displayname'. Puoi scaricare un file di esempio per adattarlo: ",
|
|
||||||
},
|
|
||||||
startImport: {
|
|
||||||
simulate_only: "Solo simulazione",
|
|
||||||
run_import: "Importa",
|
|
||||||
},
|
|
||||||
results: {
|
|
||||||
header: "Importa i risultati",
|
|
||||||
total:
|
|
||||||
"%{smart_count} voce in totale |||| %{smart_count} voci in totale",
|
|
||||||
successful: "%{smart_count} voci importate con successo",
|
|
||||||
skipped: "%{smart_count} voci ignorate",
|
|
||||||
download_skipped: "Scarica le voci ignorate",
|
|
||||||
with_error:
|
|
||||||
"%{smart_count} voce con errori ||| %{smart_count} voci con errori",
|
|
||||||
simulated_only: "Il processo era stato solamente simulato",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resources: {
|
|
||||||
users: {
|
|
||||||
name: "Utente |||| Utenti",
|
|
||||||
email: "Email",
|
|
||||||
msisdn: "Telefono",
|
|
||||||
threepid: "Email / Telefono",
|
|
||||||
fields: {
|
|
||||||
avatar: "Avatar",
|
|
||||||
id: "ID utente",
|
|
||||||
name: "Nome",
|
|
||||||
is_guest: "Ospite",
|
|
||||||
admin: "Amministratore",
|
|
||||||
deactivated: "Disattivato",
|
|
||||||
guests: "Mostra gli ospiti",
|
|
||||||
show_deactivated: "Mostra gli utenti disattivati",
|
|
||||||
user_id: "Cerca utente",
|
|
||||||
displayname: "Nickname",
|
|
||||||
password: "Password",
|
|
||||||
avatar_url: "URL dell'avatar",
|
|
||||||
avatar_src: "Avatar",
|
|
||||||
medium: "Medium",
|
|
||||||
threepids: "3PID",
|
|
||||||
address: "Indirizzo",
|
|
||||||
creation_ts_ms: "Creazione del timestamp",
|
|
||||||
consent_version: "Versione minima richiesta",
|
|
||||||
auth_provider: "Provider",
|
|
||||||
user_type: "Tipo d'utente",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
password:
|
|
||||||
"Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
|
||||||
deactivate: "Devi fornire una password per riattivare l'account.",
|
|
||||||
erase: "Constrassegna l'utente come cancellato dal GDPR",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: "Cancella i dati dell'utente",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rooms: {
|
|
||||||
name: "Stanza |||| Stanze",
|
|
||||||
fields: {
|
|
||||||
room_id: "ID della stanza",
|
|
||||||
name: "Nome",
|
|
||||||
canonical_alias: "Alias",
|
|
||||||
joined_members: "Membri",
|
|
||||||
joined_local_members: "Membri locali",
|
|
||||||
joined_local_devices: "Dispositivi locali",
|
|
||||||
state_events: "Eventi di stato / Complessità",
|
|
||||||
version: "Versione",
|
|
||||||
is_encrypted: "Criptato",
|
|
||||||
encryption: "Crittografia",
|
|
||||||
federatable: "Federabile",
|
|
||||||
public: "Visibile nella cartella della stanza",
|
|
||||||
creator: "Creatore",
|
|
||||||
join_rules: "Regole per entrare",
|
|
||||||
guest_access: "Entra come ospite",
|
|
||||||
history_visibility: "Visibilità temporale",
|
|
||||||
topic: "Topic",
|
|
||||||
avatar: "Avatar",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
/* forward_extremities:
|
|
||||||
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
|
|
||||||
},
|
|
||||||
enums: {
|
|
||||||
join_rules: {
|
|
||||||
public: "Pubblica",
|
|
||||||
knock: "Bussa",
|
|
||||||
invite: "Invita",
|
|
||||||
private: "Privata",
|
|
||||||
},
|
|
||||||
guest_access: {
|
|
||||||
can_join: "Gli utenti ospiti possono entrare",
|
|
||||||
forbidden: "Gli utenti ospiti non possono entrare",
|
|
||||||
},
|
|
||||||
history_visibility: {
|
|
||||||
invited: "Dall'invito",
|
|
||||||
joined: "Dall'entrata",
|
|
||||||
shared: "Dalla condivisione",
|
|
||||||
world_readable: "Chiunque",
|
|
||||||
},
|
|
||||||
unencrypted: "Non criptata",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Cancella stanza",
|
|
||||||
content:
|
|
||||||
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reports: {
|
|
||||||
name: "Evento segnalato |||| Eventi segnalati",
|
|
||||||
fields: {
|
|
||||||
id: "ID",
|
|
||||||
received_ts: "Orario del report",
|
|
||||||
user_id: "richiedente",
|
|
||||||
name: "nome della stanza",
|
|
||||||
score: "punteggio",
|
|
||||||
reason: "ragione",
|
|
||||||
event_id: "ID dell'evento",
|
|
||||||
event_json: {
|
|
||||||
origin: "server di origine",
|
|
||||||
origin_server_ts: "ora dell'invio",
|
|
||||||
type: "tipo di evento",
|
|
||||||
content: {
|
|
||||||
msgtype: "tipo di contenuto",
|
|
||||||
body: "contenuto",
|
|
||||||
format: "formato",
|
|
||||||
formatted_body: "contenuto formattato",
|
|
||||||
algorithm: "algoritmo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
connections: {
|
|
||||||
name: "Connessioni",
|
|
||||||
fields: {
|
|
||||||
last_seen: "Data",
|
|
||||||
ip: "Indirizzo IP",
|
|
||||||
user_agent: "agente utente",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devices: {
|
|
||||||
name: "Dispositivo |||| Dispositivi",
|
|
||||||
fields: {
|
|
||||||
device_id: "ID del dispositivo",
|
|
||||||
display_name: "Nome del dispositivo",
|
|
||||||
last_seen_ts: "Timestamp",
|
|
||||||
last_seen_ip: "Indirizzo IP",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Rimozione del dispositivo %{id}",
|
|
||||||
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
|
|
||||||
success: "Dispositivo rimosso con successo.",
|
|
||||||
failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users_media: {
|
|
||||||
name: "Media",
|
|
||||||
fields: {
|
|
||||||
media_id: "ID del media",
|
|
||||||
media_length: "Peso del file (in Byte)",
|
|
||||||
media_type: "Tipo",
|
|
||||||
upload_name: "Nome del file",
|
|
||||||
quarantined_by: "In quarantena da",
|
|
||||||
safe_from_quarantine: "Protetto dalla quarantena",
|
|
||||||
created_ts: "Creato",
|
|
||||||
last_access_ts: "Ultimo accesso",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete_media: {
|
|
||||||
name: "Media",
|
|
||||||
fields: {
|
|
||||||
before_ts: "ultimo accesso effettuato prima",
|
|
||||||
size_gt: "Più grande di (in byte)",
|
|
||||||
keep_profiles: "Mantieni le immagini del profilo",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
send: "Cancella media",
|
|
||||||
send_success: "Richiesta inviata con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protect_media: {
|
|
||||||
action: {
|
|
||||||
create: "Non protetto, proteggi",
|
|
||||||
delete: "Protetto, rimuovi protezione",
|
|
||||||
none: "In quarantena",
|
|
||||||
send_success: "Stato della protezione cambiato con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
quarantine_media: {
|
|
||||||
action: {
|
|
||||||
name: "Quarantina",
|
|
||||||
create: "Aggiungi alla quarantena",
|
|
||||||
delete: "In quarantena, rimuovi dalla quarantena",
|
|
||||||
none: "Protetto dalla quarantena",
|
|
||||||
send_success: "Stato della quarantena cambiato con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pushers: {
|
|
||||||
name: "Pusher |||| Pusher",
|
|
||||||
fields: {
|
|
||||||
app: "App",
|
|
||||||
app_display_name: "Nome dell'app",
|
|
||||||
app_id: "ID dell'app",
|
|
||||||
device_display_name: "Nome del dispositivo",
|
|
||||||
kind: "Tipo",
|
|
||||||
lang: "Lingua",
|
|
||||||
profile_tag: "Tag del profilo",
|
|
||||||
pushkey: "Pushkey",
|
|
||||||
data: { url: "URL" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
servernotices: {
|
|
||||||
name: "Avvisi del server",
|
|
||||||
send: "Invia avvisi",
|
|
||||||
fields: {
|
|
||||||
body: "Messaggio",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
send: "Invia nota",
|
|
||||||
send_success: "Avviso inviato con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user_media_statistics: {
|
|
||||||
name: "Media degli utenti",
|
|
||||||
fields: {
|
|
||||||
media_count: "Numero media",
|
|
||||||
media_length: "Lunghezza media",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
forward_extremities: {
|
|
||||||
name: "Invia estremità",
|
|
||||||
fields: {
|
|
||||||
id: "Event ID",
|
|
||||||
received_ts: "Timestamp",
|
|
||||||
depth: "Profondità",
|
|
||||||
state_group: "State group",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_state: {
|
|
||||||
name: "Eventi di stato",
|
|
||||||
fields: {
|
|
||||||
type: "Tipo",
|
|
||||||
content: "Contenuto",
|
|
||||||
origin_server_ts: "Ora dell'invio",
|
|
||||||
sender: "Mittente",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_directory: {
|
|
||||||
name: "Elenco delle stanze",
|
|
||||||
fields: {
|
|
||||||
world_readable: "gli utenti ospite possono vedere senza entrare",
|
|
||||||
guest_can_join: "gli utenti ospite possono entrare",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
title:
|
|
||||||
"Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
|
|
||||||
content:
|
|
||||||
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
|
|
||||||
erase: "Rimuovi dall'elenco",
|
|
||||||
create: "Crea",
|
|
||||||
send_success: "Stanza creata con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destinations: {
|
|
||||||
name: "Federazione",
|
|
||||||
fields: {
|
|
||||||
destination: "Destinazione",
|
|
||||||
failure_ts: "Timestamp dell'errore",
|
|
||||||
retry_last_ts: "Tentativo ultimo timestamp",
|
|
||||||
retry_interval: "Intervallo dei tentativi",
|
|
||||||
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
|
|
||||||
stream_ordering: "Flusso",
|
|
||||||
},
|
|
||||||
action: { reconnect: "Riconnetti" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
registration_tokens: {
|
|
||||||
name: "Token di registrazione",
|
|
||||||
fields: {
|
|
||||||
token: "Token",
|
|
||||||
valid: "Token valido",
|
|
||||||
uses_allowed: "Usi permessi",
|
|
||||||
pending: "In attesa",
|
|
||||||
completed: "Completato",
|
|
||||||
expiry_time: "Data della scadenza",
|
|
||||||
length: "Lunghezza",
|
|
||||||
},
|
|
||||||
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default it;
|
|
390
src/i18n/ru.js
390
src/i18n/ru.js
@ -1,390 +0,0 @@
|
|||||||
import russianMessages from "ra-language-russian";
|
|
||||||
|
|
||||||
const ru = {
|
|
||||||
...russianMessages,
|
|
||||||
synapseadmin: {
|
|
||||||
auth: {
|
|
||||||
base_url: "Домашняя страница",
|
|
||||||
welcome: "Добро пожаловать в Synapse-admin",
|
|
||||||
server_version: "Версия Synapse",
|
|
||||||
supports_specs: "поддерживает спецификации Matrix",
|
|
||||||
username_error: "Введите полный идентификатор пользователя: '@user:domain'",
|
|
||||||
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
|
|
||||||
url_error: "Некорректный сервер Matrix",
|
|
||||||
sso_sign_in: "Присоединиться с помощью SSO",
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
invalid_user_id: "Локальная часть идентификатора пользователя Matrix без домашнего сервера.",
|
|
||||||
tabs: { sso: "SSO" },
|
|
||||||
},
|
|
||||||
rooms: {
|
|
||||||
tabs: {
|
|
||||||
basic: "Основное",
|
|
||||||
members: "Участники",
|
|
||||||
detail: "Подробности",
|
|
||||||
permission: "Права",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reports: { tabs: { basic: "Основное", detail: "Подробности" } },
|
|
||||||
},
|
|
||||||
import_users: {
|
|
||||||
error: {
|
|
||||||
at_entry: "При входе %{entry}: %{message}",
|
|
||||||
error: "Ошибка",
|
|
||||||
required_field: "Обязательное поле '%{field}' не представленно",
|
|
||||||
invalid_value:
|
|
||||||
"Недопустимое значение в строке %{row}. '%{field}' поле может быть только 'true' или 'false'",
|
|
||||||
unreasonably_big:
|
|
||||||
"Отказался загружать неоправданно большой файл %{size} Мбайт",
|
|
||||||
already_in_progress: "Импорт уже запущен",
|
|
||||||
id_exits: "Идентификатор %{id} уже существует",
|
|
||||||
},
|
|
||||||
title: "Импорт пользователей из CSV",
|
|
||||||
goToPdf: "В PDF",
|
|
||||||
cards: {
|
|
||||||
importstats: {
|
|
||||||
header: "Импорт пользователей",
|
|
||||||
users_total:
|
|
||||||
"%{smart_count} пользователь в CSV файл |||| %{smart_count} пользователей в CSV файл",
|
|
||||||
guest_count: "%{smart_count} гость |||| %{smart_count} гостей",
|
|
||||||
admin_count: "%{smart_count} администратор |||| %{smart_count} администраторов",
|
|
||||||
},
|
|
||||||
conflicts: {
|
|
||||||
header: "Решение конфликтов",
|
|
||||||
mode: {
|
|
||||||
stop: "Остановиться, если конфликт произошел",
|
|
||||||
skip: "Вывести ошибку и пропустить конфликт",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ids: {
|
|
||||||
header: "Идентификаторы",
|
|
||||||
all_ids_present: "Идентификаторы представлены для каждой записи",
|
|
||||||
count_ids_present:
|
|
||||||
"%{smart_count} запись с идентификатором |||| %{smart_count} записей с идентификатором",
|
|
||||||
mode: {
|
|
||||||
ignore: "Игнорировать идентификаторы в CSV и создавать новые",
|
|
||||||
update: "Обновлять существующие записи",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
passwords: {
|
|
||||||
header: "Пароли",
|
|
||||||
all_passwords_present: "Пароли представлены для каждой записи",
|
|
||||||
count_passwords_present:
|
|
||||||
"%{smart_count} запись с паролем |||| %{smart_count} записей с паролем",
|
|
||||||
use_passwords: "Использовать пароли из CSV",
|
|
||||||
},
|
|
||||||
upload: {
|
|
||||||
header: "Загрузка CSV файла",
|
|
||||||
explanation:
|
|
||||||
"Здесь вы можете загрузить файл со значениями, разделенными запятыми, который будет обработан для создания или обновления пользователей. Файл должен содержать поля 'id' и 'displayname'. Вы можете загрузить пример здесь:",
|
|
||||||
},
|
|
||||||
startImport: {
|
|
||||||
simulate_only: "Не выполнять реальных действий",
|
|
||||||
run_import: "Импорт",
|
|
||||||
},
|
|
||||||
results: {
|
|
||||||
header: "Импорт результатов",
|
|
||||||
total:
|
|
||||||
"Всего %{smart_count} запись |||| Всего %{smart_count} записей",
|
|
||||||
successful: "%{smart_count} записей успешно импортировано",
|
|
||||||
skipped: "%{smart_count} записей пропущено",
|
|
||||||
download_skipped: "Загрузить пропущенные записи",
|
|
||||||
with_error:
|
|
||||||
"%{smart_count} запись с ошибками ||| %{smart_count} записей с ошибками",
|
|
||||||
simulated_only: "Результат не будет сохранен",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resources: {
|
|
||||||
users: {
|
|
||||||
name: "Пользователей |||| Пользователи",
|
|
||||||
email: "Почта",
|
|
||||||
msisdn: "Телефон",
|
|
||||||
threepid: "Почта / Телефон",
|
|
||||||
fields: {
|
|
||||||
avatar: "Аватар",
|
|
||||||
id: "Идентификатор пользователя",
|
|
||||||
name: "Имя",
|
|
||||||
is_guest: "Гость",
|
|
||||||
admin: "Администратор",
|
|
||||||
deactivated: "Деактивирован",
|
|
||||||
guests: "Показать гостей",
|
|
||||||
show_deactivated: "Показать деактивированных пользователей",
|
|
||||||
user_id: "Найти пользователя",
|
|
||||||
displayname: "Отображаемое имя",
|
|
||||||
password: "Пароль",
|
|
||||||
avatar_url: "Ссылка на аватар",
|
|
||||||
avatar_src: "Аватар",
|
|
||||||
medium: "Тип",
|
|
||||||
threepids: "Иной идентификатор",
|
|
||||||
address: "Адрес",
|
|
||||||
creation_ts_ms: "Время создания",
|
|
||||||
consent_version: "Версия соглашения",
|
|
||||||
auth_provider: "Поставщик",
|
|
||||||
user_type: "Тип пользователя",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
password: "Изменение пароля приведет к выходу пользователя из всех сеансов.",
|
|
||||||
deactivate: "Вы должны ввести пароль для повторной активации учетной записи.",
|
|
||||||
erase: "Пометить пользователя как удаленного в связи с защитой персональных данных",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: "Удаление пользовательских данных",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rooms: {
|
|
||||||
name: "Комнат |||| Комнаты",
|
|
||||||
fields: {
|
|
||||||
room_id: "Идентификатор комнаты",
|
|
||||||
name: "Название",
|
|
||||||
canonical_alias: "Псевдоним",
|
|
||||||
joined_members: "Участники",
|
|
||||||
joined_local_members: "Внутренние участники",
|
|
||||||
joined_local_devices: "Используемые устройства",
|
|
||||||
state_events: "События изменения состояния",
|
|
||||||
version: "Версия",
|
|
||||||
is_encrypted: "Зашифрованно",
|
|
||||||
encryption: "Шифрование",
|
|
||||||
federatable: "Федерация",
|
|
||||||
public: "Видимость в списке комнат",
|
|
||||||
creator: "Создатель",
|
|
||||||
join_rules: "Правила присоединения",
|
|
||||||
guest_access: "Гостевой доступ",
|
|
||||||
history_visibility: "Видимость истории",
|
|
||||||
topic: "Тема",
|
|
||||||
avatar: "Аватар",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
forward_extremities:
|
|
||||||
"Перенаправленные заключения, это такие события, которые не имеют потомков в рамках графа, отвечающего за их репрезентацию. Чем больше пользователей находится в комнате, тем больше операций требуется выполнить Synapse для разрешения коллиций, которые возникают при их проверке наступления событий (это дорогостоящая операция). Хотя в Synapse есть код, предотвращающий одновременное присутствие слишком большого количества таких объектов в комнате, ошибки иногда могут привести к их повторному появлению. Если в комнате содержится >10 перенаправленных заключений, имеет смысл выяснить, какая комната стала причиной их появления и, возможно, удалить их, используя SQL-запросы, упомянутые issue #1760.",
|
|
||||||
},
|
|
||||||
enums: {
|
|
||||||
join_rules: {
|
|
||||||
public: "Публичный",
|
|
||||||
knock: "По запросу",
|
|
||||||
invite: "По приглашению",
|
|
||||||
private: "Приватный",
|
|
||||||
},
|
|
||||||
guest_access: {
|
|
||||||
can_join: "Гости могут присоединиться",
|
|
||||||
forbidden: "Гости не могут присоединиться",
|
|
||||||
},
|
|
||||||
history_visibility: {
|
|
||||||
invited: "С момента приглашения",
|
|
||||||
joined: "С момента присоединения",
|
|
||||||
shared: "С момента разрешения",
|
|
||||||
world_readable: "Всегда",
|
|
||||||
},
|
|
||||||
unencrypted: "Не зашифрованно",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Удалить комнату",
|
|
||||||
content:
|
|
||||||
"Вы уверены, что хотите удалить комнату? Это невозможно отменить. Все сообщения и медиафайлы, находящиеся в общем доступе в комнате, будут удалены с сервера!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reports: {
|
|
||||||
name: "Жалоб |||| Жалобы",
|
|
||||||
fields: {
|
|
||||||
id: "идентификатор",
|
|
||||||
received_ts: "время жалобы",
|
|
||||||
user_id: "коментатор",
|
|
||||||
name: "название комнаты",
|
|
||||||
score: "оценка",
|
|
||||||
reason: "причина",
|
|
||||||
event_id: "идентификатор события",
|
|
||||||
event_json: {
|
|
||||||
origin: "сервер",
|
|
||||||
origin_server_ts: "время отправки",
|
|
||||||
type: "тип события",
|
|
||||||
content: {
|
|
||||||
msgtype: "тип содержания",
|
|
||||||
body: "содержание",
|
|
||||||
format: "формат",
|
|
||||||
formatted_body: "форматированное содержание",
|
|
||||||
algorithm: "алгоритм",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Удалить жалобу",
|
|
||||||
content:
|
|
||||||
"Вы уверены, что хотите удалить жалобу? Это невозможно отменить.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
connections: {
|
|
||||||
name: "Подключения",
|
|
||||||
fields: {
|
|
||||||
last_seen: "Дата",
|
|
||||||
ip: "IP адрес",
|
|
||||||
user_agent: "User agent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devices: {
|
|
||||||
name: "Устройств |||| Устройства",
|
|
||||||
fields: {
|
|
||||||
device_id: "Идентификатор устройства",
|
|
||||||
display_name: "Название устройства",
|
|
||||||
last_seen_ts: "Метка времени",
|
|
||||||
last_seen_ip: "IP адрес",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Удаление %{id}",
|
|
||||||
content: 'Вы уверены, что хотите удалить устройство "%{name}"?',
|
|
||||||
success: "Устройство успешно удалено.",
|
|
||||||
failure: "Произошла ошибка.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users_media: {
|
|
||||||
name: "Медиа",
|
|
||||||
fields: {
|
|
||||||
media_id: "Идентификатор медиа",
|
|
||||||
media_length: "Размер файла (в байтах)",
|
|
||||||
media_type: "Тип",
|
|
||||||
upload_name: "Имя файла",
|
|
||||||
quarantined_by: "Отправлен в карантин",
|
|
||||||
safe_from_quarantine: "Защищен от карантина",
|
|
||||||
created_ts: "Создан",
|
|
||||||
last_access_ts: "Последнее обращение",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete_media: {
|
|
||||||
name: "Медиа",
|
|
||||||
fields: {
|
|
||||||
before_ts: "последнее обращение",
|
|
||||||
size_gt: "Больше чем (в байтах)",
|
|
||||||
keep_profiles: "Сохранить аватары",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
send: "Удалить медиафайлы",
|
|
||||||
send_success: "Запрос отправлен.",
|
|
||||||
send_failure: "Произошла ошибка.",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
send: "Этот метод удаляет медиафайлы с диска сервера. Это включает в себя любые миниатюры и копии загруженных файлов. Этот метод не повлияет на файлы, загруженные во внешние хранилища.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protect_media: {
|
|
||||||
action: {
|
|
||||||
create: "Не защищено, установить защиту",
|
|
||||||
delete: "Защищено, удалить защиту",
|
|
||||||
none: "В карантине",
|
|
||||||
send_success: "Успешно изменен статус защиты.",
|
|
||||||
send_failure: "Произошла ошибка.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
quarantine_media: {
|
|
||||||
action: {
|
|
||||||
name: "Карантин",
|
|
||||||
create: "Добавить в карантин",
|
|
||||||
delete: "В карантине, вывести из карантина",
|
|
||||||
none: "Защищено от карантина",
|
|
||||||
send_success: "Успешно изменен статус карантина.",
|
|
||||||
send_failure: "Произошла ошибка.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pushers: {
|
|
||||||
name: "Уведомление |||| Уведомления",
|
|
||||||
fields: {
|
|
||||||
app: "Приложение",
|
|
||||||
app_display_name: "Отображаемое имя приложения",
|
|
||||||
app_id: "Идентификатор приложения",
|
|
||||||
device_display_name: "Отображаемое имя устройства",
|
|
||||||
kind: "Тип",
|
|
||||||
lang: "Язык",
|
|
||||||
profile_tag: "Тег профиля",
|
|
||||||
pushkey: "Токен доступа",
|
|
||||||
data: { url: "URL" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
servernotices: {
|
|
||||||
name: "Уведомления",
|
|
||||||
send: "Отправить уведомление",
|
|
||||||
fields: {
|
|
||||||
body: "Сообщение",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
send: "Отправить",
|
|
||||||
send_success: "Уведомление успешно отправлено.",
|
|
||||||
send_failure: "Произошла ошибка.",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
send: 'Отправляет уведомление выбранным пользователям. Функция "Server Notices" должна быть активирована на сервере.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user_media_statistics: {
|
|
||||||
name: "Медиа",
|
|
||||||
fields: {
|
|
||||||
media_count: "Количество медиафайлов",
|
|
||||||
media_length: "Длина медиафайлов",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
forward_extremities: {
|
|
||||||
name: "Перенаправленные заключения",
|
|
||||||
fields: {
|
|
||||||
id: "Идентификатор события",
|
|
||||||
received_ts: "Время",
|
|
||||||
depth: "Глубина",
|
|
||||||
state_group: "Группа состояния",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_state: {
|
|
||||||
name: "События изменения состояния",
|
|
||||||
fields: {
|
|
||||||
type: "Тип",
|
|
||||||
content: "Содержание",
|
|
||||||
origin_server_ts: "время отправления",
|
|
||||||
sender: "Отправитель",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_directory: {
|
|
||||||
name: "Публичных комнат |||| Публичные комнаты",
|
|
||||||
fields: {
|
|
||||||
world_readable: "Видимо для гостей",
|
|
||||||
guest_can_join: "Гости могут присоединиться",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
title:
|
|
||||||
"Удалить комнату |||| Удалить %{smart_count} комнат",
|
|
||||||
content:
|
|
||||||
"Вы уверены, что хотите удалить комнату? |||| Вы уверены, что хотите удалить %{smart_count} комнат?",
|
|
||||||
erase: "Удалить комнату",
|
|
||||||
create: "Опубликовать комнату",
|
|
||||||
send_success: "Комната успешно опубликована.",
|
|
||||||
send_failure: "Произошла ошибка.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destinations: {
|
|
||||||
name: "Федераций |||| Федерация",
|
|
||||||
fields: {
|
|
||||||
destination: "Назначение",
|
|
||||||
failure_ts: "Время сбоя",
|
|
||||||
retry_last_ts: "Время последней попытки",
|
|
||||||
retry_interval: "Интервал повторения",
|
|
||||||
last_successful_stream_ordering: "Последнее успешное соединение",
|
|
||||||
stream_ordering: "Соединение",
|
|
||||||
},
|
|
||||||
action: { reconnect: "Переподключиться" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
registration_tokens: {
|
|
||||||
name: "Токены регистрации",
|
|
||||||
fields: {
|
|
||||||
token: "Токен",
|
|
||||||
valid: "Допустимый токен",
|
|
||||||
uses_allowed: "Разрешено",
|
|
||||||
pending: "Ожидается",
|
|
||||||
completed: "Использован",
|
|
||||||
expiry_time: "Время истечения срока действия",
|
|
||||||
length: "Длина",
|
|
||||||
},
|
|
||||||
helper: { length: "Длина токена, если токен не указан." },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default ru;
|
|
5
src/index.js
Normal file
5
src/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById("root"));
|
@ -1,9 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
@ -2,7 +2,7 @@ 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: async ({ base_url, username, password, loginToken }) => {
|
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;
|
||||||
|
|
||||||
@ -38,14 +38,15 @@ const authProvider = {
|
|||||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||||
|
|
||||||
const { json } = await fetchUtils.fetchJson(login_api_url, options);
|
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
|
||||||
localStorage.setItem("home_server", json.home_server);
|
localStorage.setItem("home_server", json.home_server);
|
||||||
localStorage.setItem("user_id", json.user_id);
|
localStorage.setItem("user_id", json.user_id);
|
||||||
localStorage.setItem("access_token", json.access_token);
|
localStorage.setItem("access_token", json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
localStorage.setItem("device_id", json.device_id);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// called when the user clicks on the logout button
|
// called when the user clicks on the logout button
|
||||||
logout: async () => {
|
logout: () => {
|
||||||
console.log("logout");
|
console.log("logout");
|
||||||
|
|
||||||
const logout_api_url =
|
const logout_api_url =
|
||||||
@ -61,9 +62,11 @@ const authProvider = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (typeof access_token === "string") {
|
if (typeof access_token === "string") {
|
||||||
await fetchUtils.fetchJson(logout_api_url, options);
|
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
// called when the API returns an error
|
// called when the API returns an error
|
||||||
checkError: ({ status }) => {
|
checkError: ({ status }) => {
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
import authProvider from "./authProvider";
|
|
||||||
|
|
||||||
describe("authProvider", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
fetch.resetMocks();
|
|
||||||
localStorage.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("login", () => {
|
|
||||||
it("should successfully login with username and password", async () => {
|
|
||||||
fetch.once(
|
|
||||||
JSON.stringify({
|
|
||||||
home_server: "example.com",
|
|
||||||
user_id: "@user:example.com",
|
|
||||||
access_token: "foobar",
|
|
||||||
device_id: "some_device",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const ret = await authProvider.login({
|
|
||||||
base_url: "http://example.com",
|
|
||||||
username: "@user:example.com",
|
|
||||||
password: "secret",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ret).toBe(undefined);
|
|
||||||
expect(fetch).toBeCalledWith(
|
|
||||||
"http://example.com/_matrix/client/r0/login",
|
|
||||||
{
|
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
|
|
||||||
headers: new Headers({
|
|
||||||
Accept: ["application/json"],
|
|
||||||
"Content-Type": ["application/json"],
|
|
||||||
}),
|
|
||||||
method: "POST",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
|
|
||||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
|
||||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
|
||||||
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should successfully login with token", async () => {
|
|
||||||
fetch.once(
|
|
||||||
JSON.stringify({
|
|
||||||
home_server: "example.com",
|
|
||||||
user_id: "@user:example.com",
|
|
||||||
access_token: "foobar",
|
|
||||||
device_id: "some_device",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const ret = await authProvider.login({
|
|
||||||
base_url: "https://example.com/",
|
|
||||||
loginToken: "login_token",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ret).toBe(undefined);
|
|
||||||
expect(fetch).toBeCalledWith(
|
|
||||||
"https://example.com/_matrix/client/r0/login",
|
|
||||||
{
|
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
|
||||||
headers: new Headers({
|
|
||||||
Accept: ["application/json"],
|
|
||||||
"Content-Type": ["application/json"],
|
|
||||||
}),
|
|
||||||
method: "POST",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
|
|
||||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
|
||||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
|
||||||
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("logout", () => {
|
|
||||||
it("should remove the access_token from localStorage", async () => {
|
|
||||||
localStorage.setItem("base_url", "example.com");
|
|
||||||
localStorage.setItem("access_token", "foo");
|
|
||||||
fetch.mockResponse(JSON.stringify({}));
|
|
||||||
|
|
||||||
await authProvider.logout();
|
|
||||||
|
|
||||||
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
|
|
||||||
headers: new Headers({
|
|
||||||
Accept: ["application/json"],
|
|
||||||
Authorization: ["Bearer foo"],
|
|
||||||
}),
|
|
||||||
method: "POST",
|
|
||||||
user: { authenticated: true, token: "Bearer foo" },
|
|
||||||
});
|
|
||||||
expect(localStorage.getItem("access_token")).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("checkError", () => {
|
|
||||||
it("should resolve if error.status is not 401 or 403", async () => {
|
|
||||||
await expect(
|
|
||||||
authProvider.checkError({ status: 200 })
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject if error.status is 401", async () => {
|
|
||||||
await expect(
|
|
||||||
authProvider.checkError({ status: 401 })
|
|
||||||
).rejects.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject if error.status is 403", async () => {
|
|
||||||
await expect(
|
|
||||||
authProvider.checkError({ status: 403 })
|
|
||||||
).rejects.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("checkAuth", () => {
|
|
||||||
it("should reject when not logged in", async () => {
|
|
||||||
await expect(authProvider.checkAuth({})).rejects.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve when logged in", async () => {
|
|
||||||
localStorage.setItem("access_token", "foobar");
|
|
||||||
|
|
||||||
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getPermissions", () => {
|
|
||||||
it("should do nothing", async () => {
|
|
||||||
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -98,7 +98,7 @@ const resourceMap = {
|
|||||||
}),
|
}),
|
||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
|
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
|
||||||
params.previousData.user_id
|
params.user_id
|
||||||
)}/devices/${params.id}`,
|
)}/devices/${params.id}`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -184,9 +184,9 @@ const resourceMap = {
|
|||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
|
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
|
||||||
"home_server"
|
"home_server"
|
||||||
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
|
)}/delete?before_ts=${params.before_ts}&size_gt=${
|
||||||
params.meta.size_gt
|
params.size_gt
|
||||||
}&keep_profiles=${params.meta.keep_profiles}`,
|
}&keep_profiles=${params.keep_profiles}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -197,7 +197,7 @@ const resourceMap = {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
|
endpoint: `/_synapse/admin/v1/media/unprotect/${params.media_id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -212,7 +212,7 @@ const resourceMap = {
|
|||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
|
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
|
||||||
"home_server"
|
"home_server"
|
||||||
)}/${params.id}`,
|
)}/${params.media_id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -348,7 +348,7 @@ function getSearchOrder(order) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dataProvider = {
|
const dataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const {
|
const {
|
||||||
user_id,
|
user_id,
|
||||||
@ -383,14 +383,13 @@ const dataProvider = {
|
|||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const url = `${endpoint_url}?${stringify(query)}`;
|
const url = `${endpoint_url}?${stringify(query)}`;
|
||||||
|
|
||||||
const { json } = await jsonClient(url);
|
return jsonClient(url).then(({ json }) => ({
|
||||||
return {
|
|
||||||
data: json[res.data].map(res.map),
|
data: json[res.data].map(res.map),
|
||||||
total: res.total(json, from, perPage),
|
total: res.total(json, from, perPage),
|
||||||
};
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
getOne: async (resource, params) => {
|
getOne: (resource, params) => {
|
||||||
console.log("getOne " + resource);
|
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();
|
||||||
@ -398,13 +397,14 @@ const dataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const { json } = await jsonClient(
|
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then(
|
||||||
`${endpoint_url}/${encodeURIComponent(params.id)}`
|
({ json }) => ({
|
||||||
|
data: res.map(json),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
return { data: res.map(json) };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getMany: async (resource, params) => {
|
getMany: (resource, params) => {
|
||||||
console.log("getMany " + resource);
|
console.log("getMany " + 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();
|
||||||
@ -412,18 +412,17 @@ const dataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const responses = await Promise.all(
|
return Promise.all(
|
||||||
params.ids.map(id =>
|
params.ids.map(id =>
|
||||||
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
|
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
|
||||||
)
|
)
|
||||||
);
|
).then(responses => ({
|
||||||
return {
|
|
||||||
data: responses.map(({ json }) => res.map(json)),
|
data: responses.map(({ json }) => res.map(json)),
|
||||||
total: responses.length,
|
total: responses.length,
|
||||||
};
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
getManyReference: async (resource, params) => {
|
getManyReference: (resource, params) => {
|
||||||
console.log("getManyReference " + resource);
|
console.log("getManyReference " + resource);
|
||||||
const { page, perPage } = params.pagination;
|
const { page, perPage } = params.pagination;
|
||||||
const { field, order } = params.sort;
|
const { field, order } = params.sort;
|
||||||
@ -443,14 +442,13 @@ const dataProvider = {
|
|||||||
const ref = res["reference"](params.id);
|
const ref = res["reference"](params.id);
|
||||||
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
||||||
|
|
||||||
const { json } = await jsonClient(endpoint_url);
|
return jsonClient(endpoint_url).then(({ headers, json }) => ({
|
||||||
return {
|
|
||||||
data: json[res.data].map(res.map),
|
data: json[res.data].map(res.map),
|
||||||
total: res.total(json, from, perPage),
|
total: res.total(json, from, perPage),
|
||||||
};
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (resource, params) => {
|
update: (resource, params) => {
|
||||||
console.log("update " + resource);
|
console.log("update " + 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();
|
||||||
@ -458,17 +456,15 @@ const dataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const { json } = await jsonClient(
|
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, {
|
||||||
`${endpoint_url}/${encodeURIComponent(params.id)}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
}
|
}).then(({ json }) => ({
|
||||||
);
|
data: res.map(json),
|
||||||
return { data: res.map(json) };
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMany: async (resource, params) => {
|
updateMany: (resource, params) => {
|
||||||
console.log("updateMany " + resource);
|
console.log("updateMany " + 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();
|
||||||
@ -476,7 +472,7 @@ const dataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const responses = await Promise.all(
|
return Promise.all(
|
||||||
params.ids.map(
|
params.ids.map(
|
||||||
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
|
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
|
||||||
{
|
{
|
||||||
@ -484,11 +480,12 @@ const dataProvider = {
|
|||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
).then(responses => ({
|
||||||
return { data: responses.map(({ json }) => json) };
|
data: responses.map(({ json }) => json),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (resource, params) => {
|
create: (resource, params) => {
|
||||||
console.log("create " + resource);
|
console.log("create " + 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();
|
||||||
@ -498,14 +495,15 @@ const dataProvider = {
|
|||||||
|
|
||||||
const create = res["create"](params.data);
|
const create = res["create"](params.data);
|
||||||
const endpoint_url = homeserver + create.endpoint;
|
const endpoint_url = homeserver + create.endpoint;
|
||||||
const { json } = await jsonClient(endpoint_url, {
|
return jsonClient(endpoint_url, {
|
||||||
method: create.method,
|
method: create.method,
|
||||||
body: JSON.stringify(create.body, filterNullValues),
|
body: JSON.stringify(create.body, filterNullValues),
|
||||||
});
|
}).then(({ json }) => ({
|
||||||
return { data: res.map(json) };
|
data: res.map(json),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
createMany: async (resource, params) => {
|
createMany: (resource, params) => {
|
||||||
console.log("createMany " + resource);
|
console.log("createMany " + 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();
|
||||||
@ -513,7 +511,7 @@ const dataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
if (!("create" in res)) return Promise.reject();
|
if (!("create" in res)) return Promise.reject();
|
||||||
|
|
||||||
const responses = await Promise.all(
|
return Promise.all(
|
||||||
params.ids.map(id => {
|
params.ids.map(id => {
|
||||||
params.data.id = id;
|
params.data.id = id;
|
||||||
const cre = res["create"](params.data);
|
const cre = res["create"](params.data);
|
||||||
@ -523,11 +521,12 @@ const dataProvider = {
|
|||||||
body: JSON.stringify(cre.body, filterNullValues),
|
body: JSON.stringify(cre.body, filterNullValues),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
).then(responses => ({
|
||||||
return { data: responses.map(({ json }) => json) };
|
data: responses.map(({ json }) => json),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (resource, params) => {
|
delete: (resource, params) => {
|
||||||
console.log("delete " + resource);
|
console.log("delete " + 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();
|
||||||
@ -537,22 +536,24 @@ const dataProvider = {
|
|||||||
if ("delete" in res) {
|
if ("delete" in res) {
|
||||||
const del = res["delete"](params);
|
const del = res["delete"](params);
|
||||||
const endpoint_url = homeserver + del.endpoint;
|
const endpoint_url = homeserver + del.endpoint;
|
||||||
const { json } = await jsonClient(endpoint_url, {
|
return jsonClient(endpoint_url, {
|
||||||
method: "method" in del ? del.method : "DELETE",
|
method: "method" in del ? del.method : "DELETE",
|
||||||
body: "body" in del ? JSON.stringify(del.body) : null,
|
body: "body" in del ? JSON.stringify(del.body) : null,
|
||||||
});
|
}).then(({ json }) => ({
|
||||||
return { data: json };
|
data: json,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const { json } = await jsonClient(`${endpoint_url}/${params.id}`, {
|
return jsonClient(`${endpoint_url}/${params.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify(params.previousData, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
});
|
}).then(({ json }) => ({
|
||||||
return { data: json };
|
data: json,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMany: async (resource, params) => {
|
deleteMany: (resource, params) => {
|
||||||
console.log("deleteMany " + resource);
|
console.log("deleteMany " + 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();
|
||||||
@ -560,7 +561,7 @@ const dataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
if ("delete" in res) {
|
if ("delete" in res) {
|
||||||
const responses = await Promise.all(
|
return Promise.all(
|
||||||
params.ids.map(id => {
|
params.ids.map(id => {
|
||||||
const del = res["delete"]({ ...params, id: id });
|
const del = res["delete"]({ ...params, id: id });
|
||||||
const endpoint_url = homeserver + del.endpoint;
|
const endpoint_url = homeserver + del.endpoint;
|
||||||
@ -569,21 +570,21 @@ const dataProvider = {
|
|||||||
body: "body" in del ? JSON.stringify(del.body) : null,
|
body: "body" in del ? JSON.stringify(del.body) : null,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
).then(responses => ({
|
||||||
return {
|
|
||||||
data: responses.map(({ json }) => json),
|
data: responses.map(({ json }) => json),
|
||||||
};
|
}));
|
||||||
} else {
|
} else {
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const responses = await Promise.all(
|
return Promise.all(
|
||||||
params.ids.map(id =>
|
params.ids.map(id =>
|
||||||
jsonClient(`${endpoint_url}/${id}`, {
|
jsonClient(`${endpoint_url}/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
).then(responses => ({
|
||||||
return { data: responses.map(({ json }) => json) };
|
data: responses.map(({ json }) => json),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import { fetchUtils } from "react-admin";
|
|
||||||
|
|
||||||
export const splitMxid = mxid => {
|
|
||||||
const re =
|
|
||||||
/^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
|
||||||
return re.exec(mxid)?.groups;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isValidBaseUrl = baseUrl =>
|
|
||||||
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the homeserver URL using the well-known lookup
|
|
||||||
* @param domain the domain part of an MXID
|
|
||||||
* @returns homeserver base URL
|
|
||||||
*/
|
|
||||||
export const getWellKnownUrl = async domain => {
|
|
||||||
const wellKnownUrl = `https://${domain}/.well-known/matrix/client`;
|
|
||||||
try {
|
|
||||||
const json = await fetchUtils.fetchJson(wellKnownUrl, { method: "GET" });
|
|
||||||
return json["m.homeserver"].base_url;
|
|
||||||
} catch {
|
|
||||||
// if there is no .well-known entry, return the domain itself
|
|
||||||
return `https://${domain}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get synapse server version
|
|
||||||
* @param base_url the base URL of the homeserver
|
|
||||||
* @returns server version
|
|
||||||
*/
|
|
||||||
export const getServerVersion = async baseUrl => {
|
|
||||||
const versionUrl = `${baseUrl}/_synapse/admin/v1/server_version`;
|
|
||||||
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
|
|
||||||
return response.json.server_version;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Get supported Matrix features */
|
|
||||||
export const getSupportedFeatures = async baseUrl => {
|
|
||||||
const versionUrl = `${baseUrl}/_matrix/client/versions`;
|
|
||||||
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
|
|
||||||
return response.json;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get supported login flows
|
|
||||||
* @param baseUrl the base URL of the homeserver
|
|
||||||
* @returns array of supported login flows
|
|
||||||
*/
|
|
||||||
export const getSupportedLoginFlows = async baseUrl => {
|
|
||||||
const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`;
|
|
||||||
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
|
|
||||||
return response.json.flows;
|
|
||||||
};
|
|
@ -1,31 +0,0 @@
|
|||||||
import { isValidBaseUrl, splitMxid } from "./synapse";
|
|
||||||
|
|
||||||
describe("splitMxid", () => {
|
|
||||||
it("splits valid MXIDs", () =>
|
|
||||||
expect(splitMxid("@name:domain.tld")).toEqual({
|
|
||||||
name: "name",
|
|
||||||
domain: "domain.tld",
|
|
||||||
}));
|
|
||||||
it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined());
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isValidBaseUrl", () => {
|
|
||||||
it("accepts a http URL", () =>
|
|
||||||
expect(isValidBaseUrl("http://foo.bar")).toBeTruthy());
|
|
||||||
it("accepts a https URL", () =>
|
|
||||||
expect(isValidBaseUrl("https://foo.bar")).toBeTruthy());
|
|
||||||
it("accepts a valid URL with port", () =>
|
|
||||||
expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy());
|
|
||||||
it("rejects undefined base URLs", () =>
|
|
||||||
expect(isValidBaseUrl(undefined)).toBeFalsy());
|
|
||||||
it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy());
|
|
||||||
it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy());
|
|
||||||
it("rejects non-string base URLs", () =>
|
|
||||||
expect(isValidBaseUrl({})).toBeFalsy());
|
|
||||||
it("rejects base URLs without protocol", () =>
|
|
||||||
expect(isValidBaseUrl("foo.bar")).toBeFalsy());
|
|
||||||
it("rejects base URLs with path", () =>
|
|
||||||
expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy());
|
|
||||||
it("rejects invalid base URLs", () =>
|
|
||||||
expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy());
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user