Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36f9ce6b07 | |||
| 83c9704633 | |||
| 8c1546cd5a | |||
| 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 |
@@ -1,6 +1,7 @@
|
|||||||
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
|
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
|
||||||
tests/
|
tests/
|
||||||
build/
|
build/
|
||||||
|
dist/
|
||||||
lib/
|
lib/
|
||||||
node_modules/
|
node_modules/
|
||||||
electron_app/
|
electron_app/
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
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
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="shortcut icon" href="./favicon.ico" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Synapse-Admin"
|
||||||
|
/>
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="./manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>Synapse-Admin</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Spinner from https://projects.lukehaas.me/css-loaders/ */
|
||||||
|
|
||||||
|
.loader,
|
||||||
|
.loader:before,
|
||||||
|
.loader:after {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
color: #283593;
|
||||||
|
font-size: 11px;
|
||||||
|
text-indent: -99999em;
|
||||||
|
margin: 55px auto;
|
||||||
|
position: relative;
|
||||||
|
width: 10em;
|
||||||
|
height: 10em;
|
||||||
|
box-shadow: inset 0 0 0 1em;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
-ms-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader:before,
|
||||||
|
.loader:after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader:before {
|
||||||
|
width: 5.2em;
|
||||||
|
height: 10.2em;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 10.2em 0 0 10.2em;
|
||||||
|
top: -0.1em;
|
||||||
|
left: -0.1em;
|
||||||
|
-webkit-transform-origin: 5.2em 5.1em;
|
||||||
|
transform-origin: 5.2em 5.1em;
|
||||||
|
-webkit-animation: load2 2s infinite ease 1.5s;
|
||||||
|
animation: load2 2s infinite ease 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader:after {
|
||||||
|
width: 5.2em;
|
||||||
|
height: 10.2em;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 0 10.2em 10.2em 0;
|
||||||
|
top: -0.1em;
|
||||||
|
left: 5.1em;
|
||||||
|
-webkit-transform-origin: 0px 5.1em;
|
||||||
|
transform-origin: 0px 5.1em;
|
||||||
|
-webkit-animation: load2 2s infinite ease;
|
||||||
|
animation: load2 2s infinite ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes load2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes load2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root">
|
||||||
|
<div class="loader-container">
|
||||||
|
<div class="loader">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer
|
||||||
|
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||||
|
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
|
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
||||||
|
Synapse-Admin <b>(%REACT_APP_VERSION%)</b> by Awesome Technologies Innovationslabor GmbH
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
|
</html>
|
||||||
+18
-19
@@ -10,34 +10,34 @@
|
|||||||
"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.55.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint": "^8.56.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",
|
||||||
|
"vite": "^4.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mui/icons-material": "^5.14.19",
|
"@mui/icons-material": "^5.15.7",
|
||||||
"@mui/material": "^5.14.8",
|
"@mui/material": "^5.15.7",
|
||||||
"@mui/styles": "5.14.10",
|
"@mui/styles": "^5.15.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.16.2",
|
"ra-language-french": "^4.16.9",
|
||||||
"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": "^4.16.9",
|
"react-admin": "^4.16.9",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.0.0"
|
||||||
"react-scripts": "^5.0.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
"start": "REACT_APP_VERSION=$(git describe --tags) vite serve",
|
||||||
"build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build",
|
"build": "REACT_APP_VERSION=$(git describe --tags) vite build",
|
||||||
"fix:other": "yarn prettier --write",
|
"fix:other": "yarn prettier --write",
|
||||||
"fix:code": "yarn test:lint --fix",
|
"fix:code": "yarn test:lint --fix",
|
||||||
"fix": "yarn fix:code && yarn fix:other",
|
"fix": "yarn fix:code && yarn fix:other",
|
||||||
@@ -45,8 +45,7 @@
|
|||||||
"test:code": "react-scripts test",
|
"test:code": "react-scripts test",
|
||||||
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
|
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
|
||||||
"test:style": "yarn prettier --list-different",
|
"test:style": "yarn prettier --list-different",
|
||||||
"test": "yarn test:style && yarn test:lint && yarn test:code",
|
"test": "yarn test:style && yarn test:lint && yarn test:code"
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
|
|||||||
+1
-1
@@ -46,4 +46,4 @@
|
|||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
-112
@@ -1,112 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Admin,
|
|
||||||
CustomRoutes,
|
|
||||||
Resource,
|
|
||||||
resolveBrowserLocale,
|
|
||||||
} from "react-admin";
|
|
||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
|
||||||
import authProvider from "./synapse/authProvider";
|
|
||||||
import dataProvider from "./synapse/dataProvider";
|
|
||||||
import { 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 path="/import_users" element={<ImportFeature />} />
|
|
||||||
</CustomRoutes>
|
|
||||||
<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;
|
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
const fixed_base_url = undefined; // FIXME: process.env.REACT_APP_SERVER;
|
||||||
|
|
||||||
|
// 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(fixed_base_url)}
|
||||||
|
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;
|
||||||
@@ -6,7 +6,17 @@ import { useRecordContext } from "react-admin";
|
|||||||
const AvatarField = ({ source, ...rest }) => {
|
const AvatarField = ({ source, ...rest }) => {
|
||||||
const record = useRecordContext(rest);
|
const record = useRecordContext(rest);
|
||||||
const src = get(record, source)?.toString();
|
const src = get(record, source)?.toString();
|
||||||
return <Avatar src={src} {...rest} />;
|
const { alt, classes, sizes, sx, variant } = rest;
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
alt={alt}
|
||||||
|
classes={classes}
|
||||||
|
sizes={sizes}
|
||||||
|
src={src}
|
||||||
|
sx={sx}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AvatarField;
|
export default AvatarField;
|
||||||
@@ -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;
|
||||||
@@ -32,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);
|
||||||
@@ -191,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;
|
||||||
@@ -307,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") {
|
||||||
@@ -332,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,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
fetchUtils,
|
|
||||||
Form,
|
Form,
|
||||||
FormDataConsumer,
|
FormDataConsumer,
|
||||||
Notification,
|
Notification,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
useLogin,
|
useLogin,
|
||||||
useNotify,
|
useNotify,
|
||||||
useLocaleState,
|
useLocaleState,
|
||||||
|
useStoreContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -27,6 +27,13 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
import {
|
||||||
|
getServerVersion,
|
||||||
|
getSupportedLoginFlows,
|
||||||
|
getWellKnownUrl,
|
||||||
|
isValidBaseUrl,
|
||||||
|
splitMxid,
|
||||||
|
} from "../synapse/synapse";
|
||||||
|
|
||||||
const FormBox = styled(Box)(({ theme }) => ({
|
const FormBox = styled(Box)(({ theme }) => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -75,15 +82,15 @@ const FormBox = styled(Box)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = ({ cfg_base_url }) => {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
const store = useStoreContext();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||||
const [locale, setLocale] = useLocaleState();
|
const [locale, setLocale] = useLocaleState();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = store.getItem("base_url");
|
||||||
const cfg_base_url = process.env.REACT_APP_SERVER;
|
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||||
|
|
||||||
@@ -96,8 +103,8 @@ const LoginPage = () => {
|
|||||||
"",
|
"",
|
||||||
window.location.href.replace(loginToken[0], "#").split("#")[0]
|
window.location.href.replace(loginToken[0], "#").split("#")[0]
|
||||||
);
|
);
|
||||||
const baseUrl = localStorage.getItem("sso_base_url");
|
const baseUrl = store.getItem("sso_base_url");
|
||||||
localStorage.removeItem("sso_base_url");
|
store.removeItem("sso_base_url");
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
const auth = {
|
const auth = {
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
@@ -113,8 +120,8 @@ const LoginPage = () => {
|
|||||||
typeof error === "string"
|
typeof error === "string"
|
||||||
? error
|
? error
|
||||||
: typeof error === "undefined" || !error.message
|
: typeof error === "undefined" || !error.message
|
||||||
? "ra.auth.sign_in_error"
|
? "ra.auth.sign_in_error"
|
||||||
: error.message
|
: error.message
|
||||||
);
|
);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
@@ -155,102 +162,57 @@ const LoginPage = () => {
|
|||||||
typeof error === "string"
|
typeof error === "string"
|
||||||
? error
|
? error
|
||||||
: typeof error === "undefined" || !error.message
|
: typeof error === "undefined" || !error.message
|
||||||
? "ra.auth.sign_in_error"
|
? "ra.auth.sign_in_error"
|
||||||
: error.message,
|
: error.message,
|
||||||
{ type: "warning" }
|
{ type: "warning" }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSSO = () => {
|
const handleSSO = () => {
|
||||||
localStorage.setItem("sso_base_url", ssoBaseUrl);
|
store.setItem("sso_base_url", ssoBaseUrl);
|
||||||
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
|
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
|
||||||
window.location.href
|
window.location.href
|
||||||
)}`;
|
)}`;
|
||||||
window.location.href = ssoFullUrl;
|
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 UserData = ({ formData }) => {
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
const [serverVersion, setServerVersion] = useState("");
|
const [serverVersion, setServerVersion] = useState("");
|
||||||
|
|
||||||
const handleUsernameChange = _ => {
|
const handleUsernameChange = _ => {
|
||||||
if (formData.base_url || cfg_base_url) return;
|
if (formData.base_url || cfg_base_url) return;
|
||||||
// check if username is a full qualified userId then set base_url accordially
|
// check if username is a full qualified userId then set base_url accordingly
|
||||||
const home_server = extractHomeServer(formData.username);
|
const domain = splitMxid(formData.username)?.domain;
|
||||||
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
|
if (domain) {
|
||||||
if (home_server) {
|
getWellKnownUrl(domain).then(url => form.setValue("base_url", url));
|
||||||
// fetch .well-known entry to get base_url
|
|
||||||
fetchUtils
|
|
||||||
.fetchJson(wellKnownUrl, { method: "GET" })
|
|
||||||
.then(({ json }) => {
|
|
||||||
form.setValue("base_url", json["m.homeserver"].base_url);
|
|
||||||
})
|
|
||||||
.catch(_ => {
|
|
||||||
// if there is no .well-known entry, try the home server name
|
|
||||||
form.setValue("base_url", `https://${home_server}`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
_ => {
|
if (!isValidBaseUrl(formData.base_url)) return;
|
||||||
if (
|
|
||||||
!formData.base_url ||
|
getServerVersion(formData.base_url)
|
||||||
!formData.base_url.match(
|
.then(serverVersion =>
|
||||||
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/
|
setServerVersion(
|
||||||
|
`${translate("synapseadmin.auth.server_version")} ${serverVersion}`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
.catch(() => setServerVersion(""));
|
||||||
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
|
// Set SSO Url
|
||||||
const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`;
|
getSupportedLoginFlows(formData.base_url)
|
||||||
let supportPass = false,
|
.then(loginFlows => {
|
||||||
supportSSO = false;
|
const supportPass =
|
||||||
fetchUtils
|
loginFlows.find(f => f.type === "m.login.password") !== undefined;
|
||||||
.fetchJson(authMethodUrl, { method: "GET" })
|
const supportSSO =
|
||||||
.then(({ json }) => {
|
loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
||||||
json.flows.forEach(f => {
|
setSupportPassAuth(supportPass);
|
||||||
if (f.type === "m.login.password") {
|
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
||||||
supportPass = true;
|
})
|
||||||
} else if (f.type === "m.login.sso") {
|
.catch(() => setSSOBaseUrl(""));
|
||||||
supportSSO = true;
|
}, [formData.base_url]);
|
||||||
}
|
|
||||||
});
|
|
||||||
setSupportPassAuth(supportPass);
|
|
||||||
if (supportSSO) {
|
|
||||||
setSSOBaseUrl(formData.base_url);
|
|
||||||
} else {
|
|
||||||
setSSOBaseUrl("");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(_ => {
|
|
||||||
setSSOBaseUrl("");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[formData.base_url]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -286,7 +248,7 @@ const LoginPage = () => {
|
|||||||
name="base_url"
|
name="base_url"
|
||||||
component={renderInput}
|
component={renderInput}
|
||||||
label="synapseadmin.auth.base_url"
|
label="synapseadmin.auth.base_url"
|
||||||
disabled={cfg_base_url || loading}
|
disabled={cfg_base_url != null || loading}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
className="input"
|
className="input"
|
||||||
@@ -307,9 +269,13 @@ const LoginPage = () => {
|
|||||||
<FormBox>
|
<FormBox>
|
||||||
<Card className="card">
|
<Card className="card">
|
||||||
<Box className="avatar">
|
<Box className="avatar">
|
||||||
<Avatar className="icon">
|
{loading ? (
|
||||||
<LockIcon />
|
<CircularProgress size={25} thickness={2} />
|
||||||
</Avatar>
|
) : (
|
||||||
|
<Avatar className="icon">
|
||||||
|
<LockIcon />
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
|
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
|
||||||
<Box className="form">
|
<Box className="form">
|
||||||
@@ -339,7 +305,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
{loading && <CircularProgress size={25} thickness={2} />}
|
|
||||||
{translate("ra.auth.sign_in")}
|
{translate("ra.auth.sign_in")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -349,7 +314,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading || ssoBaseUrl === ""}
|
disabled={loading || ssoBaseUrl === ""}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
{loading && <CircularProgress size={25} thickness={2} />}
|
|
||||||
{translate("synapseadmin.auth.sso_sign_in")}
|
{translate("synapseadmin.auth.sso_sign_in")}
|
||||||
</Button>
|
</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
@@ -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: "users",
|
||||||
|
icon: RegistrationTokenIcon,
|
||||||
|
list: RegistrationTokenList,
|
||||||
|
edit: RegistrationTokenEdit,
|
||||||
|
create: RegistrationTokenCreate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default resource;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { Fragment } from "react";
|
import React from "react";
|
||||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
useCreate,
|
useCreate,
|
||||||
|
useDataProvider,
|
||||||
useListContext,
|
useListContext,
|
||||||
useNotify,
|
useNotify,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
@@ -22,13 +22,14 @@ import {
|
|||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
|
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
|
||||||
import AvatarField from "./AvatarField";
|
import AvatarField from "./AvatarField";
|
||||||
|
|
||||||
const RoomDirectoryPagination = props => (
|
const RoomDirectoryPagination = () => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} />
|
<Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RoomDirectoryDeleteButton = props => {
|
export const RoomDirectoryUnpublishButton = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,12 +45,12 @@ export const RoomDirectoryDeleteButton = props => {
|
|||||||
smart_count: 1,
|
smart_count: 1,
|
||||||
})}
|
})}
|
||||||
resource="room_directory"
|
resource="room_directory"
|
||||||
icon={<FolderSharedIcon />}
|
icon={<RoomDirectoryIcon />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomDirectoryBulkDeleteButton = props => (
|
export const RoomDirectoryBulkUnpublishButton = props => (
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
{...props}
|
{...props}
|
||||||
label="resources.room_directory.action.erase"
|
label="resources.room_directory.action.erase"
|
||||||
@@ -57,61 +58,63 @@ export const RoomDirectoryBulkDeleteButton = props => (
|
|||||||
confirmTitle="resources.room_directory.action.title"
|
confirmTitle="resources.room_directory.action.title"
|
||||||
confirmContent="resources.room_directory.action.content"
|
confirmContent="resources.room_directory.action.content"
|
||||||
resource="room_directory"
|
resource="room_directory"
|
||||||
icon={<FolderSharedIcon />}
|
icon={<RoomDirectoryIcon />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RoomDirectoryBulkSaveButton = () => {
|
export const RoomDirectoryBulkPublishButton = props => {
|
||||||
const { selectedIds } = useListContext();
|
const { selectedIds } = useListContext();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const unselectAll = useUnselectAll();
|
const unselectAllRooms = useUnselectAll("rooms");
|
||||||
const { createMany, isloading } = useMutation();
|
const dataProvider = useDataProvider();
|
||||||
|
const { mutate, isLoading } = useMutation(
|
||||||
const handleSend = values => {
|
() =>
|
||||||
createMany(
|
dataProvider.createMany("room_directory", {
|
||||||
["room_directory", "createMany", { ids: selectedIds, data: {} }],
|
ids: selectedIds,
|
||||||
{
|
data: {},
|
||||||
onSuccess: data => {
|
}),
|
||||||
notify("resources.room_directory.action.send_success");
|
{
|
||||||
unselectAll("rooms");
|
onSuccess: () => {
|
||||||
refresh();
|
notify("resources.room_directory.action.send_success");
|
||||||
},
|
unselectAllRooms();
|
||||||
onError: error =>
|
refresh();
|
||||||
notify("resources.room_directory.action.send_failure", {
|
},
|
||||||
type: "error",
|
onError: () =>
|
||||||
}),
|
notify("resources.room_directory.action.send_failure", {
|
||||||
}
|
type: "error",
|
||||||
);
|
}),
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
{...props}
|
||||||
label="resources.room_directory.action.create"
|
label="resources.room_directory.action.create"
|
||||||
onClick={handleSend}
|
onClick={mutate}
|
||||||
disabled={isloading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<FolderSharedIcon />
|
<RoomDirectoryIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomDirectorySaveButton = () => {
|
export const RoomDirectoryPublishButton = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const [create, { isloading }] = useCreate();
|
const [create, { isLoading }] = useCreate();
|
||||||
|
|
||||||
const handleSend = values => {
|
const handleSend = () => {
|
||||||
create(
|
create(
|
||||||
"room_directory",
|
"room_directory",
|
||||||
{ data: { id: record.id } },
|
{ data: { id: record.id } },
|
||||||
{
|
{
|
||||||
onSuccess: data => {
|
onSuccess: () => {
|
||||||
notify("resources.room_directory.action.send_success");
|
notify("resources.room_directory.action.send_success");
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: error =>
|
onError: () =>
|
||||||
notify("resources.room_directory.action.send_failure", {
|
notify("resources.room_directory.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
@@ -121,21 +124,16 @@ export const RoomDirectorySaveButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
{...props}
|
||||||
label="resources.room_directory.action.create"
|
label="resources.room_directory.action.create"
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={isloading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<FolderSharedIcon />
|
<RoomDirectoryIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomDirectoryBulkActionButtons = () => (
|
|
||||||
<Fragment>
|
|
||||||
<RoomDirectoryBulkDeleteButton />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
const RoomDirectoryListActions = () => (
|
const RoomDirectoryListActions = () => (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
<SelectColumnsButton />
|
<SelectColumnsButton />
|
||||||
@@ -150,8 +148,8 @@ export const RoomDirectoryList = () => (
|
|||||||
actions={<RoomDirectoryListActions />}
|
actions={<RoomDirectoryListActions />}
|
||||||
>
|
>
|
||||||
<DatagridConfigurable
|
<DatagridConfigurable
|
||||||
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
|
rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
|
||||||
bulkActionButtons={<RoomDirectoryBulkActionButtons />}
|
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
||||||
omit={["room_id", "canonical_alias", "topic"]}
|
omit={["room_id", "canonical_alias", "topic"]}
|
||||||
>
|
>
|
||||||
<AvatarField
|
<AvatarField
|
||||||
@@ -198,3 +196,11 @@ export const RoomDirectoryList = () => (
|
|||||||
</DatagridConfigurable>
|
</DatagridConfigurable>
|
||||||
</List>
|
</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,6 +7,7 @@ import {
|
|||||||
Toolbar,
|
Toolbar,
|
||||||
required,
|
required,
|
||||||
useCreate,
|
useCreate,
|
||||||
|
useDataProvider,
|
||||||
useListContext,
|
useListContext,
|
||||||
useNotify,
|
useNotify,
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
@@ -23,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 => (
|
||||||
@@ -47,11 +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 />}
|
|
||||||
redirect={false}
|
|
||||||
save={onSend}
|
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
source="body"
|
source="body"
|
||||||
label="resources.servernotices.fields.body"
|
label="resources.servernotices.fields.body"
|
||||||
@@ -71,14 +68,15 @@ 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, { isloading }] = 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");
|
||||||
@@ -93,7 +91,7 @@ export const ServerNoticeButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label="resources.servernotices.send"
|
label="resources.servernotices.send"
|
||||||
onClick={handleDialogOpen}
|
onClick={handleDialogOpen}
|
||||||
@@ -104,53 +102,54 @@ export const ServerNoticeButton = () => {
|
|||||||
<ServerNoticeDialog
|
<ServerNoticeDialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleDialogClose}
|
onClose={handleDialogClose}
|
||||||
onSend={handleSend}
|
onSubmit={handleSend}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServerNoticeBulkButton = () => {
|
export const ServerNoticeBulkButton = () => {
|
||||||
const { selectedIds } = useListContext();
|
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, isloading } = 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,
|
||||||
["servernotices", "createMany", { ids: selectedIds, data: values }],
|
}),
|
||||||
{
|
{
|
||||||
onSuccess: data => {
|
onSuccess: () => {
|
||||||
notify("resources.servernotices.action.send_success");
|
notify("resources.servernotices.action.send_success");
|
||||||
unselectAll("users");
|
unselectAllUsers();
|
||||||
handleDialogClose();
|
closeDialog();
|
||||||
},
|
},
|
||||||
onError: error =>
|
onError: () =>
|
||||||
notify("resources.servernotices.action.send_failure", {
|
notify("resources.servernotices.action.send_failure", {
|
||||||
type: "error",
|
type: "error",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label="resources.servernotices.send"
|
label="resources.servernotices.send"
|
||||||
onClick={handleDialogOpen}
|
onClick={openDialog}
|
||||||
disabled={isloading}
|
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 = {
|
||||||
@@ -41,19 +41,13 @@ 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", {
|
||||||
@@ -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,7 +104,7 @@ 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" }}
|
||||||
>
|
>
|
||||||
@@ -183,3 +178,12 @@ export const DestinationShow = props => {
|
|||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resource = {
|
||||||
|
name: "destinations",
|
||||||
|
icon: DestinationsIcon,
|
||||||
|
list: DestinationList,
|
||||||
|
show: DestinationShow,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resource;
|
||||||
@@ -1,75 +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 { alpha, useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
export const DeviceRemoveButton = props => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const record = useRecordContext();
|
|
||||||
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}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
|
||||||
// Reset on mouse devices
|
|
||||||
"@media (hover: none)": {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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,4 +1,4 @@
|
|||||||
import React, { Fragment, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
Button,
|
Button,
|
||||||
@@ -29,7 +29,7 @@ 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";
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
|
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
const dateParser = v => {
|
const dateParser = v => {
|
||||||
@@ -38,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}>
|
||||||
@@ -61,11 +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 />}
|
|
||||||
redirect={false}
|
|
||||||
save={onSend}
|
|
||||||
>
|
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
fullWidth
|
fullWidth
|
||||||
source="before_ts"
|
source="before_ts"
|
||||||
@@ -97,18 +91,20 @@ export const DeleteMediaButton = props => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [deleteOne, { isLoading }] = 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();
|
||||||
},
|
},
|
||||||
onError: () =>
|
onError: () =>
|
||||||
notify("resources.delete_media.action.send_failure", {
|
notify("resources.delete_media.action.send_failure", {
|
||||||
@@ -119,10 +115,11 @@ 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={isLoading}
|
disabled={isLoading}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
@@ -139,26 +136,27 @@ export const DeleteMediaButton = props => {
|
|||||||
</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");
|
||||||
@@ -174,7 +172,8 @@ 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");
|
||||||
@@ -193,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", {
|
||||||
@@ -219,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>
|
||||||
@@ -232,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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -247,14 +246,15 @@ 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");
|
||||||
@@ -270,7 +270,8 @@ 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");
|
||||||
@@ -285,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", {
|
||||||
@@ -293,7 +294,7 @@ export const QuarantineMediaButton = props => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={true}>
|
<Button {...props} disabled={true}>
|
||||||
<ClearIcon />
|
<ClearIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,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>
|
||||||
@@ -319,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,4 +1,4 @@
|
|||||||
import React, { Fragment } from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
DatagridConfigurable,
|
DatagridConfigurable,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
ExportButton,
|
ExportButton,
|
||||||
Filter,
|
|
||||||
FunctionField,
|
FunctionField,
|
||||||
List,
|
List,
|
||||||
NumberField,
|
NumberField,
|
||||||
@@ -35,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 = {
|
||||||
@@ -51,11 +51,11 @@ const date_format = {
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomPagination = props => (
|
const RoomPagination = () => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const RoomTitle = props => {
|
const RoomTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
var name = "";
|
var name = "";
|
||||||
@@ -70,23 +70,18 @@ const RoomTitle = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomShowActions = ({ 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
|
||||||
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"
|
||||||
@@ -103,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" />
|
||||||
@@ -279,22 +275,18 @@ export const RoomShow = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RoomBulkActionButtons = () => (
|
const RoomBulkActionButtons = () => (
|
||||||
<Fragment>
|
<>
|
||||||
<RoomDirectoryBulkSaveButton />
|
<RoomDirectoryBulkPublishButton />
|
||||||
<RoomDirectoryBulkDeleteButton />
|
<RoomDirectoryBulkUnpublishButton />
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
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 />];
|
||||||
<Filter {...props}>
|
|
||||||
<SearchInput source="search_term" alwaysOn />
|
|
||||||
</Filter>
|
|
||||||
);
|
|
||||||
|
|
||||||
const RoomListActions = () => (
|
const RoomListActions = () => (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
@@ -303,14 +295,15 @@ const RoomListActions = () => (
|
|||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RoomList = () => {
|
export const RoomList = props => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
|
{...props}
|
||||||
pagination={<RoomPagination />}
|
pagination={<RoomPagination />}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
filters={<RoomFilter />}
|
filters={roomFilters}
|
||||||
actions={<RoomListActions />}
|
actions={<RoomListActions />}
|
||||||
>
|
>
|
||||||
<DatagridConfigurable
|
<DatagridConfigurable
|
||||||
@@ -350,3 +343,12 @@ export const RoomList = () => {
|
|||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resource = {
|
||||||
|
name: "rooms",
|
||||||
|
icon: RoomIcon,
|
||||||
|
list: RoomList,
|
||||||
|
show: RoomShow,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resource;
|
||||||
@@ -1,83 +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" }}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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,4 +1,4 @@
|
|||||||
import React, { cloneElement, Fragment } from "react";
|
import React, { cloneElement } from "react";
|
||||||
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||||
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
||||||
import DevicesIcon from "@mui/icons-material/Devices";
|
import DevicesIcon from "@mui/icons-material/Devices";
|
||||||
@@ -7,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,
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
Create,
|
Create,
|
||||||
Edit,
|
Edit,
|
||||||
List,
|
List,
|
||||||
Filter,
|
|
||||||
Toolbar,
|
Toolbar,
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
SimpleFormIterator,
|
SimpleFormIterator,
|
||||||
@@ -73,7 +73,7 @@ const date_format = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UserListActions = ({
|
const UserListActions = ({
|
||||||
currentSort,
|
sort,
|
||||||
className,
|
className,
|
||||||
resource,
|
resource,
|
||||||
filters,
|
filters,
|
||||||
@@ -103,7 +103,7 @@ const UserListActions = ({
|
|||||||
<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}
|
||||||
@@ -121,65 +121,60 @@ 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>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UserList = props => {
|
export const UserList = props => (
|
||||||
return (
|
<List
|
||||||
<List
|
{...props}
|
||||||
{...props}
|
filters={userFilters}
|
||||||
filters={<UserFilter />}
|
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
actions={<UserListActions maxResults={10000} />}
|
||||||
actions={<UserListActions maxResults={10000} />}
|
pagination={<UserPagination />}
|
||||||
pagination={<UserPagination />}
|
>
|
||||||
>
|
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
||||||
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
<AvatarField
|
||||||
<AvatarField
|
source="avatar_src"
|
||||||
source="avatar_src"
|
sx={{ height: "40px", width: "40px" }}
|
||||||
sx={{ height: "40px", width: "40px" }}
|
sortBy="avatar_url"
|
||||||
sortBy="avatar_url"
|
/>
|
||||||
/>
|
<TextField source="id" sortBy="name" />
|
||||||
<TextField source="id" sortBy="name" />
|
<TextField source="displayname" />
|
||||||
<TextField source="displayname" />
|
<BooleanField source="is_guest" />
|
||||||
<BooleanField source="is_guest" />
|
<BooleanField source="admin" />
|
||||||
<BooleanField source="admin" />
|
<BooleanField source="deactivated" />
|
||||||
<BooleanField source="deactivated" />
|
<DateField
|
||||||
<DateField
|
source="creation_ts"
|
||||||
source="creation_ts"
|
label="resources.users.fields.creation_ts_ms"
|
||||||
label="resources.users.fields.creation_ts_ms"
|
showTime
|
||||||
showTime
|
options={date_format}
|
||||||
options={date_format}
|
/>
|
||||||
/>
|
</Datagrid>
|
||||||
</Datagrid>
|
</List>
|
||||||
</List>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/appendices#user-identifiers
|
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||||||
// here only local part of user_id
|
// here only local part of user_id
|
||||||
@@ -303,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 (
|
||||||
@@ -530,3 +525,13 @@ export const UserEdit = props => {
|
|||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resource = {
|
||||||
|
name: "users",
|
||||||
|
icon: UserIcon,
|
||||||
|
list: UserList,
|
||||||
|
edit: UserEdit,
|
||||||
|
create: UserCreate,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resource;
|
||||||
+8
-1
@@ -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"));
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
const REACT_APP_SERVER = import.meta.env.VITE_APP_SERVER;
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { fetchUtils } from "react-admin";
|
import { fetchUtils } from "react-admin";
|
||||||
|
|
||||||
const authProvider = {
|
const authProvider = (fixed_base_url, store) => ({
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
login: ({ base_url, username, password, loginToken }) => {
|
login: ({ base_url, username, password, loginToken }) => {
|
||||||
// force homeserver for protection in case the form is manipulated
|
// force homeserver for protection in case the form is manipulated
|
||||||
base_url = process.env.REACT_APP_SERVER || base_url;
|
base_url = fixed_base_url || base_url;
|
||||||
|
|
||||||
console.log("login ");
|
console.log("login ");
|
||||||
const options = {
|
const options = {
|
||||||
@@ -12,7 +12,7 @@ const authProvider = {
|
|||||||
body: JSON.stringify(
|
body: JSON.stringify(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
device_id: localStorage.getItem("device_id"),
|
device_id: store.getItem("device_id"),
|
||||||
initial_device_display_name: "Synapse Admin",
|
initial_device_display_name: "Synapse Admin",
|
||||||
},
|
},
|
||||||
loginToken
|
loginToken
|
||||||
@@ -33,16 +33,16 @@ const authProvider = {
|
|||||||
// server, since the admin might want to access the admin API via some
|
// server, since the admin might want to access the admin API via some
|
||||||
// private address
|
// private address
|
||||||
base_url = base_url.replace(/\/+$/g, "");
|
base_url = base_url.replace(/\/+$/g, "");
|
||||||
localStorage.setItem("base_url", base_url);
|
store.setItem("base_url", base_url);
|
||||||
|
|
||||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||||
|
|
||||||
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
|
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
|
||||||
localStorage.setItem("home_server", json.home_server);
|
store.setItem("home_server", json.home_server);
|
||||||
localStorage.setItem("user_id", json.user_id);
|
store.setItem("user_id", json.user_id);
|
||||||
localStorage.setItem("access_token", json.access_token);
|
store.setItem("access_token", json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
store.setItem("device_id", json.device_id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// called when the user clicks on the logout button
|
// called when the user clicks on the logout button
|
||||||
@@ -86,6 +86,6 @@ const authProvider = {
|
|||||||
},
|
},
|
||||||
// called when the user navigates to a new location, to check for permissions / roles
|
// called when the user navigates to a new location, to check for permissions / roles
|
||||||
getPermissions: () => Promise.resolve(),
|
getPermissions: () => Promise.resolve(),
|
||||||
};
|
});
|
||||||
|
|
||||||
export default authProvider;
|
export default authProvider;
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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());
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user