mirror of
https://github.com/UA-Fediland/synapse-admin.git
synced 2024-11-23 15:01:28 +00:00
Compare commits
54 commits
323ad9f9e2
...
7d4d765ab4
Author | SHA1 | Date | |
---|---|---|---|
|
7d4d765ab4 | ||
|
dfee94af96 | ||
|
ae7f6e18e5 | ||
|
d1e9f38b14 | ||
|
4054249359 | ||
|
f240318525 | ||
|
0852b54a8e | ||
|
8688ab7d0e | ||
|
abc677dc16 | ||
|
9d26a1ce3a | ||
|
3116b4e07a | ||
|
3a34c03509 | ||
|
384bc6553c | ||
|
df87432157 | ||
|
2afc7aeca4 | ||
|
ac843b3244 | ||
|
82155c23a1 | ||
|
64a89f6552 | ||
|
5b8882bd80 | ||
|
3fe0e95069 | ||
|
3cd0aa4446 | ||
|
3adc6b4663 | ||
|
76ef017244 | ||
|
00ecb29d6b | ||
|
4204eb902f | ||
|
6430aca02b | ||
|
b8a0b4bef5 | ||
|
2c769c309e | ||
|
82578c6570 | ||
|
005abfb4a2 | ||
|
155e73b9c6 | ||
|
1eb787fd9b | ||
|
691969e1a1 | ||
|
d520c6d618 | ||
|
af453eea71 | ||
|
78d1d34a84 | ||
|
a222af273f | ||
|
51def5775d | ||
|
6363e3d32e | ||
|
ba4345be1e | ||
|
b70ee7c55d | ||
|
9f03ec9b0f | ||
|
d8d393cdf6 | ||
|
58e02d6dff | ||
|
17379a7325 | ||
|
13fbc419c2 | ||
|
e757876c35 | ||
|
8b99695e60 | ||
|
4ab5ae2585 | ||
|
7579873d87 | ||
|
e2ce934f2a | ||
|
56bc4a56b9 | ||
|
4bae32b57c | ||
|
6e395e3b0f |
40 changed files with 4772 additions and 5968 deletions
4
.github/workflows/build-test.yml
vendored
4
.github/workflows/build-test.yml
vendored
|
@ -12,10 +12,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --immutable
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
8
.github/workflows/docker-release.yml
vendored
8
.github/workflows/docker-release.yml
vendored
|
@ -19,11 +19,11 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
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@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|
6
.github/workflows/edge_ghpage.yml
vendored
6
.github/workflows/edge_ghpage.yml
vendored
|
@ -11,16 +11,16 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout 🛎️
|
- name: Checkout 🛎️
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
- name: Install and Build 🔧
|
- name: Install and Build 🔧
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
yarn install --immutable
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.3
|
uses: JamesIves/github-pages-deploy-action@v4.5.0
|
||||||
with:
|
with:
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
folder: build
|
folder: build
|
||||||
|
|
4
.github/workflows/github-release.yml
vendored
4
.github/workflows/github-release.yml
vendored
|
@ -14,10 +14,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
- run: yarn install
|
- 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`
|
||||||
|
|
8
.github/workflows/test-docker-image.yml
vendored
8
.github/workflows/test-docker-image.yml
vendored
|
@ -19,11 +19,11 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
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@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
|
|
|
@ -6,7 +6,7 @@ ARG REACT_APP_SERVER
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src
|
COPY . /src
|
||||||
RUN yarn --network-timeout=300000 install
|
RUN yarn --network-timeout=300000 install --immutable
|
||||||
RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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/matrix-org/synapse) v1.52.0 for all functions to work as expected!
|
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.52.0 for all functions to work as expected!
|
||||||
|
|
||||||
You get your server version with the request `/_synapse/admin/v1/server_version`.
|
You get your server version with the request `/_synapse/admin/v1/server_version`.
|
||||||
See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html).
|
See also [Synapse version API](https://element-hq.github.io/synapse/latest/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://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints)
|
See also [Synapse administration endpoints](https://element-hq.github.io/synapse/latest/reverse_proxy.html#synapse-administration-endpoints)
|
||||||
|
|
||||||
### Use without install
|
### Use without install
|
||||||
|
|
||||||
|
|
33
package.json
33
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "0.8.5",
|
"version": "0.9.1",
|
||||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
@ -10,31 +10,28 @@
|
||||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^12.1.5",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"eslint": "^8.48.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^2.2.0",
|
"prettier": "^3.2.5"
|
||||||
"ra-test": "^3.19.12"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@mui/icons-material": "^5.15.10",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@mui/material": "^5.15.10",
|
||||||
"@mui/icons-material": "^5.14.8",
|
"@mui/styles": "^5.15.10",
|
||||||
"@mui/material": "^5.14.8",
|
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"prop-types": "^15.8.1",
|
|
||||||
"ra-language-chinese": "^2.0.10",
|
"ra-language-chinese": "^2.0.10",
|
||||||
"ra-language-french": "^4.13.3",
|
"ra-language-french": "^4.16.11",
|
||||||
"ra-language-german": "^3.13.4",
|
"ra-language-german": "^3.13.4",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"react": "^17.0.0",
|
"react": "^18.0.0",
|
||||||
"react-admin": "^3.19.12",
|
"react-admin": "^4.16.11",
|
||||||
"react-dom": "^17.0.2",
|
"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:example.org,Jane Doe,secretpassword,false,true,false
|
testuser22,Jane Doe,secretpassword,false,true,false
|
||||||
,John Doe,,false,false,false
|
,John Doe,,false,false,false
|
||||||
|
|
|
107
src/App.js
107
src/App.js
|
@ -1,107 +0,0 @@
|
||||||
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";
|
|
||||||
import italianMessages from "./i18n/it";
|
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
|
||||||
const messages = {
|
|
||||||
de: germanMessages,
|
|
||||||
en: englishMessages,
|
|
||||||
fr: frenchMessages,
|
|
||||||
it: italianMessages,
|
|
||||||
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
Normal file
72
src/App.jsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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 registrationToken from "./components/RegistrationTokens";
|
||||||
|
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";
|
||||||
|
|
||||||
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
|
const messages = {
|
||||||
|
de: germanMessages,
|
||||||
|
en: englishMessages,
|
||||||
|
fr: frenchMessages,
|
||||||
|
it: italianMessages,
|
||||||
|
zh: chineseMessages,
|
||||||
|
};
|
||||||
|
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 {...registrationToken} />
|
||||||
|
<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;
|
22
src/components/AvatarField.jsx
Normal file
22
src/components/AvatarField.jsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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;
|
18
src/components/AvatarField.test.js
Normal file
18
src/components/AvatarField.test.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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,6 +2,7 @@ import React from "react";
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
|
DeleteButton,
|
||||||
List,
|
List,
|
||||||
NumberField,
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
@ -10,9 +11,12 @@ 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 = {
|
||||||
|
@ -24,14 +28,14 @@ const date_format = {
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReportPagination = props => (
|
const ReportPagination = () => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination 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}>
|
<Show {...props} actions={<ReportShowActions />}>
|
||||||
<TabbedShowLayout>
|
<TabbedShowLayout>
|
||||||
<Tab
|
<Tab
|
||||||
label={translate("synapseadmin.reports.tabs.basic", {
|
label={translate("synapseadmin.reports.tabs.basic", {
|
||||||
|
@ -90,7 +94,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.users.fields.device_id"
|
label="resources.devices.fields.device_id"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabbedShowLayout>
|
</TabbedShowLayout>
|
||||||
|
@ -98,26 +102,47 @@ export const ReportShow = props => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReportList = ({ ...props }) => {
|
const ReportShowActions = () => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<TopToolbar>
|
||||||
{...props}
|
<DeleteButton
|
||||||
pagination={<ReportPagination />}
|
record={record}
|
||||||
sort={{ field: "received_ts", order: "DESC" }}
|
mutationMode="pessimistic"
|
||||||
bulkActionButtons={false}
|
confirmTitle="resources.reports.action.erase.title"
|
||||||
>
|
confirmContent="resources.reports.action.erase.content"
|
||||||
<Datagrid rowClick="show">
|
/>
|
||||||
<TextField source="id" sortable={false} />
|
</TopToolbar>
|
||||||
<DateField
|
|
||||||
source="received_ts"
|
|
||||||
showTime
|
|
||||||
options={date_format}
|
|
||||||
sortable={true}
|
|
||||||
/>
|
|
||||||
<TextField sortable={false} source="user_id" />
|
|
||||||
<TextField sortable={false} source="name" />
|
|
||||||
<TextField sortable={false} source="score" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReportList = props => (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
pagination={<ReportPagination />}
|
||||||
|
sort={{ field: "received_ts", order: "DESC" }}
|
||||||
|
>
|
||||||
|
<Datagrid rowClick="show" bulkActionButtons={false}>
|
||||||
|
<TextField source="id" sortable={false} />
|
||||||
|
<DateField
|
||||||
|
source="received_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={true}
|
||||||
|
/>
|
||||||
|
<TextField sortable={false} source="user_id" />
|
||||||
|
<TextField sortable={false} source="name" />
|
||||||
|
<TextField sortable={false} source="score" />
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resource = {
|
||||||
|
name: "reports",
|
||||||
|
icon: ReportIcon,
|
||||||
|
list: ReportList,
|
||||||
|
show: ReportShow,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resource;
|
|
@ -1,12 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import { useDataProvider, useNotify, Title } from "react-admin";
|
||||||
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,
|
||||||
|
@ -23,19 +17,6 @@ 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",
|
||||||
|
@ -51,7 +32,7 @@ function TranslatableOption({ value, text }) {
|
||||||
return <option value={value}>{translate(text)}</option>;
|
return <option value={value}>{translate(text)}</option>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilePicker = props => {
|
const FilePicker = () => {
|
||||||
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);
|
||||||
|
@ -210,7 +191,7 @@ const FilePicker = props => {
|
||||||
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;
|
||||||
|
@ -326,7 +307,7 @@ const FilePicker = props => {
|
||||||
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") {
|
||||||
|
@ -351,7 +332,7 @@ const FilePicker = props => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async okToSubmit => {
|
async _okToSubmit => {
|
||||||
if (LOGGING)
|
if (LOGGING)
|
||||||
console.log(
|
console.log(
|
||||||
"OK to create record " +
|
"OK to create record " +
|
|
@ -1,378 +0,0 @@
|
||||||
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\-.]+(:\d{1,5})?$/
|
|
||||||
)
|
|
||||||
)
|
|
||||||
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="it">Italiano</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;
|
|
327
src/components/LoginPage.jsx
Normal file
327
src/components/LoginPage.jsx
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
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,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
import {
|
||||||
|
getServerVersion,
|
||||||
|
getSupportedLoginFlows,
|
||||||
|
getWellKnownUrl,
|
||||||
|
isValidBaseUrl,
|
||||||
|
splitMxid,
|
||||||
|
} from "../synapse/synapse";
|
||||||
|
|
||||||
|
const FormBox = styled(Box)(({ theme }) => ({
|
||||||
|
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.grey[500],
|
||||||
|
},
|
||||||
|
[`& .hint`]: {
|
||||||
|
marginTop: "1em",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
[`& .form`]: {
|
||||||
|
padding: "0 1em 1em 1em",
|
||||||
|
},
|
||||||
|
[`& .input`]: {
|
||||||
|
marginTop: "1em",
|
||||||
|
},
|
||||||
|
[`& .actions`]: {
|
||||||
|
padding: "0 1em 1em 1em",
|
||||||
|
},
|
||||||
|
[`& .serverVersion`]: {
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
marginBottom: "1em",
|
||||||
|
marginLeft: "0.5em",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 renderInput = ({
|
||||||
|
meta: { touched, error } = {},
|
||||||
|
input: { ...inputProps },
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<TextField
|
||||||
|
error={!!(touched && error)}
|
||||||
|
helperText={touched && error}
|
||||||
|
{...inputProps}
|
||||||
|
{...props}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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(""));
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
component={renderInput}
|
||||||
|
label="ra.auth.username"
|
||||||
|
disabled={loading || !supportPassAuth}
|
||||||
|
onBlur={handleUsernameChange}
|
||||||
|
resettable
|
||||||
|
fullWidth
|
||||||
|
className="input"
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<PasswordInput
|
||||||
|
name="password"
|
||||||
|
component={renderInput}
|
||||||
|
label="ra.auth.password"
|
||||||
|
type="password"
|
||||||
|
disabled={loading || !supportPassAuth}
|
||||||
|
resettable
|
||||||
|
fullWidth
|
||||||
|
className="input"
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<TextInput
|
||||||
|
name="base_url"
|
||||||
|
component={renderInput}
|
||||||
|
label="synapseadmin.auth.base_url"
|
||||||
|
disabled={cfg_base_url || loading}
|
||||||
|
resettable
|
||||||
|
fullWidth
|
||||||
|
className="input"
|
||||||
|
validate={[required(), validateBaseUrl]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography className="serverVersion">{serverVersion}</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="input"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</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 { TestContext } from "ra-test";
|
import { AdminContext } from "react-admin";
|
||||||
import LoginPage from "./LoginPage";
|
import LoginPage from "./LoginPage";
|
||||||
|
|
||||||
describe("LoginForm", () => {
|
describe("LoginForm", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
render(
|
render(
|
||||||
<TestContext>
|
<AdminContext>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</TestContext>
|
</AdminContext>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,39 +0,0 @@
|
||||||
// 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,18 +6,19 @@ 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",
|
||||||
|
@ -53,40 +54,41 @@ const dateFormatter = v => {
|
||||||
return `${year}-${month}-${day}T${hour}:${minute}`;
|
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RegistrationTokenFilter = props => (
|
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
|
||||||
<Filter {...props}>
|
|
||||||
<BooleanInput source="valid" alwaysOn />
|
export const RegistrationTokenList = props => (
|
||||||
</Filter>
|
<List
|
||||||
|
{...props}
|
||||||
|
filters={registrationTokenFilters}
|
||||||
|
filterDefaultValues={{ valid: true }}
|
||||||
|
pagination={false}
|
||||||
|
perPage={500}
|
||||||
|
>
|
||||||
|
<Datagrid rowClick="edit">
|
||||||
|
<TextField source="token" sortable={false} />
|
||||||
|
<NumberField source="uses_allowed" sortable={false} />
|
||||||
|
<NumberField source="pending" sortable={false} />
|
||||||
|
<NumberField source="completed" sortable={false} />
|
||||||
|
<DateField
|
||||||
|
source="expiry_time"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RegistrationTokenList = props => {
|
|
||||||
return (
|
|
||||||
<List
|
|
||||||
{...props}
|
|
||||||
filters={<RegistrationTokenFilter />}
|
|
||||||
filterDefaultValues={{ valid: true }}
|
|
||||||
pagination={false}
|
|
||||||
perPage={500}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="edit">
|
|
||||||
<TextField source="token" sortable={false} />
|
|
||||||
<NumberField source="uses_allowed" sortable={false} />
|
|
||||||
<NumberField source="pending" sortable={false} />
|
|
||||||
<NumberField source="completed" sortable={false} />
|
|
||||||
<DateField
|
|
||||||
source="expiry_time"
|
|
||||||
showTime
|
|
||||||
options={date_format}
|
|
||||||
sortable={false}
|
|
||||||
/>
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RegistrationTokenCreate = props => (
|
export const RegistrationTokenCreate = props => (
|
||||||
<Create {...props}>
|
<Create {...props} redirect="list">
|
||||||
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}>
|
<SimpleForm
|
||||||
|
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"
|
||||||
|
@ -109,24 +111,32 @@ 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 />
|
<NumberInput source="pending" disabled />
|
||||||
<NumberInput source="pending" disabled />
|
<NumberInput source="completed" disabled />
|
||||||
<NumberInput source="completed" disabled />
|
<NumberInput
|
||||||
<NumberInput
|
source="uses_allowed"
|
||||||
source="uses_allowed"
|
validate={validateUsesAllowed}
|
||||||
validate={validateUsesAllowed}
|
step={1}
|
||||||
step={1}
|
/>
|
||||||
/>
|
<DateTimeInput
|
||||||
<DateTimeInput
|
source="expiry_time"
|
||||||
source="expiry_time"
|
parse={dateParser}
|
||||||
parse={dateParser}
|
format={dateFormatter}
|
||||||
format={dateFormatter}
|
/>
|
||||||
/>
|
</SimpleForm>
|
||||||
</SimpleForm>
|
</Edit>
|
||||||
</Edit>
|
);
|
||||||
);
|
|
||||||
|
const resource = {
|
||||||
|
name: "registration_tokens",
|
||||||
|
icon: RegistrationTokenIcon,
|
||||||
|
list: RegistrationTokenList,
|
||||||
|
edit: RegistrationTokenEdit,
|
||||||
|
create: RegistrationTokenCreate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default resource;
|
|
@ -1,260 +0,0 @@
|
||||||
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
|
|
||||||
);
|
|
206
src/components/RoomDirectory.jsx
Normal file
206
src/components/RoomDirectory.jsx
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
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, { Fragment, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
|
@ -7,12 +7,14 @@ import {
|
||||||
Toolbar,
|
Toolbar,
|
||||||
required,
|
required,
|
||||||
useCreate,
|
useCreate,
|
||||||
useMutation,
|
useDataProvider,
|
||||||
|
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 {
|
||||||
|
@ -22,7 +24,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
|
const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
const ServerNoticeToolbar = props => (
|
const ServerNoticeToolbar = props => (
|
||||||
|
@ -46,12 +48,7 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{translate("resources.servernotices.helper.send")}
|
{translate("resources.servernotices.helper.send")}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<SimpleForm
|
<SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
|
||||||
toolbar={<ServerNoticeToolbar />}
|
|
||||||
submitOnEnter={false}
|
|
||||||
redirect={false}
|
|
||||||
save={onSend}
|
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
source="body"
|
source="body"
|
||||||
label="resources.servernotices.fields.body"
|
label="resources.servernotices.fields.body"
|
||||||
|
@ -67,24 +64,25 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServerNoticeButton = props => {
|
export const ServerNoticeButton = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [create, { loading }] = useCreate("servernotices");
|
const [create, { isloading }] = useCreate();
|
||||||
|
|
||||||
const handleDialogOpen = () => setOpen(true);
|
const handleDialogOpen = () => setOpen(true);
|
||||||
const handleDialogClose = () => setOpen(false);
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
const handleSend = values => {
|
const handleSend = values => {
|
||||||
create(
|
create(
|
||||||
{ payload: { data: { id: record.id, ...values } } },
|
"servernotices",
|
||||||
|
{ data: { id: record.id, ...values } },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.servernotices.action.send_success");
|
notify("resources.servernotices.action.send_success");
|
||||||
handleDialogClose();
|
handleDialogClose();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onError: () =>
|
||||||
notify("resources.servernotices.action.send_failure", {
|
notify("resources.servernotices.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
|
@ -93,67 +91,65 @@ export const ServerNoticeButton = props => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label="resources.servernotices.send"
|
label="resources.servernotices.send"
|
||||||
onClick={handleDialogOpen}
|
onClick={handleDialogOpen}
|
||||||
disabled={loading}
|
disabled={isloading}
|
||||||
>
|
>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog
|
<ServerNoticeDialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleDialogClose}
|
onClose={handleDialogClose}
|
||||||
onSend={handleSend}
|
onSubmit={handleSend}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServerNoticeBulkButton = ({ selectedIds }) => {
|
export const ServerNoticeBulkButton = () => {
|
||||||
|
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 unselectAll = useUnselectAll();
|
const unselectAllUsers = useUnselectAll("users");
|
||||||
const [createMany, { loading }] = useMutation();
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
const handleDialogOpen = () => setOpen(true);
|
const { mutate: sendNotices, isLoading } = useMutation(
|
||||||
const handleDialogClose = () => setOpen(false);
|
data =>
|
||||||
|
dataProvider.createMany("servernotices", {
|
||||||
const handleSend = values => {
|
ids: selectedIds,
|
||||||
createMany(
|
data: data,
|
||||||
{
|
}),
|
||||||
type: "createMany",
|
{
|
||||||
resource: "servernotices",
|
onSuccess: () => {
|
||||||
payload: { ids: selectedIds, data: values },
|
notify("resources.servernotices.action.send_success");
|
||||||
|
unselectAllUsers();
|
||||||
|
closeDialog();
|
||||||
},
|
},
|
||||||
{
|
onError: () =>
|
||||||
onSuccess: ({ data }) => {
|
notify("resources.servernotices.action.send_failure", {
|
||||||
notify("resources.servernotices.action.send_success");
|
type: "error",
|
||||||
unselectAll("users");
|
}),
|
||||||
handleDialogClose();
|
}
|
||||||
},
|
);
|
||||||
onFailure: error =>
|
|
||||||
notify("resources.servernotices.action.send_failure", {
|
|
||||||
type: "error",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label="resources.servernotices.send"
|
label="resources.servernotices.send"
|
||||||
onClick={handleDialogOpen}
|
onClick={openDialog}
|
||||||
disabled={loading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog
|
<ServerNoticeDialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleDialogClose}
|
onClose={closeDialog}
|
||||||
onSend={handleSend}
|
onSubmit={sendNotices}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -3,7 +3,6 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
Filter,
|
|
||||||
List,
|
List,
|
||||||
Pagination,
|
Pagination,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
|
@ -21,11 +20,12 @@ import {
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||||
|
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
||||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
|
||||||
const DestinationPagination = props => (
|
const DestinationPagination = () => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const date_format = {
|
const date_format = {
|
||||||
|
@ -37,23 +37,17 @@ const date_format = {
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const destinationRowStyle = (record, index) => ({
|
const destinationRowSx = (record, _index) => ({
|
||||||
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
||||||
});
|
});
|
||||||
|
|
||||||
const DestinationFilter = ({ ...props }) => {
|
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
||||||
return (
|
|
||||||
<Filter {...props}>
|
|
||||||
<SearchInput source="destination" alwaysOn />
|
|
||||||
</Filter>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DestinationReconnectButton = props => {
|
export const DestinationReconnectButton = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [handleReconnect, { isLoading }] = useDelete("destinations");
|
const [handleReconnect, { isLoading }] = useDelete();
|
||||||
|
|
||||||
// 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;
|
||||||
|
@ -63,7 +57,8 @@ export const DestinationReconnectButton = props => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
handleReconnect(
|
handleReconnect(
|
||||||
{ payload: { id: record.id } },
|
"destinations",
|
||||||
|
{ id: record.id },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("ra.notification.updated", {
|
notify("ra.notification.updated", {
|
||||||
|
@ -71,7 +66,7 @@ export const DestinationReconnectButton = props => {
|
||||||
});
|
});
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () => {
|
onError: () => {
|
||||||
notify("ra.message.error", { type: "error" });
|
notify("ra.message.error", { type: "error" });
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -89,13 +84,13 @@ export const DestinationReconnectButton = props => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DestinationShowActions = props => (
|
const DestinationShowActions = () => (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
<DestinationReconnectButton />
|
<DestinationReconnectButton />
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
const DestinationTitle = props => {
|
const DestinationTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
|
@ -109,14 +104,14 @@ export const DestinationList = props => {
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={<DestinationFilter />}
|
filters={destinationFilters}
|
||||||
pagination={<DestinationPagination />}
|
pagination={<DestinationPagination />}
|
||||||
sort={{ field: "destination", order: "ASC" }}
|
sort={{ field: "destination", order: "ASC" }}
|
||||||
bulkActionButtons={false}
|
|
||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
rowStyle={destinationRowStyle}
|
rowSx={destinationRowSx}
|
||||||
rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`}
|
rowClick={(id, _resource, _record) => `${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} />
|
||||||
|
@ -160,7 +155,7 @@ export const DestinationShow = props => {
|
||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
rowClick={(id, basePath, record) => `/rooms/${id}/show`}
|
rowClick={(id, resource, record) => `/rooms/${id}/show`}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
source="room_id"
|
source="room_id"
|
||||||
|
@ -183,3 +178,12 @@ export const DestinationShow = props => {
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resource = {
|
||||||
|
name: "destinations",
|
||||||
|
icon: DestinationsIcon,
|
||||||
|
list: DestinationList,
|
||||||
|
show: DestinationShow,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resource;
|
|
@ -1,84 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
51
src/components/devices.jsx
Normal file
51
src/components/devices.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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,7 +1,4 @@
|
||||||
import React, { Fragment, useState } from "react";
|
import React, { 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,
|
||||||
|
@ -30,24 +27,9 @@ 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 useStyles = makeStyles(
|
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||||
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 => {
|
||||||
|
@ -56,19 +38,17 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
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"
|
icon={<DeleteSweepIcon />}
|
||||||
icon={<DeleteSweepIcon />}
|
/>
|
||||||
/>
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
<Button label="ra.action.cancel" onClick={onClose}>
|
<IconCancel />
|
||||||
<IconCancel />
|
</Button>
|
||||||
</Button>
|
</Toolbar>
|
||||||
</Toolbar>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} loading={loading}>
|
<Dialog open={open} onClose={onClose} loading={loading}>
|
||||||
|
@ -79,12 +59,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{translate("resources.delete_media.helper.send")}
|
{translate("resources.delete_media.helper.send")}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<SimpleForm
|
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
toolbar={<DeleteMediaToolbar />}
|
|
||||||
submitOnEnter={false}
|
|
||||||
redirect={false}
|
|
||||||
save={onSend}
|
|
||||||
>
|
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
fullWidth
|
fullWidth
|
||||||
source="before_ts"
|
source="before_ts"
|
||||||
|
@ -113,23 +88,25 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteMediaButton = props => {
|
export const DeleteMediaButton = props => {
|
||||||
const classes = useStyles(props);
|
const theme = useTheme();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [deleteOne, { loading }] = useDelete("delete_media");
|
const [deleteOne, { isLoading }] = useDelete();
|
||||||
|
|
||||||
const handleDialogOpen = () => setOpen(true);
|
const openDialog = () => setOpen(true);
|
||||||
const handleDialogClose = () => setOpen(false);
|
const closeDialog = () => setOpen(false);
|
||||||
|
|
||||||
const handleSend = values => {
|
const deleteMedia = values => {
|
||||||
deleteOne(
|
deleteOne(
|
||||||
{ payload: { ...values } },
|
"delete_media",
|
||||||
|
// 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");
|
||||||
handleDialogClose();
|
closeDialog();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onError: () =>
|
||||||
notify("resources.delete_media.action.send_failure", {
|
notify("resources.delete_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
|
@ -138,43 +115,54 @@ export const DeleteMediaButton = props => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
{...props}
|
||||||
label="resources.delete_media.action.send"
|
label="resources.delete_media.action.send"
|
||||||
onClick={handleDialogOpen}
|
onClick={openDialog}
|
||||||
disabled={loading}
|
disabled={isLoading}
|
||||||
className={classnames("ra-delete-button", classes.deleteButton)}
|
sx={{
|
||||||
|
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={handleDialogClose}
|
onClose={closeDialog}
|
||||||
onSend={handleSend}
|
onSubmit={deleteMedia}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProtectMediaButton = props => {
|
export const ProtectMediaButton = () => {
|
||||||
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, { loading }] = useCreate("protect_media");
|
const [create, { isLoading }] = useCreate();
|
||||||
const [deleteOne] = useDelete("protect_media");
|
const [deleteOne] = useDelete();
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
const handleProtect = () => {
|
const handleProtect = () => {
|
||||||
create(
|
create(
|
||||||
{ payload: { data: record } },
|
"protect_media",
|
||||||
|
{ data: record },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.protect_media.action.send_success");
|
notify("resources.protect_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onError: () =>
|
||||||
notify("resources.protect_media.action.send_failure", {
|
notify("resources.protect_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
|
@ -184,13 +172,14 @@ export const ProtectMediaButton = props => {
|
||||||
|
|
||||||
const handleUnprotect = () => {
|
const handleUnprotect = () => {
|
||||||
deleteOne(
|
deleteOne(
|
||||||
{ payload: { ...record } },
|
"protect_media",
|
||||||
|
{ id: record.id },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.protect_media.action.send_success");
|
notify("resources.protect_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onError: () =>
|
||||||
notify("resources.protect_media.action.send_failure", {
|
notify("resources.protect_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
|
@ -203,7 +192,7 @@ export const ProtectMediaButton = props => {
|
||||||
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", {
|
||||||
|
@ -229,7 +218,7 @@ export const ProtectMediaButton = props => {
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleUnprotect} disabled={loading}>
|
<Button onClick={handleUnprotect} disabled={isLoading}>
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -242,13 +231,13 @@ export const ProtectMediaButton = props => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleProtect} disabled={loading}>
|
<Button onClick={handleProtect} disabled={isLoading}>
|
||||||
<LockOpenIcon />
|
<LockOpenIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -257,20 +246,21 @@ export const QuarantineMediaButton = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [create, { loading }] = useCreate("quarantine_media");
|
const [create, { isLoading }] = useCreate();
|
||||||
const [deleteOne] = useDelete("quarantine_media");
|
const [deleteOne] = useDelete();
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
const handleQuarantaine = () => {
|
const handleQuarantaine = () => {
|
||||||
create(
|
create(
|
||||||
{ payload: { data: record } },
|
"quarantine_media",
|
||||||
|
{ data: record },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.quarantine_media.action.send_success");
|
notify("resources.quarantine_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onError: () =>
|
||||||
notify("resources.quarantine_media.action.send_failure", {
|
notify("resources.quarantine_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
|
@ -280,13 +270,14 @@ export const QuarantineMediaButton = props => {
|
||||||
|
|
||||||
const handleRemoveQuarantaine = () => {
|
const handleRemoveQuarantaine = () => {
|
||||||
deleteOne(
|
deleteOne(
|
||||||
{ payload: { ...record } },
|
"quarantine_media",
|
||||||
|
{ id: record.id, previousData: record },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notify("resources.quarantine_media.action.send_success");
|
notify("resources.quarantine_media.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onFailure: () =>
|
onError: () =>
|
||||||
notify("resources.quarantine_media.action.send_failure", {
|
notify("resources.quarantine_media.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
|
@ -295,7 +286,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", {
|
||||||
|
@ -303,7 +294,7 @@ export const QuarantineMediaButton = props => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={true}>
|
<Button {...props} disabled={true}>
|
||||||
<ClearIcon />
|
<ClearIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -316,7 +307,11 @@ export const QuarantineMediaButton = props => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleRemoveQuarantaine} disabled={loading}>
|
<Button
|
||||||
|
{...props}
|
||||||
|
onClick={handleRemoveQuarantaine}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
<BlockIcon color="error" />
|
<BlockIcon color="error" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -329,12 +324,12 @@ export const QuarantineMediaButton = props => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleQuarantaine} disabled={loading}>
|
<Button onClick={handleQuarantaine} disabled={isLoading}>
|
||||||
<BlockIcon />
|
<BlockIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,18 +1,20 @@
|
||||||
import React, { Fragment } from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
DateField,
|
DateField,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
|
DatagridConfigurable,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
Filter,
|
ExportButton,
|
||||||
|
FunctionField,
|
||||||
List,
|
List,
|
||||||
NumberField,
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
ReferenceManyField,
|
ReferenceManyField,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
|
SelectColumnsButton,
|
||||||
SelectField,
|
SelectField,
|
||||||
Show,
|
Show,
|
||||||
Tab,
|
Tab,
|
||||||
|
@ -22,10 +24,8 @@ import {
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import get from "lodash/get";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import PropTypes from "prop-types";
|
import Box from "@mui/material/Box";
|
||||||
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,11 +34,12 @@ 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 {
|
||||||
RoomDirectoryBulkDeleteButton,
|
RoomDirectoryBulkUnpublishButton,
|
||||||
RoomDirectoryBulkSaveButton,
|
RoomDirectoryBulkPublishButton,
|
||||||
RoomDirectoryDeleteButton,
|
RoomDirectoryUnpublishButton,
|
||||||
RoomDirectorySaveButton,
|
RoomDirectoryPublishButton,
|
||||||
} from "./RoomDirectory";
|
} from "./RoomDirectory";
|
||||||
|
|
||||||
const date_format = {
|
const date_format = {
|
||||||
|
@ -50,44 +51,11 @@ const date_format = {
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const RoomPagination = () => (
|
||||||
helper_forward_extremities: {
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
margin: "0.5em",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const RoomPagination = props => (
|
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const EncryptionField = ({ source, record = {}, emptyText }) => {
|
const RoomTitle = () => {
|
||||||
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 = "";
|
||||||
|
@ -102,24 +70,18 @@ const RoomTitle = props => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomShowActions = ({ basePath, data, resource }) => {
|
const RoomShowActions = () => {
|
||||||
|
const record = useRecordContext();
|
||||||
var roomDirectoryStatus = "";
|
var roomDirectoryStatus = "";
|
||||||
if (data) {
|
if (record) {
|
||||||
roomDirectoryStatus = data.public;
|
roomDirectoryStatus = record.public;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{roomDirectoryStatus === false && (
|
{roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
|
||||||
<RoomDirectorySaveButton record={data} />
|
{roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
|
||||||
)}
|
|
||||||
{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"
|
||||||
|
@ -129,7 +91,6 @@ const RoomShowActions = ({ basePath, data, resource }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomShow = props => {
|
export const RoomShow = props => {
|
||||||
const classes = useStyles({ props });
|
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||||
|
@ -137,6 +98,7 @@ 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" />
|
||||||
|
@ -171,7 +133,8 @@ export const RoomShow = props => {
|
||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
rowClick={(id, basePath, record) => "/users/" + id}
|
rowClick={(id, resource, record) => "/users/" + id}
|
||||||
|
bulkActionButtons={false}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
source="id"
|
source="id"
|
||||||
|
@ -256,7 +219,7 @@ export const RoomShow = props => {
|
||||||
target="room_id"
|
target="room_id"
|
||||||
addLabel={false}
|
addLabel={false}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="type" sortable={false} />
|
<TextField source="type" sortable={false} />
|
||||||
<DateField
|
<DateField
|
||||||
source="origin_server_ts"
|
source="origin_server_ts"
|
||||||
|
@ -281,15 +244,20 @@ export const RoomShow = props => {
|
||||||
icon={<FastForwardIcon />}
|
icon={<FastForwardIcon />}
|
||||||
path="forward_extremities"
|
path="forward_extremities"
|
||||||
>
|
>
|
||||||
<div className={classes.helper_forward_extremities}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
margin: "0.5em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{translate("resources.rooms.helper.forward_extremities")}
|
{translate("resources.rooms.helper.forward_extremities")}
|
||||||
</div>
|
</Box>
|
||||||
<ReferenceManyField
|
<ReferenceManyField
|
||||||
reference="forward_extremities"
|
reference="forward_extremities"
|
||||||
target="room_id"
|
target="room_id"
|
||||||
addLabel={false}
|
addLabel={false}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="id" sortable={false} />
|
<TextField source="id" sortable={false} />
|
||||||
<DateField
|
<DateField
|
||||||
source="received_ts"
|
source="received_ts"
|
||||||
|
@ -307,104 +275,81 @@ export const RoomShow = props => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomBulkActionButtons = props => (
|
const RoomBulkActionButtons = () => (
|
||||||
<Fragment>
|
<>
|
||||||
<RoomDirectoryBulkSaveButton {...props} />
|
<RoomDirectoryBulkPublishButton />
|
||||||
<RoomDirectoryBulkDeleteButton {...props} />
|
<RoomDirectoryBulkUnpublishButton />
|
||||||
<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 RoomFilter = ({ ...props }) => {
|
const roomFilters = [<SearchInput source="search_term" alwaysOn />];
|
||||||
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 RoomNameField = props => {
|
const RoomListActions = () => (
|
||||||
const { source } = props;
|
<TopToolbar>
|
||||||
const record = useRecordContext();
|
<SelectColumnsButton />
|
||||||
return (
|
<ExportButton />
|
||||||
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
RoomNameField.propTypes = {
|
export const RoomList = props => {
|
||||||
label: PropTypes.string,
|
const theme = useTheme();
|
||||||
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={<RoomFilter />}
|
filters={roomFilters}
|
||||||
bulkActionButtons={<RoomBulkActionButtons />}
|
actions={<RoomListActions />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick="show">
|
<DatagridConfigurable
|
||||||
<EncryptionField
|
rowClick="show"
|
||||||
|
bulkActionButtons={<RoomBulkActionButtons />}
|
||||||
|
omit={[
|
||||||
|
"joined_local_members",
|
||||||
|
"state_events",
|
||||||
|
"version",
|
||||||
|
"federatable",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<BooleanField
|
||||||
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" />
|
||||||
{localMembersFilter && <TextField source="joined_local_members" />}
|
<TextField source="joined_local_members" />
|
||||||
{stateEventsFilter && <TextField source="state_events" />}
|
<TextField source="state_events" />
|
||||||
{versionFilter && <TextField source="version" />}
|
<TextField source="version" />
|
||||||
{federateableFilter && <BooleanField source="federatable" />}
|
<BooleanField source="federatable" />
|
||||||
<BooleanField source="public" />
|
<BooleanField source="public" />
|
||||||
</Datagrid>
|
</DatagridConfigurable>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
const resource = {
|
||||||
return {
|
name: "rooms",
|
||||||
roomFilters: state.admin.resources.rooms.list.params.displayedFilters,
|
icon: RoomIcon,
|
||||||
};
|
list: RoomList,
|
||||||
}
|
show: RoomShow,
|
||||||
|
};
|
||||||
|
|
||||||
export const RoomList = connect(mapStateToProps)(FilterableRoomList);
|
export default resource;
|
|
@ -1,81 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
79
src/components/statistics.jsx
Normal file
79
src/components/statistics.jsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
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,5 +1,4 @@
|
||||||
import React, { cloneElement, Fragment } from "react";
|
import React, { cloneElement } 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";
|
||||||
|
@ -8,6 +7,7 @@ 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,7 +18,6 @@ import {
|
||||||
Create,
|
Create,
|
||||||
Edit,
|
Edit,
|
||||||
List,
|
List,
|
||||||
Filter,
|
|
||||||
Toolbar,
|
Toolbar,
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
SimpleFormIterator,
|
SimpleFormIterator,
|
||||||
|
@ -49,28 +48,10 @@ 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" },
|
||||||
|
@ -92,7 +73,7 @@ const date_format = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserListActions = ({
|
const UserListActions = ({
|
||||||
currentSort,
|
sort,
|
||||||
className,
|
className,
|
||||||
resource,
|
resource,
|
||||||
filters,
|
filters,
|
||||||
|
@ -101,7 +82,6 @@ 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,
|
||||||
|
@ -119,18 +99,18 @@ const UserListActions = ({
|
||||||
filterValues,
|
filterValues,
|
||||||
context: "button",
|
context: "button",
|
||||||
})}
|
})}
|
||||||
<CreateButton basePath={basePath} />
|
<CreateButton />
|
||||||
<ExportButton
|
<ExportButton
|
||||||
disabled={total === 0}
|
disabled={total === 0}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
sort={currentSort}
|
sort={sort}
|
||||||
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={redirect} label="CSV Import">
|
<Button component={Link} to="/import_users" label="CSV Import">
|
||||||
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
|
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
|
||||||
</Button>
|
</Button>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
|
@ -141,72 +121,61 @@ UserListActions.defaultProps = {
|
||||||
onUnselectItems: () => null,
|
onUnselectItems: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserPagination = props => (
|
const UserPagination = () => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const UserFilter = props => (
|
const userFilters = [
|
||||||
<Filter {...props}>
|
<SearchInput source="name" alwaysOn />,
|
||||||
<SearchInput source="name" alwaysOn />
|
<BooleanInput source="guests" 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 = props => (
|
const UserBulkActionButtons = () => (
|
||||||
<Fragment>
|
<>
|
||||||
<ServerNoticeBulkButton {...props} />
|
<ServerNoticeBulkButton />
|
||||||
<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>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AvatarField = ({ source, className, record = {} }) => (
|
export const UserList = props => (
|
||||||
<Avatar src={record[source]} className={className} />
|
<List
|
||||||
|
{...props}
|
||||||
|
filters={userFilters}
|
||||||
|
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||||
|
sort={{ field: "name", order: "ASC" }}
|
||||||
|
actions={<UserListActions maxResults={10000} />}
|
||||||
|
pagination={<UserPagination />}
|
||||||
|
>
|
||||||
|
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
||||||
|
<AvatarField
|
||||||
|
source="avatar_src"
|
||||||
|
sx={{ height: "40px", width: "40px" }}
|
||||||
|
sortBy="avatar_url"
|
||||||
|
/>
|
||||||
|
<TextField source="id" sortBy="name" />
|
||||||
|
<TextField source="displayname" />
|
||||||
|
<BooleanField source="is_guest" />
|
||||||
|
<BooleanField source="admin" />
|
||||||
|
<BooleanField source="deactivated" />
|
||||||
|
<DateField
|
||||||
|
source="creation_ts"
|
||||||
|
label="resources.users.fields.creation_ts_ms"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
/>
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UserList = props => {
|
|
||||||
const classes = useStyles();
|
|
||||||
return (
|
|
||||||
<List
|
|
||||||
{...props}
|
|
||||||
filters={<UserFilter />}
|
|
||||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
|
||||||
sort={{ field: "name", order: "ASC" }}
|
|
||||||
actions={<UserListActions maxResults={10000} />}
|
|
||||||
bulkActionButtons={<UserBulkActionButtons />}
|
|
||||||
pagination={<UserPagination />}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="edit">
|
|
||||||
<AvatarField
|
|
||||||
source="avatar_src"
|
|
||||||
className={classes.small}
|
|
||||||
sortBy="avatar_url"
|
|
||||||
/>
|
|
||||||
<TextField source="id" sortBy="name" />
|
|
||||||
<TextField source="displayname" />
|
|
||||||
<BooleanField source="is_guest" />
|
|
||||||
<BooleanField source="admin" />
|
|
||||||
<BooleanField source="deactivated" />
|
|
||||||
<DateField
|
|
||||||
source="creation_ts"
|
|
||||||
label="resources.users.fields.creation_ts_ms"
|
|
||||||
showTime
|
|
||||||
options={date_format}
|
|
||||||
/>
|
|
||||||
</Datagrid>
|
|
||||||
</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
|
||||||
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
|
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
|
||||||
|
@ -262,7 +231,7 @@ export function generateRandomUser() {
|
||||||
|
|
||||||
const UserEditToolbar = props => (
|
const UserEditToolbar = props => (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<SaveButton submitOnEnter={true} disabled={props.pristine} />
|
<SaveButton disabled={props.pristine} />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -302,7 +271,6 @@ 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" />
|
||||||
|
@ -330,7 +298,7 @@ export const UserCreate = props => (
|
||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|
||||||
const UserTitle = props => {
|
const UserTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
|
@ -344,7 +312,6 @@ const UserTitle = props => {
|
||||||
};
|
};
|
||||||
|
|
||||||
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 />}>
|
||||||
|
@ -356,7 +323,7 @@ export const UserEdit = props => {
|
||||||
<AvatarField
|
<AvatarField
|
||||||
source="avatar_src"
|
source="avatar_src"
|
||||||
sortable={false}
|
sortable={false}
|
||||||
className={classes.large}
|
sx={{ height: "120px", width: "120px", float: "right" }}
|
||||||
/>
|
/>
|
||||||
<TextInput source="id" disabled />
|
<TextInput source="id" disabled />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
|
@ -369,7 +336,6 @@ 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" />
|
||||||
|
@ -451,7 +417,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%" }}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="ip" sortable={false} />
|
<TextField source="ip" sortable={false} />
|
||||||
<DateField
|
<DateField
|
||||||
source="last_seen"
|
source="last_seen"
|
||||||
|
@ -513,7 +479,8 @@ export const UserEdit = props => {
|
||||||
>
|
>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}
|
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
|
||||||
|
bulkActionButtons={false}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
source="id"
|
source="id"
|
||||||
|
@ -543,7 +510,7 @@ export const UserEdit = props => {
|
||||||
target="user_id"
|
target="user_id"
|
||||||
addLabel={false}
|
addLabel={false}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<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} />
|
||||||
|
@ -559,3 +526,13 @@ export const UserEdit = props => {
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resource = {
|
||||||
|
name: "users",
|
||||||
|
icon: UserIcon,
|
||||||
|
list: UserList,
|
||||||
|
edit: UserEdit,
|
||||||
|
create: UserCreate,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resource;
|
|
@ -188,7 +188,7 @@ const de = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: {
|
reports: {
|
||||||
name: "Ereignisbericht |||| Ereignisberichte",
|
name: "Gemeldetes Ereignis |||| Gemeldete Ereignisse",
|
||||||
fields: {
|
fields: {
|
||||||
id: "ID",
|
id: "ID",
|
||||||
received_ts: "Meldezeit",
|
received_ts: "Meldezeit",
|
||||||
|
@ -210,6 +210,13 @@ 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",
|
||||||
|
|
|
@ -207,6 +207,13 @@ 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",
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById("root"));
|
|
9
src/index.jsx
Normal file
9
src/index.jsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
|
@ -98,7 +98,7 @@ const resourceMap = {
|
||||||
}),
|
}),
|
||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
|
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
|
||||||
params.user_id
|
params.previousData.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.before_ts}&size_gt=${
|
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
|
||||||
params.size_gt
|
params.meta.size_gt
|
||||||
}&keep_profiles=${params.keep_profiles}`,
|
}&keep_profiles=${params.meta.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.media_id}`,
|
endpoint: `/_synapse/admin/v1/media/unprotect/${params.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.media_id}`,
|
)}/${params.id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -456,7 +456,7 @@ const dataProvider = {
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, {
|
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
}).then(({ json }) => ({
|
}).then(({ json }) => ({
|
||||||
|
@ -546,7 +546,7 @@ const dataProvider = {
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
return jsonClient(`${endpoint_url}/${params.id}`, {
|
return jsonClient(`${endpoint_url}/${params.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.previousData, filterNullValues),
|
||||||
}).then(({ json }) => ({
|
}).then(({ json }) => ({
|
||||||
data: json,
|
data: json,
|
||||||
}));
|
}));
|
||||||
|
|
48
src/synapse/synapse.js
Normal file
48
src/synapse/synapse.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
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 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;
|
||||||
|
};
|
31
src/synapse/synapse.test.js
Normal file
31
src/synapse/synapse.test.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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 a new issue