Compare commits

..

35 Commits

Author SHA1 Message Date
Manuel Stahl 36f9ce6b07 Use vite.js instead of react-scripts
- react-scripts are not maintained anymore
- vite.js is well suited for single page applications

See https://darekkay.com/blog/create-react-app-to-vite/

Change-Id: Ib884748e373094a640b576894ff67b98c3584ec8
2024-02-07 17:03:28 +01:00
Manuel Stahl 83c9704633 Use store from react-admin instead of localStorage
By default this is the same, but it can be changed.

Change-Id: Id1b11872b5f7bc83c18f8d74b03ba6401c277da0
2024-02-07 17:03:28 +01:00
Manuel Stahl 8c1546cd5a WIP: Extract process.env
Change-Id: I9efb1079c0c88e6e0272c5fda734a367aa8f84a3
2024-02-07 17:03:28 +01:00
Manuel Stahl 8688ab7d0e Fix update in dataProvider
Fixes #461

Change-Id: Icc4b0264cfda04a8a28595d153c43cdf75524673
2024-02-07 16:48:28 +01:00
Manuel Stahl abc677dc16 Simplify DeviceRemoveButton
Change-Id: I23dcb327d2612db7fc132889d623b709dce34f06
2024-02-07 16:40:42 +01:00
Steffo 9d26a1ce3a Allow deletion of event reports (#462)
* feat: Allow event reports to get deleted
* chore: Change german translation of reports name to be more fitting
2024-02-07 16:34:50 +01:00
Timo Gurr 3116b4e07a Show topic in room basic view 2024-02-07 16:23:54 +01:00
dependabot[bot] 3a34c03509 Bump @mui/styles from 5.15.7 to 5.15.8 (#463)
Bumps [@mui/styles](https://github.com/mui/material-ui/tree/HEAD/packages/mui-styles) from 5.15.7 to 5.15.8.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.8/packages/mui-styles)

---
updated-dependencies:
- dependency-name: "@mui/styles"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 16:18:50 +01:00
dependabot[bot] 384bc6553c Bump JamesIves/github-pages-deploy-action from 4.4.3 to 4.5.0 (#457)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.4.3 to 4.5.0.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.4.3...v4.5.0)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 16:18:15 +01:00
dependabot[bot] df87432157 Bump follow-redirects from 1.14.8 to 1.15.5 (#450)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.5.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.5)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 16:17:49 +01:00
Manuel Stahl 2afc7aeca4 Rename all JSX files to have proper file extension
Change-Id: I4ab382f7673a815164f74154e6b03b370fd76a33
2024-02-07 15:27:34 +01:00
Manuel Stahl ac843b3244 Upgrade packages to latest version
yarn upgrade --latest

Change-Id: I07c71927ffa6c811fe7cbf8bd2a47503e55499ce
2024-02-07 15:27:34 +01:00
Manuel Stahl 82155c23a1 Upgrade react to v18
https://react.dev/blog/2022/03/08/react-18-upgrade-guide#updates-to-client-rendering-apis

Change-Id: Ibac40eb3d900f54955dfbc8f5e2833a0c47941a6
2024-02-07 15:27:33 +01:00
Manuel Stahl 64a89f6552 Extract helper functions from LoginPage
Change-Id: I507e223d0eff00bac3963d0b71f9bd648b9ab7b1
2024-02-07 15:15:40 +01:00
Manuel Stahl 5b8882bd80 Fix AvatarField
Change-Id: I9614163942fcb8667885b524caf944500605c55d
2024-02-07 15:15:36 +01:00
Manuel Stahl 3fe0e95069 Set "requireAuth" for all pages
Change-Id: I1b68d46f9f7d9a843a5b26f0906d1f71569487cf
2024-02-07 12:03:00 +01:00
Dirk Klimpel 3cd0aa4446 Update links to Synapse in README.md (#458) 2024-02-07 11:14:25 +01:00
dklimpel 3adc6b4663 Use new API of dataProvider
Change-Id: I2789f1f1384b48e876bee5af421ff5db66fa3416
2024-02-07 08:49:26 +01:00
Manuel Stahl 76ef017244 Refactor media
Change-Id: Ic24c53048c35b76532af24d9c5c9bf831688344b
2024-02-07 08:49:11 +01:00
dklimpel 00ecb29d6b Refactor RoomDirectory
Change-Id: Ie3bd606fc91b2673d2a3422f8fd465258d3211b0
2024-02-07 08:28:12 +01:00
Manuel Stahl 4204eb902f Refactor ServerNotices
https://marmelab.com/react-admin/Upgrade.html#usequery-usemutation-and-usequerywithstore-have-been-removed

Change-Id: Id12f727d8813f78c3ae300035aeb1333a1272e02
2024-02-07 08:28:12 +01:00
dklimpel 6430aca02b Rename save to onSubmit in SimpleForm
https: //marmelab.com/react-admin/Upgrade.html#the-form-components-save-prop-has-been-renamed-to-onsubmit

Change-Id: Iaf2c0b665c8058336d4df6326531780a2790e71d
2024-02-07 08:28:12 +01:00
dklimpel b8a0b4bef5 Move redirect from SimpleForm to Create
Change-Id: I7c5c0043a49bcb16c131e400b2ebe022e233c5ae
2024-02-06 15:11:20 +01:00
dklimpel 2c769c309e Move Toolbar's alwaysEnableSaveButton into SaveButton
https: //marmelab.com/react-admin/Upgrade.html#toolbars-alwaysenablesavebutton-prop-has-been-removed

Change-Id: I6c8693d4f55bfabdeaa677bd294d8663b7f14d69
2024-02-06 15:11:20 +01:00
dklimpel 82578c6570 Change unselectAll syntax
https: //marmelab.com/react-admin/Upgrade.html#useunselectall-syntax-changed

Change-Id: Ie8d261e863fe4726b3a5925ed0446eb824c6e517
2024-02-06 15:11:20 +01:00
dklimpel 005abfb4a2 Update pagination
https: //marmelab.com/react-admin/Upgrade.html#no-more-props-injection-in-custom-pagination-and-empty-components

Change-Id: I6f4d3941dee22cf00da30bada5442f3fdd345127
2024-02-06 15:11:20 +01:00
dklimpel 155e73b9c6 Rename currentSort to sort
https: //marmelab.com/react-admin/Upgrade.html#currentsort-renamed-to-sort

Change-Id: I676adefe0073a9a0343dcd598e9559ecf30c38af
2024-02-06 15:11:20 +01:00
dklimpel 1eb787fd9b Replace "onFailure" with "onError"
https://marmelab.com/react-admin/Upgrade.html#onsuccess-and-onfailure-props-have-moved

Change-Id: I30ae51e06df0293391988a7a84be9c6ef2b158b3
2024-02-06 15:11:12 +01:00
dklimpel 691969e1a1 Fix translation of device_id in EventReport
Change-Id: Ife6cfdae1fce9b477fc12b2e0cdd6bcea4b8b734
2024-02-06 15:11:07 +01:00
Manuel Stahl d520c6d618 Export resources as objects
Change-Id: I3c501369abf27fa21293c0434c56a00aaf8a64cd
2024-02-05 15:59:43 +01:00
Manuel Stahl af453eea71 Remove/mark unused parameters
All top level components should pass props to the generic react-admin
component to be more versatile.

Change-Id: I25dd099cde1aefacbc748dc4716a8b0a3db9ab93
2024-02-05 15:59:43 +01:00
Manuel Stahl 78d1d34a84 Simplify filters
Change-Id: I3e4cb7134a92c949bfb62d753c682a6c8fca6736
2024-02-05 15:59:43 +01:00
dklimpel a222af273f Update dataProvider hooks
Change-Id: Ic19f7a6ad97b1392c96c91a19e76b8983c9d0fd2
2024-02-05 15:59:43 +01:00
Manuel Stahl 51def5775d Replace Fragment with short form
https://legacy.reactjs.org/docs/fragments.html#short-syntax

Change-Id: Ib1af57fc5e87ded8c1fee38dcbd60fae8621cb07
2024-02-05 15:59:43 +01:00
Manuel Stahl 6363e3d32e Use icon as loading spinner in login page
Change-Id: Ie0e8d0a9e1242849fb8b18875d752dd15facaaf9
2024-02-05 15:59:43 +01:00
37 changed files with 2972 additions and 8697 deletions
+1
View File
@@ -1,6 +1,7 @@
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
tests/
build/
dist/
lib/
node_modules/
electron_app/
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
yarn build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.3
uses: JamesIves/github-pages-deploy-action@v4.5.0
with:
branch: gh-pages
folder: build
+1
View File
@@ -10,6 +10,7 @@
# production
/build
/dist
# misc
.DS_Store
+3 -3
View File
@@ -13,10 +13,10 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
### 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`.
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.
@@ -27,7 +27,7 @@ You need access to the following endpoints:
- `/_matrix`
- `/_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
+149
View File
@@ -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
View File
@@ -10,34 +10,34 @@
"url": "https://github.com/Awesome-Technologies/synapse-admin"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.4.3",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.2",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"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",
"prettier": "^2.2.0"
"prettier": "^3.2.5",
"vite": "^4.0.0"
},
"dependencies": {
"@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.8",
"@mui/styles": "5.14.10",
"@mui/icons-material": "^5.15.7",
"@mui/material": "^5.15.7",
"@mui/styles": "^5.15.8",
"papaparse": "^5.4.1",
"prop-types": "^15.8.1",
"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-italian": "^3.13.1",
"react": "^17.0.0",
"react": "^18.0.0",
"react-admin": "^4.16.9",
"react-dom": "^17.0.2",
"react-scripts": "^5.0.1"
"react-dom": "^18.0.0"
},
"scripts": {
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
"build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build",
"start": "REACT_APP_VERSION=$(git describe --tags) vite serve",
"build": "REACT_APP_VERSION=$(git describe --tags) vite build",
"fix:other": "yarn prettier --write",
"fix:code": "yarn test:lint --fix",
"fix": "yarn fix:code && yarn fix:other",
@@ -45,8 +45,7 @@
"test:code": "react-scripts test",
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
"test:style": "yarn prettier --list-different",
"test": "yarn test:style && yarn test:lint && yarn test:code",
"eject": "react-scripts eject"
"test": "yarn test:style && yarn test:lint && yarn test:code"
},
"eslintConfig": {
"extends": "react-app"
+1 -1
View File
@@ -46,4 +46,4 @@
</a>
</footer>
</body>
</html>
</html>
-112
View File
@@ -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
View File
@@ -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 record = useRecordContext(rest);
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;
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
import { RecordContextProvider } from "react-admin";
import { render, screen } from "@testing-library/react";
import AvatarField from "./AvatarField";
describe("AvatarField", () => {
it("shows image", () => {
const value = {
avatar: "foo",
};
render(
<RecordContextProvider value={value}>
<AvatarField source="avatar" />
</RecordContextProvider>
);
expect(screen.getByRole("img").getAttribute("src")).toBe("foo");
});
});
@@ -2,6 +2,7 @@ import React from "react";
import {
Datagrid,
DateField,
DeleteButton,
List,
NumberField,
Pagination,
@@ -10,9 +11,12 @@ import {
Tab,
TabbedShowLayout,
TextField,
TopToolbar,
useRecordContext,
useTranslate,
} from "react-admin";
import PageviewIcon from "@mui/icons-material/Pageview";
import ReportIcon from "@mui/icons-material/Warning";
import ViewListIcon from "@mui/icons-material/ViewList";
const date_format = {
@@ -24,14 +28,14 @@ const date_format = {
second: "2-digit",
};
const ReportPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
const ReportPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
export const ReportShow = props => {
const translate = useTranslate();
return (
<Show {...props}>
<Show {...props} actions={<ReportShowActions />}>
<TabbedShowLayout>
<Tab
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.device_id"
label="resources.users.fields.device_id"
label="resources.devices.fields.device_id"
/>
</Tab>
</TabbedShowLayout>
@@ -98,26 +102,47 @@ export const ReportShow = props => {
);
};
export const ReportList = ({ ...props }) => {
const ReportShowActions = () => {
const record = useRecordContext();
return (
<List
{...props}
pagination={<ReportPagination />}
sort={{ field: "received_ts", order: "DESC" }}
bulkActionButtons={false}
>
<Datagrid rowClick="show">
<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>
<TopToolbar>
<DeleteButton
record={record}
mutationMode="pessimistic"
confirmTitle="resources.reports.action.erase.title"
confirmContent="resources.reports.action.erase.content"
/>
</TopToolbar>
);
};
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>;
}
const FilePicker = props => {
const FilePicker = () => {
const [values, setValues] = useState(null);
const [error, setError] = useState(null);
const [stats, setStats] = useState(null);
@@ -191,7 +191,7 @@ const FilePicker = props => {
return true;
};
const runImport = async e => {
const runImport = async _e => {
if (progress !== null) {
notify("import_users.errors.already_in_progress");
return;
@@ -307,7 +307,7 @@ const FilePicker = props => {
let retries = 0;
const submitRecord = recordData => {
return dataProvider.getOne("users", { id: recordData.id }).then(
async alreadyExists => {
async _alreadyExists => {
if (LOGGING) console.log("already existed");
if (useridMode === "update" || conflictMode === "skip") {
@@ -332,7 +332,7 @@ const FilePicker = props => {
}
}
},
async okToSubmit => {
async _okToSubmit => {
if (LOGGING)
console.log(
"OK to create record " +
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import {
fetchUtils,
Form,
FormDataConsumer,
Notification,
@@ -8,6 +7,7 @@ import {
useLogin,
useNotify,
useLocaleState,
useStoreContext,
useTranslate,
PasswordInput,
TextInput,
@@ -27,6 +27,13 @@ import {
} from "@mui/material";
import { styled } from "@mui/material/styles";
import LockIcon from "@mui/icons-material/Lock";
import {
getServerVersion,
getSupportedLoginFlows,
getWellKnownUrl,
isValidBaseUrl,
splitMxid,
} from "../synapse/synapse";
const FormBox = styled(Box)(({ theme }) => ({
display: "flex",
@@ -75,15 +82,15 @@ const FormBox = styled(Box)(({ theme }) => ({
},
}));
const LoginPage = () => {
const LoginPage = ({ cfg_base_url }) => {
const login = useLogin();
const notify = useNotify();
const store = useStoreContext();
const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true);
const [locale, setLocale] = useLocaleState();
const translate = useTranslate();
const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER;
const base_url = store.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
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]
);
const baseUrl = localStorage.getItem("sso_base_url");
localStorage.removeItem("sso_base_url");
const baseUrl = store.getItem("sso_base_url");
store.removeItem("sso_base_url");
if (baseUrl) {
const auth = {
base_url: baseUrl,
@@ -113,8 +120,8 @@ const LoginPage = () => {
typeof error === "string"
? error
: typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error"
: error.message
? "ra.auth.sign_in_error"
: error.message
);
console.error(error);
});
@@ -155,102 +162,57 @@ const LoginPage = () => {
typeof error === "string"
? error
: typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error"
: error.message,
? "ra.auth.sign_in_error"
: error.message,
{ type: "warning" }
);
});
};
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(
window.location.href
)}`;
window.location.href = ssoFullUrl;
};
const extractHomeServer = username => {
const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/;
if (!username) return null;
const res = username.match(usernameRegex);
if (res) return res[1];
return null;
};
const UserData = ({ formData }) => {
const form = useFormContext();
const [serverVersion, setServerVersion] = useState("");
const handleUsernameChange = _ => {
if (formData.base_url || cfg_base_url) return;
// check if username is a full qualified userId then set base_url accordially
const home_server = extractHomeServer(formData.username);
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
if (home_server) {
// fetch .well-known entry to get base_url
fetchUtils
.fetchJson(wellKnownUrl, { method: "GET" })
.then(({ json }) => {
form.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}`);
});
// check if username is a full qualified userId then set base_url accordingly
const domain = splitMxid(formData.username)?.domain;
if (domain) {
getWellKnownUrl(domain).then(url => form.setValue("base_url", url));
}
};
useEffect(
_ => {
if (
!formData.base_url ||
!formData.base_url.match(
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/
useEffect(() => {
if (!isValidBaseUrl(formData.base_url)) return;
getServerVersion(formData.base_url)
.then(serverVersion =>
setServerVersion(
`${translate("synapseadmin.auth.server_version")} ${serverVersion}`
)
)
return;
const versionUrl = `${formData.base_url}/_synapse/admin/v1/server_version`;
fetchUtils
.fetchJson(versionUrl, { method: "GET" })
.then(({ json }) => {
setServerVersion(
`${translate("synapseadmin.auth.server_version")} ${
json["server_version"]
}`
);
})
.catch(_ => {
setServerVersion("");
});
.catch(() => setServerVersion(""));
// Set SSO Url
const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`;
let supportPass = false,
supportSSO = false;
fetchUtils
.fetchJson(authMethodUrl, { method: "GET" })
.then(({ json }) => {
json.flows.forEach(f => {
if (f.type === "m.login.password") {
supportPass = true;
} else if (f.type === "m.login.sso") {
supportSSO = true;
}
});
setSupportPassAuth(supportPass);
if (supportSSO) {
setSSOBaseUrl(formData.base_url);
} else {
setSSOBaseUrl("");
}
})
.catch(_ => {
setSSOBaseUrl("");
});
},
[formData.base_url]
);
// Set SSO Url
getSupportedLoginFlows(formData.base_url)
.then(loginFlows => {
const supportPass =
loginFlows.find(f => f.type === "m.login.password") !== undefined;
const supportSSO =
loginFlows.find(f => f.type === "m.login.sso") !== undefined;
setSupportPassAuth(supportPass);
setSSOBaseUrl(supportSSO ? formData.base_url : "");
})
.catch(() => setSSOBaseUrl(""));
}, [formData.base_url]);
return (
<>
@@ -286,7 +248,7 @@ const LoginPage = () => {
name="base_url"
component={renderInput}
label="synapseadmin.auth.base_url"
disabled={cfg_base_url || loading}
disabled={cfg_base_url != null || loading}
resettable
fullWidth
className="input"
@@ -307,9 +269,13 @@ const LoginPage = () => {
<FormBox>
<Card className="card">
<Box className="avatar">
<Avatar className="icon">
<LockIcon />
</Avatar>
{loading ? (
<CircularProgress size={25} thickness={2} />
) : (
<Avatar className="icon">
<LockIcon />
</Avatar>
)}
</Box>
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
<Box className="form">
@@ -339,7 +305,6 @@ const LoginPage = () => {
disabled={loading || !supportPassAuth}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("ra.auth.sign_in")}
</Button>
<Button
@@ -349,7 +314,6 @@ const LoginPage = () => {
disabled={loading || ssoBaseUrl === ""}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("synapseadmin.auth.sso_sign_in")}
</Button>
</CardActions>
@@ -6,18 +6,19 @@ import {
DateField,
DateTimeInput,
Edit,
Filter,
List,
maxValue,
number,
NumberField,
NumberInput,
regex,
SaveButton,
SimpleForm,
TextInput,
TextField,
Toolbar,
} from "react-admin";
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
const date_format = {
year: "numeric",
@@ -53,40 +54,41 @@ const dateFormatter = v => {
return `${year}-${month}-${day}T${hour}:${minute}`;
};
const RegistrationTokenFilter = props => (
<Filter {...props}>
<BooleanInput source="valid" alwaysOn />
</Filter>
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
export const RegistrationTokenList = props => (
<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 => (
<Create {...props}>
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}>
<Create {...props} redirect="list">
<SimpleForm
toolbar={
<Toolbar>
{/* It is possible to create tokens per default without input. */}
<SaveButton alwaysEnable />
</Toolbar>
}
>
<TextInput
source="token"
autoComplete="off"
@@ -109,24 +111,32 @@ export const RegistrationTokenCreate = props => (
</Create>
);
export const RegistrationTokenEdit = props => {
return (
<Edit {...props}>
<SimpleForm>
<TextInput source="token" disabled />
<NumberInput source="pending" disabled />
<NumberInput source="completed" disabled />
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<DateTimeInput
source="expiry_time"
parse={dateParser}
format={dateFormatter}
/>
</SimpleForm>
</Edit>
);
export const RegistrationTokenEdit = props => (
<Edit {...props}>
<SimpleForm>
<TextInput source="token" disabled />
<NumberInput source="pending" disabled />
<NumberInput source="completed" disabled />
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<DateTimeInput
source="expiry_time"
parse={dateParser}
format={dateFormatter}
/>
</SimpleForm>
</Edit>
);
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 FolderSharedIcon from "@mui/icons-material/FolderShared";
import React from "react";
import {
BooleanField,
BulkDeleteButton,
@@ -14,6 +13,7 @@ import {
TextField,
TopToolbar,
useCreate,
useDataProvider,
useListContext,
useNotify,
useTranslate,
@@ -22,13 +22,14 @@ import {
useUnselectAll,
} from "react-admin";
import { useMutation } from "react-query";
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
import AvatarField from "./AvatarField";
const RoomDirectoryPagination = props => (
<Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} />
const RoomDirectoryPagination = () => (
<Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
);
export const RoomDirectoryDeleteButton = props => {
export const RoomDirectoryUnpublishButton = props => {
const translate = useTranslate();
return (
@@ -44,12 +45,12 @@ export const RoomDirectoryDeleteButton = props => {
smart_count: 1,
})}
resource="room_directory"
icon={<FolderSharedIcon />}
icon={<RoomDirectoryIcon />}
/>
);
};
export const RoomDirectoryBulkDeleteButton = props => (
export const RoomDirectoryBulkUnpublishButton = props => (
<BulkDeleteButton
{...props}
label="resources.room_directory.action.erase"
@@ -57,61 +58,63 @@ export const RoomDirectoryBulkDeleteButton = props => (
confirmTitle="resources.room_directory.action.title"
confirmContent="resources.room_directory.action.content"
resource="room_directory"
icon={<FolderSharedIcon />}
icon={<RoomDirectoryIcon />}
/>
);
export const RoomDirectoryBulkSaveButton = () => {
export const RoomDirectoryBulkPublishButton = props => {
const { selectedIds } = useListContext();
const notify = useNotify();
const refresh = useRefresh();
const unselectAll = useUnselectAll();
const { createMany, isloading } = useMutation();
const handleSend = values => {
createMany(
["room_directory", "createMany", { ids: selectedIds, data: {} }],
{
onSuccess: data => {
notify("resources.room_directory.action.send_success");
unselectAll("rooms");
refresh();
},
onError: error =>
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
}
);
};
const unselectAllRooms = useUnselectAll("rooms");
const dataProvider = useDataProvider();
const { mutate, isLoading } = useMutation(
() =>
dataProvider.createMany("room_directory", {
ids: selectedIds,
data: {},
}),
{
onSuccess: () => {
notify("resources.room_directory.action.send_success");
unselectAllRooms();
refresh();
},
onError: () =>
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
}
);
return (
<Button
{...props}
label="resources.room_directory.action.create"
onClick={handleSend}
disabled={isloading}
onClick={mutate}
disabled={isLoading}
>
<FolderSharedIcon />
<RoomDirectoryIcon />
</Button>
);
};
export const RoomDirectorySaveButton = () => {
export const RoomDirectoryPublishButton = props => {
const record = useRecordContext();
const notify = useNotify();
const refresh = useRefresh();
const [create, { isloading }] = useCreate();
const [create, { isLoading }] = useCreate();
const handleSend = values => {
const handleSend = () => {
create(
"room_directory",
{ data: { id: record.id } },
{
onSuccess: data => {
onSuccess: () => {
notify("resources.room_directory.action.send_success");
refresh();
},
onError: error =>
onError: () =>
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
@@ -121,21 +124,16 @@ export const RoomDirectorySaveButton = () => {
return (
<Button
{...props}
label="resources.room_directory.action.create"
onClick={handleSend}
disabled={isloading}
disabled={isLoading}
>
<FolderSharedIcon />
<RoomDirectoryIcon />
</Button>
);
};
const RoomDirectoryBulkActionButtons = () => (
<Fragment>
<RoomDirectoryBulkDeleteButton />
</Fragment>
);
const RoomDirectoryListActions = () => (
<TopToolbar>
<SelectColumnsButton />
@@ -150,8 +148,8 @@ export const RoomDirectoryList = () => (
actions={<RoomDirectoryListActions />}
>
<DatagridConfigurable
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
bulkActionButtons={<RoomDirectoryBulkActionButtons />}
rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
omit={["room_id", "canonical_alias", "topic"]}
>
<AvatarField
@@ -198,3 +196,11 @@ export const RoomDirectoryList = () => (
</DatagridConfigurable>
</List>
);
const resource = {
name: "room_directory",
icon: RoomDirectoryIcon,
list: RoomDirectoryList,
};
export default resource;
@@ -1,4 +1,4 @@
import React, { Fragment, useState } from "react";
import React, { useState } from "react";
import {
Button,
SaveButton,
@@ -7,6 +7,7 @@ import {
Toolbar,
required,
useCreate,
useDataProvider,
useListContext,
useNotify,
useRecordContext,
@@ -23,7 +24,7 @@ import {
DialogTitle,
} from "@mui/material";
const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
const translate = useTranslate();
const ServerNoticeToolbar = props => (
@@ -47,11 +48,7 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
<DialogContentText>
{translate("resources.servernotices.helper.send")}
</DialogContentText>
<SimpleForm
toolbar={<ServerNoticeToolbar />}
redirect={false}
save={onSend}
>
<SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
<TextInput
source="body"
label="resources.servernotices.fields.body"
@@ -71,14 +68,15 @@ export const ServerNoticeButton = () => {
const record = useRecordContext();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [create, { isloading }] = useCreate("servernotices");
const [create, { isloading }] = useCreate();
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleSend = values => {
create(
{ payload: { data: { id: record.id, ...values } } },
"servernotices",
{ data: { id: record.id, ...values } },
{
onSuccess: () => {
notify("resources.servernotices.action.send_success");
@@ -93,7 +91,7 @@ export const ServerNoticeButton = () => {
};
return (
<Fragment>
<>
<Button
label="resources.servernotices.send"
onClick={handleDialogOpen}
@@ -104,53 +102,54 @@ export const ServerNoticeButton = () => {
<ServerNoticeDialog
open={open}
onClose={handleDialogClose}
onSend={handleSend}
onSubmit={handleSend}
/>
</Fragment>
</>
);
};
export const ServerNoticeBulkButton = () => {
const { selectedIds } = useListContext();
const [open, setOpen] = useState(false);
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const notify = useNotify();
const unselectAll = useUnselectAll();
const { createMany, isloading } = useMutation();
const unselectAllUsers = useUnselectAll("users");
const dataProvider = useDataProvider();
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleSend = values => {
createMany(
["servernotices", "createMany", { ids: selectedIds, data: values }],
{
onSuccess: data => {
notify("resources.servernotices.action.send_success");
unselectAll("users");
handleDialogClose();
},
onError: error =>
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
}
);
};
const { mutate: sendNotices, isLoading } = useMutation(
data =>
dataProvider.createMany("servernotices", {
ids: selectedIds,
data: data,
}),
{
onSuccess: () => {
notify("resources.servernotices.action.send_success");
unselectAllUsers();
closeDialog();
},
onError: () =>
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
}
);
return (
<Fragment>
<>
<Button
label="resources.servernotices.send"
onClick={handleDialogOpen}
disabled={isloading}
onClick={openDialog}
disabled={isLoading}
>
<MessageIcon />
</Button>
<ServerNoticeDialog
open={open}
onClose={handleDialogClose}
onSend={handleSend}
onClose={closeDialog}
onSubmit={sendNotices}
/>
</Fragment>
</>
);
};
@@ -3,7 +3,6 @@ import {
Button,
Datagrid,
DateField,
Filter,
List,
Pagination,
ReferenceField,
@@ -21,11 +20,12 @@ import {
useTranslate,
} from "react-admin";
import AutorenewIcon from "@mui/icons-material/Autorenew";
import DestinationsIcon from "@mui/icons-material/CloudQueue";
import FolderSharedIcon from "@mui/icons-material/FolderShared";
import ViewListIcon from "@mui/icons-material/ViewList";
const DestinationPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
const DestinationPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const date_format = {
@@ -41,19 +41,13 @@ const destinationRowSx = (record, _index) => ({
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
});
const DestinationFilter = props => {
return (
<Filter {...props}>
<SearchInput source="destination" alwaysOn />
</Filter>
);
};
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
export const DestinationReconnectButton = props => {
export const DestinationReconnectButton = () => {
const record = useRecordContext();
const refresh = useRefresh();
const notify = useNotify();
const [handleReconnect, { isLoading }] = useDelete("destinations");
const [handleReconnect, { isLoading }] = useDelete();
// Reconnect is not required if no error has occurred. (`failure_ts`)
if (!record || !record.failure_ts) return null;
@@ -63,7 +57,8 @@ export const DestinationReconnectButton = props => {
e.stopPropagation();
handleReconnect(
{ payload: { id: record.id } },
"destinations",
{ id: record.id },
{
onSuccess: () => {
notify("ra.notification.updated", {
@@ -89,13 +84,13 @@ export const DestinationReconnectButton = props => {
);
};
const DestinationShowActions = props => (
const DestinationShowActions = () => (
<TopToolbar>
<DestinationReconnectButton />
</TopToolbar>
);
const DestinationTitle = props => {
const DestinationTitle = () => {
const record = useRecordContext();
const translate = useTranslate();
return (
@@ -109,7 +104,7 @@ export const DestinationList = props => {
return (
<List
{...props}
filters={<DestinationFilter />}
filters={destinationFilters}
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
>
@@ -183,3 +178,12 @@ export const DestinationShow = props => {
</Show>
);
};
const resource = {
name: "destinations",
icon: DestinationsIcon,
list: DestinationList,
show: DestinationShow,
};
export default resource;
-75
View File
@@ -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>
);
};
+51
View File
@@ -0,0 +1,51 @@
import React from "react";
import {
DeleteButton,
useDelete,
useNotify,
useRecordContext,
useRefresh,
} from "react-admin";
export const DeviceRemoveButton = props => {
const record = useRecordContext();
const refresh = useRefresh();
const notify = useNotify();
const [removeDevice] = useDelete();
if (!record) return null;
const handleConfirm = () => {
removeDevice(
"devices",
// needs previousData for user_id
{ id: record.id, previousData: record },
{
onSuccess: () => {
notify("resources.devices.action.erase.success");
refresh();
},
onError: () => {
notify("resources.devices.action.erase.failure", { type: "error" });
},
}
);
};
return (
<DeleteButton
{...props}
label="ra.action.remove"
confirmTitle="resources.devices.action.erase.title"
confirmContent="resources.devices.action.erase.content"
onConfirm={handleConfirm}
mutationMode="pessimistic"
redirect={false}
translateOptions={{
id: record.id,
name: record.display_name ? record.display_name : record.id,
}}
/>
);
};
@@ -1,4 +1,4 @@
import React, { Fragment, useState } from "react";
import React, { useState } from "react";
import {
BooleanInput,
Button,
@@ -29,7 +29,7 @@ import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import { alpha, useTheme } from "@mui/material/styles";
const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
const translate = useTranslate();
const dateParser = v => {
@@ -38,19 +38,17 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
return d.getTime();
};
const DeleteMediaToolbar = props => {
return (
<Toolbar {...props}>
<SaveButton
label="resources.delete_media.action.send"
icon={<DeleteSweepIcon />}
/>
<Button label="ra.action.cancel" onClick={onClose}>
<IconCancel />
</Button>
</Toolbar>
);
};
const DeleteMediaToolbar = props => (
<Toolbar {...props}>
<SaveButton
label="resources.delete_media.action.send"
icon={<DeleteSweepIcon />}
/>
<Button label="ra.action.cancel" onClick={onClose}>
<IconCancel />
</Button>
</Toolbar>
);
return (
<Dialog open={open} onClose={onClose} loading={loading}>
@@ -61,11 +59,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
<DialogContentText>
{translate("resources.delete_media.helper.send")}
</DialogContentText>
<SimpleForm
toolbar={<DeleteMediaToolbar />}
redirect={false}
save={onSend}
>
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
<DateTimeInput
fullWidth
source="before_ts"
@@ -97,18 +91,20 @@ export const DeleteMediaButton = props => {
const theme = useTheme();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [deleteOne, { isLoading }] = useDelete("delete_media");
const [deleteOne, { isLoading }] = useDelete();
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const handleSend = values => {
const deleteMedia = values => {
deleteOne(
{ payload: { ...values } },
"delete_media",
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
{ meta: values },
{
onSuccess: () => {
notify("resources.delete_media.action.send_success");
handleDialogClose();
closeDialog();
},
onError: () =>
notify("resources.delete_media.action.send_failure", {
@@ -119,10 +115,11 @@ export const DeleteMediaButton = props => {
};
return (
<Fragment>
<>
<Button
{...props}
label="resources.delete_media.action.send"
onClick={handleDialogOpen}
onClick={openDialog}
disabled={isLoading}
sx={{
color: theme.palette.error.main,
@@ -139,26 +136,27 @@ export const DeleteMediaButton = props => {
</Button>
<DeleteMediaDialog
open={open}
onClose={handleDialogClose}
onSend={handleSend}
onClose={closeDialog}
onSubmit={deleteMedia}
/>
</Fragment>
</>
);
};
export const ProtectMediaButton = props => {
export const ProtectMediaButton = () => {
const record = useRecordContext();
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { loading }] = useCreate("protect_media");
const [deleteOne] = useDelete("protect_media");
const [create, { isLoading }] = useCreate();
const [deleteOne] = useDelete();
if (!record) return null;
const handleProtect = () => {
create(
{ payload: { data: record } },
"protect_media",
{ data: record },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
@@ -174,7 +172,8 @@ export const ProtectMediaButton = props => {
const handleUnprotect = () => {
deleteOne(
{ payload: { ...record } },
"protect_media",
{ id: record.id },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
@@ -193,7 +192,7 @@ export const ProtectMediaButton = props => {
Wrapping Tooltip with <div>
https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
*/
<Fragment>
<>
{record.quarantined_by && (
<Tooltip
title={translate("resources.protect_media.action.none", {
@@ -219,7 +218,7 @@ export const ProtectMediaButton = props => {
arrow
>
<div>
<Button onClick={handleUnprotect} disabled={loading}>
<Button onClick={handleUnprotect} disabled={isLoading}>
<LockIcon />
</Button>
</div>
@@ -232,13 +231,13 @@ export const ProtectMediaButton = props => {
})}
>
<div>
<Button onClick={handleProtect} disabled={loading}>
<Button onClick={handleProtect} disabled={isLoading}>
<LockOpenIcon />
</Button>
</div>
</Tooltip>
)}
</Fragment>
</>
);
};
@@ -247,14 +246,15 @@ export const QuarantineMediaButton = props => {
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { loading }] = useCreate("quarantine_media");
const [deleteOne] = useDelete("quarantine_media");
const [create, { isLoading }] = useCreate();
const [deleteOne] = useDelete();
if (!record) return null;
const handleQuarantaine = () => {
create(
{ payload: { data: record } },
"quarantine_media",
{ data: record },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
@@ -270,7 +270,8 @@ export const QuarantineMediaButton = props => {
const handleRemoveQuarantaine = () => {
deleteOne(
{ payload: { ...record } },
"quarantine_media",
{ id: record.id, previousData: record },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
@@ -285,7 +286,7 @@ export const QuarantineMediaButton = props => {
};
return (
<Fragment>
<>
{record.safe_from_quarantine && (
<Tooltip
title={translate("resources.quarantine_media.action.none", {
@@ -293,7 +294,7 @@ export const QuarantineMediaButton = props => {
})}
>
<div>
<Button disabled={true}>
<Button {...props} disabled={true}>
<ClearIcon />
</Button>
</div>
@@ -306,7 +307,11 @@ export const QuarantineMediaButton = props => {
})}
>
<div>
<Button onClick={handleRemoveQuarantaine} disabled={loading}>
<Button
{...props}
onClick={handleRemoveQuarantaine}
disabled={isLoading}
>
<BlockIcon color="error" />
</Button>
</div>
@@ -319,12 +324,12 @@ export const QuarantineMediaButton = props => {
})}
>
<div>
<Button onClick={handleQuarantaine} disabled={loading}>
<Button onClick={handleQuarantaine} disabled={isLoading}>
<BlockIcon />
</Button>
</div>
</Tooltip>
)}
</Fragment>
</>
);
};
@@ -1,4 +1,4 @@
import React, { Fragment } from "react";
import React from "react";
import {
BooleanField,
BulkDeleteButton,
@@ -7,7 +7,6 @@ import {
DatagridConfigurable,
DeleteButton,
ExportButton,
Filter,
FunctionField,
List,
NumberField,
@@ -35,11 +34,12 @@ import UserIcon from "@mui/icons-material/Group";
import ViewListIcon from "@mui/icons-material/ViewList";
import VisibilityIcon from "@mui/icons-material/Visibility";
import EventIcon from "@mui/icons-material/Event";
import RoomIcon from "@mui/icons-material/ViewList";
import {
RoomDirectoryBulkDeleteButton,
RoomDirectoryBulkSaveButton,
RoomDirectoryDeleteButton,
RoomDirectorySaveButton,
RoomDirectoryBulkUnpublishButton,
RoomDirectoryBulkPublishButton,
RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton,
} from "./RoomDirectory";
const date_format = {
@@ -51,11 +51,11 @@ const date_format = {
second: "2-digit",
};
const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
const RoomPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const RoomTitle = props => {
const RoomTitle = () => {
const record = useRecordContext();
const translate = useTranslate();
var name = "";
@@ -70,23 +70,18 @@ const RoomTitle = props => {
);
};
const RoomShowActions = ({ data, resource }) => {
const RoomShowActions = () => {
const record = useRecordContext();
var roomDirectoryStatus = "";
if (data) {
roomDirectoryStatus = data.public;
if (record) {
roomDirectoryStatus = record.public;
}
return (
<TopToolbar>
{roomDirectoryStatus === false && (
<RoomDirectorySaveButton record={data} />
)}
{roomDirectoryStatus === true && (
<RoomDirectoryDeleteButton record={data} />
)}
{roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
{roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
<DeleteButton
record={data}
resource={resource}
mutationMode="pessimistic"
confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content"
@@ -103,6 +98,7 @@ export const RoomShow = props => {
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
<TextField source="room_id" />
<TextField source="name" />
<TextField source="topic" />
<TextField source="canonical_alias" />
<ReferenceField source="creator" reference="users">
<TextField source="id" />
@@ -279,22 +275,18 @@ export const RoomShow = props => {
};
const RoomBulkActionButtons = () => (
<Fragment>
<RoomDirectoryBulkSaveButton />
<RoomDirectoryBulkDeleteButton />
<>
<RoomDirectoryBulkPublishButton />
<RoomDirectoryBulkUnpublishButton />
<BulkDeleteButton
confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic"
/>
</Fragment>
</>
);
const RoomFilter = props => (
<Filter {...props}>
<SearchInput source="search_term" alwaysOn />
</Filter>
);
const roomFilters = [<SearchInput source="search_term" alwaysOn />];
const RoomListActions = () => (
<TopToolbar>
@@ -303,14 +295,15 @@ const RoomListActions = () => (
</TopToolbar>
);
export const RoomList = () => {
export const RoomList = props => {
const theme = useTheme();
return (
<List
{...props}
pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }}
filters={<RoomFilter />}
filters={roomFilters}
actions={<RoomListActions />}
>
<DatagridConfigurable
@@ -350,3 +343,12 @@ export const RoomList = () => {
</List>
);
};
const resource = {
name: "rooms",
icon: RoomIcon,
list: RoomList,
show: RoomShow,
};
export default resource;
-83
View File
@@ -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>
);
};
+79
View File
@@ -0,0 +1,79 @@
import React from "react";
import { cloneElement } from "react";
import {
Datagrid,
ExportButton,
List,
NumberField,
Pagination,
sanitizeListRestProps,
SearchInput,
TextField,
TopToolbar,
useListContext,
} from "react-admin";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import { DeleteMediaButton } from "./media";
const ListActions = props => {
const { className, exporter, filters, maxResults, ...rest } = props;
const { sort, resource, displayedFilters, filterValues, showFilter, total } =
useListContext();
return (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters &&
cloneElement(filters, {
resource,
showFilter,
displayedFilters,
filterValues,
context: "button",
})}
<DeleteMediaButton />
<ExportButton
disabled={total === 0}
resource={resource}
sort={sort}
filterValues={filterValues}
maxResults={maxResults}
/>
</TopToolbar>
);
};
const UserMediaStatsPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
export const UserMediaStatsList = props => (
<List
{...props}
actions={<ListActions />}
filters={userMediaStatsFilters}
pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }}
>
<Datagrid
rowClick={(id, resource, record) => "/users/" + id + "/media"}
bulkActionButtons={false}
>
<TextField source="user_id" label="resources.users.fields.id" />
<TextField
source="displayname"
label="resources.users.fields.displayname"
/>
<NumberField source="media_count" />
<NumberField source="media_length" />
</Datagrid>
</List>
);
const resource = {
name: "user_media_statistics",
icon: EqualizerIcon,
list: UserMediaStatsList,
};
export default resource;
@@ -1,4 +1,4 @@
import React, { cloneElement, Fragment } from "react";
import React, { cloneElement } from "react";
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import ContactMailIcon from "@mui/icons-material/ContactMail";
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 PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import UserIcon from "@mui/icons-material/Group";
import ViewListIcon from "@mui/icons-material/ViewList";
import {
ArrayInput,
@@ -17,7 +18,6 @@ import {
Create,
Edit,
List,
Filter,
Toolbar,
SimpleForm,
SimpleFormIterator,
@@ -73,7 +73,7 @@ const date_format = {
};
const UserListActions = ({
currentSort,
sort,
className,
resource,
filters,
@@ -103,7 +103,7 @@ const UserListActions = ({
<ExportButton
disabled={total === 0}
resource={resource}
sort={currentSort}
sort={sort}
filter={{ ...filterValues, ...permanentFilter }}
exporter={exporter}
maxResults={maxResults}
@@ -121,65 +121,60 @@ UserListActions.defaultProps = {
onUnselectItems: () => null,
};
const UserPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
const UserPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const UserFilter = props => (
<Filter {...props}>
<SearchInput source="name" alwaysOn />
<BooleanInput source="guests" alwaysOn />
<BooleanInput
label="resources.users.fields.show_deactivated"
source="deactivated"
alwaysOn
/>
</Filter>
);
const userFilters = [
<SearchInput source="name" alwaysOn />,
<BooleanInput source="guests" alwaysOn />,
<BooleanInput
label="resources.users.fields.show_deactivated"
source="deactivated"
alwaysOn
/>,
];
const UserBulkActionButtons = props => (
<Fragment>
<ServerNoticeBulkButton {...props} />
const UserBulkActionButtons = () => (
<>
<ServerNoticeBulkButton />
<BulkDeleteButton
{...props}
label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
/>
</Fragment>
</>
);
export const UserList = props => {
return (
<List
{...props}
filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField
source="avatar_src"
sx={{ height: "40px", width: "40px" }}
sortBy="avatar_url"
/>
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<BooleanField source="deactivated" />
<DateField
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
options={date_format}
/>
</Datagrid>
</List>
);
};
export const UserList = props => (
<List
{...props}
filters={userFilters}
filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField
source="avatar_src"
sx={{ height: "40px", width: "40px" }}
sortBy="avatar_url"
/>
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<BooleanField source="deactivated" />
<DateField
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
options={date_format}
/>
</Datagrid>
</List>
);
// https://matrix.org/docs/spec/appendices#user-identifiers
// here only local part of user_id
@@ -303,7 +298,7 @@ export const UserCreate = props => (
</Create>
);
const UserTitle = props => {
const UserTitle = () => {
const record = useRecordContext();
const translate = useTranslate();
return (
@@ -530,3 +525,13 @@ export const UserEdit = props => {
</Edit>
);
};
const resource = {
name: "users",
icon: UserIcon,
list: UserList,
edit: UserEdit,
create: UserCreate,
};
export default resource;
+8 -1
View File
@@ -188,7 +188,7 @@ const de = {
},
},
reports: {
name: "Ereignisbericht |||| Ereignisberichte",
name: "Gemeldetes Ereignis |||| Gemeldete Ereignisse",
fields: {
id: "ID",
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: {
name: "Verbindungen",
+7
View File
@@ -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: {
name: "Connections",
-5
View File
@@ -1,5 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
+11
View File
@@ -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>
);
+9 -9
View File
@@ -1,10 +1,10 @@
import { fetchUtils } from "react-admin";
const authProvider = {
const authProvider = (fixed_base_url, store) => ({
// called when the user attempts to log in
login: ({ base_url, username, password, loginToken }) => {
// 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 ");
const options = {
@@ -12,7 +12,7 @@ const authProvider = {
body: JSON.stringify(
Object.assign(
{
device_id: localStorage.getItem("device_id"),
device_id: store.getItem("device_id"),
initial_device_display_name: "Synapse Admin",
},
loginToken
@@ -33,16 +33,16 @@ const authProvider = {
// server, since the admin might want to access the admin API via some
// private address
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 login_api_url = decoded_base_url + "/_matrix/client/r0/login";
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
localStorage.setItem("home_server", json.home_server);
localStorage.setItem("user_id", json.user_id);
localStorage.setItem("access_token", json.access_token);
localStorage.setItem("device_id", json.device_id);
store.setItem("home_server", json.home_server);
store.setItem("user_id", json.user_id);
store.setItem("access_token", json.access_token);
store.setItem("device_id", json.device_id);
});
},
// 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
getPermissions: () => Promise.resolve(),
};
});
export default authProvider;
+8 -8
View File
@@ -98,7 +98,7 @@ const resourceMap = {
}),
delete: params => ({
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
params.user_id
params.previousData.user_id
)}/devices/${params.id}`,
}),
},
@@ -184,9 +184,9 @@ const resourceMap = {
delete: params => ({
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
"home_server"
)}/delete?before_ts=${params.before_ts}&size_gt=${
params.size_gt
}&keep_profiles=${params.keep_profiles}`,
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
params.meta.size_gt
}&keep_profiles=${params.meta.keep_profiles}`,
method: "POST",
}),
},
@@ -197,7 +197,7 @@ const resourceMap = {
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unprotect/${params.media_id}`,
endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
method: "POST",
}),
},
@@ -212,7 +212,7 @@ const resourceMap = {
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
"home_server"
)}/${params.media_id}`,
)}/${params.id}`,
method: "POST",
}),
},
@@ -456,7 +456,7 @@ const dataProvider = {
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, {
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
}).then(({ json }) => ({
@@ -546,7 +546,7 @@ const dataProvider = {
const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.id}`, {
method: "DELETE",
body: JSON.stringify(params.data, filterNullValues),
body: JSON.stringify(params.previousData, filterNullValues),
}).then(({ json }) => ({
data: json,
}));
+48
View File
@@ -0,0 +1,48 @@
import { fetchUtils } from "react-admin";
export const splitMxid = mxid => {
const re =
/^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
return re.exec(mxid)?.groups;
};
export const isValidBaseUrl = baseUrl =>
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
/**
* Resolve the homeserver URL using the well-known lookup
* @param domain the domain part of an MXID
* @returns homeserver base URL
*/
export const getWellKnownUrl = async domain => {
const wellKnownUrl = `https://${domain}/.well-known/matrix/client`;
try {
const json = await fetchUtils.fetchJson(wellKnownUrl, { method: "GET" });
return json["m.homeserver"].base_url;
} catch {
// if there is no .well-known entry, return the domain itself
return `https://${domain}`;
}
};
/**
* Get synapse server version
* @param base_url the base URL of the homeserver
* @returns server version
*/
export const getServerVersion = async baseUrl => {
const versionUrl = `${baseUrl}/_synapse/admin/v1/server_version`;
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
return response.json.server_version;
};
/**
* Get supported login flows
* @param baseUrl the base URL of the homeserver
* @returns array of supported login flows
*/
export const getSupportedLoginFlows = async baseUrl => {
const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`;
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
return response.json.flows;
};
+31
View File
@@ -0,0 +1,31 @@
import { isValidBaseUrl, splitMxid } from "./synapse";
describe("splitMxid", () => {
it("splits valid MXIDs", () =>
expect(splitMxid("@name:domain.tld")).toEqual({
name: "name",
domain: "domain.tld",
}));
it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined());
});
describe("isValidBaseUrl", () => {
it("accepts a http URL", () =>
expect(isValidBaseUrl("http://foo.bar")).toBeTruthy());
it("accepts a https URL", () =>
expect(isValidBaseUrl("https://foo.bar")).toBeTruthy());
it("accepts a valid URL with port", () =>
expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy());
it("rejects undefined base URLs", () =>
expect(isValidBaseUrl(undefined)).toBeFalsy());
it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy());
it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy());
it("rejects non-string base URLs", () =>
expect(isValidBaseUrl({})).toBeFalsy());
it("rejects base URLs without protocol", () =>
expect(isValidBaseUrl("foo.bar")).toBeFalsy());
it("rejects base URLs with path", () =>
expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy());
it("rejects invalid base URLs", () =>
expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy());
});
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
+2016 -7978
View File
File diff suppressed because it is too large Load Diff