Compare commits

..

67 Commits

Author SHA1 Message Date
311db77306
Disable registration tokens 2024-04-20 22:59:35 +03:00
7c08f846a5
Update translate 2024-04-20 22:52:08 +03:00
f2f096d8a5
Update russian language 2024-04-20 22:34:55 +03:00
16191d9cc8
Update russian language 2024-04-20 22:08:36 +03:00
703129f88b
Add russian language 2024-04-19 13:14:04 +03:00
Manuel Stahl
c9364f631b Fetch tags in github workflows
Tags are required to construct the version information.

Change-Id: Ic1af3e8f50eafafcc8a0c3ca37f362d6bd05e116
2024-04-18 21:13:06 +02:00
Manuel Stahl
08dc5f6271 Push docker images also to ghcr.io
Fixes #350.

Change-Id: Ifdb7e4e7fda46efd0ed9e760587033f52ff4a130
2024-04-18 17:44:06 +02:00
Manuel Stahl
c9cb9aa9e0 Show Matrix specs supported by the homeserver
Change-Id: I01c110fb4b3de4de49b34f290c91c8bf424521fe
2024-04-18 10:01:52 +02:00
Manuel Stahl
25020c2d5b Remove unused function "renderInput"
Seems to be obsolete since react-admin v4.

Change-Id: I9f1d528a43510efd61befd23a05d1c8ebf40ddfd
2024-04-18 10:01:52 +02:00
Manuel Stahl
1acffdb618 Make functions in dataProvider async
Change-Id: Iab36ba6379340e47e7d58b1b2d882cd7cc111f41
2024-04-18 10:01:52 +02:00
Manuel Stahl
0b4f3a60c0 Make login and logout in authProvider async
Change-Id: I6bfb1c7a5a3c5a43f9fa622e87d9d487a95a0b6e
2024-04-18 10:01:52 +02:00
Manuel Stahl
33d29e01b1 Add authProvider test
Change-Id: Ia5acce659a386437687e38ae03d578e3bccb9324
2024-04-18 10:01:52 +02:00
Gavin Mogan
a2e47cb793
Add source urls to docker so tools can find sourcecode (#506)
For tools like renovate or dependabot, they like to put changelog notes in PRs updating deps. Having the labels allows the tools to link it back to sourcecode and share commits/release notes
2024-04-17 20:32:41 +02:00
Manuel Stahl
7deb9bcf7e Bump version to 0.9.2
Change-Id: I8d5f98f10fe16189c12b2ce0f0fff073ec81fed5
2024-04-17 09:53:48 +02:00
Manuel Stahl
8185d7f0b0 Remove obsolete .travis.yml
Change-Id: I65f80b5bcf6db3fd85ddbb3ea25f86bb2466bace
2024-04-17 09:53:48 +02:00
Manuel Stahl
37e1fcc96d Fix App test
Change-Id: Iacaa6f5e70925b857f24554e6aba64234b1cae44
2024-04-17 09:53:48 +02:00
Fateme Shamohammadi
f6e193c51c Add farsi translations (#504)
Change-Id: Iee74dbf229197359a148dec7e75ef6f744a1856d
2024-04-16 14:53:05 +02:00
dependabot[bot]
fa1f86491f
Bump ra-language-french from 4.16.12 to 4.16.15 (#503)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 4.16.12 to 4.16.15.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v4.16.12...v4.16.15)

---
updated-dependencies:
- dependency-name: ra-language-french
  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-04-16 09:27:50 +02:00
dependabot[bot]
b6546b89ad
Bump the npm_and_yarn group with 3 updates (#496)
Bumps the npm_and_yarn group with 3 updates: [express](https://github.com/expressjs/express), [follow-redirects](https://github.com/follow-redirects/follow-redirects) and [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware).


Updates `express` from 4.18.2 to 4.19.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

Updates `follow-redirects` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

Updates `webpack-dev-middleware` from 5.3.3 to 5.3.4
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: follow-redirects
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 09:22:55 +02:00
dependabot[bot]
baa8e4ad95
Bump @testing-library/react from 14.2.1 to 15.0.2 (#505)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 14.2.1 to 15.0.2.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v14.2.1...v15.0.2)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 09:22:43 +02:00
dependabot[bot]
bd6d5847e1
Bump react-admin from 4.16.11 to 4.16.15 (#502)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 4.16.11 to 4.16.15.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v4.16.11...v4.16.15)

---
updated-dependencies:
- dependency-name: react-admin
  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-04-16 09:22:03 +02:00
dependabot[bot]
32c5867e2a
Bump @mui/material from 5.15.14 to 5.15.15 (#501)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.15.14 to 5.15.15.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/next/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.15/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  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-04-16 09:21:33 +02:00
dependabot[bot]
f68a5de64a
Bump softprops/action-gh-release from 1 to 2 (#482)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](de2c0eb89a...3198ee18f8)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 09:19:14 +02:00
dependabot[bot]
e326599da2
Bump @mui/icons-material from 5.15.10 to 5.15.15 (#499)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 5.15.10 to 5.15.15.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v5.15.15/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.15/packages/mui-icons-material)

---
updated-dependencies:
- dependency-name: "@mui/icons-material"
  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-04-04 12:28:59 +02:00
dependabot[bot]
8d6852ca8c
Bump @mui/styles from 5.15.10 to 5.15.15 (#498)
Bumps [@mui/styles](https://github.com/mui/material-ui/tree/HEAD/packages/mui-styles) from 5.15.10 to 5.15.15.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v5.15.15/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.15/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-04-04 12:28:49 +02:00
dependabot[bot]
ee859a2926
Bump @mui/material from 5.15.10 to 5.15.14 (#492)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.15.10 to 5.15.14.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/next/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.14/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  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-04-04 12:22:31 +02:00
dependabot[bot]
0c4ca1459f
Bump ra-language-french from 4.16.11 to 4.16.12 (#481)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 4.16.11 to 4.16.12.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v4.16.11...v4.16.12)

---
updated-dependencies:
- dependency-name: ra-language-french
  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-04-04 12:22:19 +02:00
dependabot[bot]
ebba0f66f7
Bump eslint from 8.56.0 to 8.57.0 (#479)
Bumps [eslint](https://github.com/eslint/eslint) from 8.56.0 to 8.57.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.56.0...v8.57.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  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-04-04 12:22:09 +02:00
Manuel Stahl
7d4d765ab4 Bump react-admin from 4.16.9 to 4.16.11
Bumps [react-admin](https://github.com/marmelab/react-admin) from 4.16.9 to 4.16.11.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v4.16.9...v4.16.11)

Change-Id: I6ae1c3ad892a65b707f9ee6e3a22b6be8f706394
2024-02-19 12:37:26 +01:00
Manuel Stahl
dfee94af96 Bump @mui/* from 5.15.7 to 5.15.10
- [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.10/packages/mui-material)

Change-Id: I59b486e1b3351f4e685d7bd317ea4d96c3d24209
2024-02-19 12:36:02 +01:00
Manuel Stahl
ae7f6e18e5 Use --immutable flag whenever "yarn install" is called by a tool
Fixes #347

Change-Id: I1b8423f9cef46a425c1ec7665c8285af10c56df6
2024-02-19 11:58:44 +01:00
Manuel Stahl
d1e9f38b14 Fix example.csv
User must be only the name part, not the full MXID as we can only create
local users.

Fixes #406

Change-Id: Ida7b6db28d88417f28b59a02df1f3d7a010aa110
2024-02-19 11:58:44 +01:00
Sebastian
4054249359
Update RegistrationTokens.jsx: Fix resource name (#469)
fixes #468
2024-02-08 17:30:21 +01:00
Manuel Stahl
f240318525 Bump version to 0.9.1
Change-Id: I8436d0ebd48a99f4b2ca2b7b213d94689b440d57
2024-02-08 15:05:26 +01:00
Dirk Klimpel
0852b54a8e
Disable bulkActionButtons for not needed room and user tabs (#466) 2024-02-08 09:31:11 +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
42 changed files with 5480 additions and 4797 deletions

View File

@ -16,6 +16,6 @@ jobs:
with: with:
node-version: "18" node-version: "18"
- name: Install dependencies - name: Install dependencies
run: yarn --frozen-lockfile run: yarn --immutable
- name: Run tests - name: Run tests
run: yarn test run: yarn test

View File

@ -1,51 +1,63 @@
name: Create docker image(s) and push to docker hub name: Create docker image(s) and push to docker hub and ghcr.io
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
on: on:
push: push:
# Sequence of patterns matched against refs/heads # Sequence of patterns matched against refs/heads
# prettier-ignore # prettier-ignore
branches: branches:
# Push events on master branch # Push events on master branch
- master - master
# Sequence of patterns matched against refs/tags # Sequence of patterns matched against refs/tags
tags: tags:
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag - '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
jobs: jobs:
docker: docker:
name: Push Docker image to multiple registries
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-tags: true
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Calculate docker image tag
id: set-tag - name: Login to GHCR
run: | uses: docker/login-action@v3
case "${GITHUB_REF}" in with:
refs/heads/master|refs/heads/main) registry: ghcr.io
tag=latest username: ${{ github.actor }}
;; password: ${{ secrets.GITHUB_TOKEN }}
refs/tags/*)
tag=${GITHUB_REF#refs/tags/} - name: Extract metadata (tags, labels) for Docker
;; id: meta
*) uses: docker/metadata-action@v5
tag=${GITHUB_SHA} with:
;; images: |
esac awesometechnologies/synapse-admin
echo "::set-output name=tag::$tag" ghcr.io/${{ github.repository }}
- name: Build and Push Tag - name: Build and Push Tag
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}" tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@ -11,16 +11,18 @@ jobs:
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-tags: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"
- name: Install and Build 🔧 - name: Install and Build 🔧
run: | run: |
yarn install yarn install --immutable
yarn build yarn build
- name: Deploy 🚀 - name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.3 uses: JamesIves/github-pages-deploy-action@v4.5.0
with: with:
branch: gh-pages branch: gh-pages
folder: build folder: build

View File

@ -14,17 +14,19 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-tags: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"
- run: yarn install - run: yarn install --immutable
- run: yarn build - run: yarn build
- run: | - run: |
version=`git describe --dirty --tags || echo unknown` version=`git describe --dirty --tags || echo unknown`
mkdir -p dist mkdir -p dist
cp -r build synapse-admin-$version cp -r build synapse-admin-$version
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 - uses: softprops/action-gh-release@3198ee18f814cdf787321b4a32a26ddbf37acc52
with: with:
files: dist/*.tar.gz files: dist/*.tar.gz
env: env:

View File

@ -1,6 +0,0 @@
dist: focal
language: node_js
node_js:
- 18
cache: yarn

View File

@ -1,12 +1,12 @@
# Builder # Builder
FROM node:lts as builder FROM node:lts as builder
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
ARG REACT_APP_SERVER ARG REACT_APP_SERVER
WORKDIR /src WORKDIR /src
COPY . /src COPY . /src
RUN yarn --network-timeout=300000 install RUN yarn --network-timeout=300000 install --immutable
RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build

View File

@ -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

View File

@ -1,6 +1,6 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.9.0", "version": "0.9.2",
"description": "Admin GUI for the Matrix.org server Synapse", "description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH", "author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -10,29 +10,30 @@
"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": "^15.0.2",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.5.2",
"eslint": "^8.55.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.1.3",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^2.2.0" "prettier": "^3.2.5"
}, },
"dependencies": { "dependencies": {
"@mui/icons-material": "^5.14.19", "@mui/icons-material": "^5.15.15",
"@mui/material": "^5.14.8", "@mui/material": "^5.15.15",
"@mui/styles": "5.14.10", "@mui/styles": "^5.15.15",
"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.15",
"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", "ra-language-farsi": "^4.2.0",
"react-admin": "^4.16.9", "ra-language-russian": "^4.14.2",
"react-dom": "^17.0.2", "react": "^18.0.0",
"react-admin": "^4.16.15",
"react-dom": "^18.0.0",
"react-scripts": "^5.0.1" "react-scripts": "^5.0.1"
}, },
"scripts": { "scripts": {

View File

@ -1,3 +1,3 @@
id,displayname,password,is_guest,admin,deactivated id,displayname,password,is_guest,admin,deactivated
@testuser22:example.org,Jane Doe,secretpassword,false,true,false testuser22,Jane Doe,secretpassword,false,true,false
,John Doe,,false,false,false ,John Doe,,false,false,false

1 id displayname password is_guest admin deactivated
2 @testuser22:example.org testuser22 Jane Doe secretpassword false true false
3 John Doe false false false

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;

72
src/App.jsx Normal file
View File

@ -0,0 +1,72 @@
import React from "react";
import {
Admin,
CustomRoutes,
Resource,
resolveBrowserLocale,
} from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
import users from "./components/users";
import rooms from "./components/rooms";
import userMediaStats from "./components/statistics";
import reports from "./components/EventReports";
import roomDirectory from "./components/RoomDirectory";
import destinations from "./components/destinations";
import LoginPage from "./components/LoginPage";
import { ImportFeature } from "./components/ImportFeature";
import { Route } from "react-router-dom";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr";
import chineseMessages from "./i18n/zh";
import italianMessages from "./i18n/it";
import russianMessages from "./i18n/ru";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
de: germanMessages,
en: englishMessages,
fr: frenchMessages,
it: italianMessages,
zh: chineseMessages,
ru: russianMessages
};
const i18nProvider = polyglotI18nProvider(
locale => (messages[locale] ? messages[locale] : messages.en),
resolveBrowserLocale()
);
const App = () => (
<Admin
disableTelemetry
requireAuth
loginPage={LoginPage}
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
>
<CustomRoutes>
<Route path="/import_users" element={<ImportFeature />} />
</CustomRoutes>
<Resource {...users} />
<Resource {...rooms} />
<Resource {...userMediaStats} />
<Resource {...reports} />
<Resource {...roomDirectory} />
<Resource {...destinations} />
<Resource name="connections" />
<Resource name="devices" />
<Resource name="room_members" />
<Resource name="users_media" />
<Resource name="joined_rooms" />
<Resource name="pushers" />
<Resource name="servernotices" />
<Resource name="forward_extremities" />
<Resource name="room_state" />
<Resource name="destination_rooms" />
</Admin>
);
export default App;

View File

@ -1,9 +0,0 @@
import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
describe("App", () => {
it("renders", () => {
render(<App />);
});
});

10
src/App.test.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";
describe("App", () => {
it("renders", async () => {
render(<App />);
await screen.findAllByText("Welcome to Synapse-admin");
});
});

View File

@ -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;

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");
});
});

View File

@ -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;

View File

@ -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 " +

View File

@ -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,
@ -22,16 +21,23 @@ import {
CircularProgress, CircularProgress,
MenuItem, MenuItem,
Select, Select,
TextField,
Typography, Typography,
} 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,
getSupportedFeatures,
getSupportedLoginFlows,
getWellKnownUrl,
isValidBaseUrl,
splitMxid,
} from "../synapse/synapse";
const FormBox = styled(Box)(({ theme }) => ({ const FormBox = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
minHeight: "calc(100vh - 1em)", minHeight: "calc(100vh - 1rem)",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)", background: "url(./images/floating-cogs.svg)",
@ -40,12 +46,12 @@ const FormBox = styled(Box)(({ theme }) => ({
backgroundSize: "cover", backgroundSize: "cover",
[`& .card`]: { [`& .card`]: {
minWidth: "30em", width: "30rem",
marginTop: "6em", marginTop: "6rem",
marginBottom: "6em", marginBottom: "6rem",
}, },
[`& .avatar`]: { [`& .avatar`]: {
margin: "1em", margin: "1rem",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
}, },
@ -54,24 +60,31 @@ const FormBox = styled(Box)(({ theme }) => ({
}, },
[`& .hint`]: { [`& .hint`]: {
marginTop: "1em", marginTop: "1em",
marginBottom: "1em",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
color: theme.palette.grey[600], color: theme.palette.grey[600],
}, },
[`& .form`]: { [`& .form`]: {
padding: "0 1em 1em 1em", padding: "0 1rem 1rem 1rem",
}, },
[`& .input`]: { [`& .select`]: {
marginTop: "1em", marginBottom: "2rem",
}, },
[`& .actions`]: { [`& .actions`]: {
padding: "0 1em 1em 1em", padding: "0 1rem 1rem 1rem",
}, },
[`& .serverVersion`]: { [`& .serverVersion`]: {
color: theme.palette.grey[500], color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif", fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginBottom: "1em", marginLeft: "0.5rem",
marginLeft: "0.5em", },
[`& .matrixVersions`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem",
marginBottom: "1rem",
marginLeft: "0.5rem",
}, },
})); }));
@ -113,28 +126,14 @@ 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);
}); });
} }
} }
const renderInput = ({
meta: { touched, error } = {},
input: { ...inputProps },
...props
}) => (
<TextField
error={!!(touched && error)}
helperText={touched && error}
{...inputProps}
{...props}
fullWidth
/>
);
const validateBaseUrl = value => { const validateBaseUrl = value => {
if (!value.match(/^(http|https):\/\//)) { if (!value.match(/^(http|https):\/\//)) {
return translate("synapseadmin.auth.protocol_error"); return translate("synapseadmin.auth.protocol_error");
@ -155,8 +154,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,
{ type: "warning" } { type: "warning" }
); );
}); });
@ -170,87 +169,51 @@ const LoginPage = () => {
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 [matrixVersions, setMatrixVersions] = 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 getSupportedFeatures(formData.base_url)
const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`; .then(features =>
let supportPass = false, setMatrixVersions(
supportSSO = false; `${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
fetchUtils )
.fetchJson(authMethodUrl, { method: "GET" }) )
.then(({ json }) => { .catch(() => setMatrixVersions(""));
json.flows.forEach(f => {
if (f.type === "m.login.password") { // Set SSO Url
supportPass = true; getSupportedLoginFlows(formData.base_url)
} else if (f.type === "m.login.sso") { .then(loginFlows => {
supportSSO = true; const supportPass =
} loginFlows.find(f => f.type === "m.login.password") !== undefined;
}); const supportSSO =
setSupportPassAuth(supportPass); loginFlows.find(f => f.type === "m.login.sso") !== undefined;
if (supportSSO) { setSupportPassAuth(supportPass);
setSSOBaseUrl(formData.base_url); setSSOBaseUrl(supportSSO ? formData.base_url : "");
} else { })
setSSOBaseUrl(""); .catch(() => setSSOBaseUrl(""));
} }, [formData.base_url]);
})
.catch(_ => {
setSSOBaseUrl("");
});
},
[formData.base_url]
);
return ( return (
<> <>
@ -258,7 +221,6 @@ const LoginPage = () => {
<TextInput <TextInput
autoFocus autoFocus
name="username" name="username"
component={renderInput}
label="ra.auth.username" label="ra.auth.username"
disabled={loading || !supportPassAuth} disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange} onBlur={handleUsernameChange}
@ -271,7 +233,6 @@ const LoginPage = () => {
<Box> <Box>
<PasswordInput <PasswordInput
name="password" name="password"
component={renderInput}
label="ra.auth.password" label="ra.auth.password"
type="password" type="password"
disabled={loading || !supportPassAuth} disabled={loading || !supportPassAuth}
@ -284,7 +245,6 @@ const LoginPage = () => {
<Box> <Box>
<TextInput <TextInput
name="base_url" name="base_url"
component={renderInput}
label="synapseadmin.auth.base_url" label="synapseadmin.auth.base_url"
disabled={cfg_base_url || loading} disabled={cfg_base_url || loading}
resettable resettable
@ -294,6 +254,7 @@ const LoginPage = () => {
/> />
</Box> </Box>
<Typography className="serverVersion">{serverVersion}</Typography> <Typography className="serverVersion">{serverVersion}</Typography>
<Typography className="matrixVersions">{matrixVersions}</Typography>
</> </>
); );
}; };
@ -307,9 +268,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">
@ -320,13 +285,15 @@ const LoginPage = () => {
}} }}
fullWidth fullWidth
disabled={loading} disabled={loading}
className="input" className="select"
> >
<MenuItem value="de">Deutsch</MenuItem> <MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>
<MenuItem value="fr">Français</MenuItem> <MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem> <MenuItem value="it">Italiano</MenuItem>
<MenuItem value="zh">简体中文</MenuItem> <MenuItem value="zh">简体中文</MenuItem>
<MenuItem value="fa">Persian(فارسی)</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
</Select> </Select>
<FormDataConsumer> <FormDataConsumer>
{formDataProps => <UserData {...formDataProps} />} {formDataProps => <UserData {...formDataProps} />}
@ -339,7 +306,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 +315,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>

View File

@ -6,18 +6,19 @@ import {
DateField, DateField,
DateTimeInput, DateTimeInput,
Edit, Edit,
Filter,
List, List,
maxValue, maxValue,
number, number,
NumberField, NumberField,
NumberInput, NumberInput,
regex, regex,
SaveButton,
SimpleForm, SimpleForm,
TextInput, TextInput,
TextField, TextField,
Toolbar, Toolbar,
} from "react-admin"; } from "react-admin";
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
const date_format = { const date_format = {
year: "numeric", year: "numeric",
@ -53,40 +54,41 @@ const dateFormatter = v => {
return `${year}-${month}-${day}T${hour}:${minute}`; return `${year}-${month}-${day}T${hour}:${minute}`;
}; };
const RegistrationTokenFilter = props => ( const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
<Filter {...props}>
<BooleanInput source="valid" alwaysOn /> export const RegistrationTokenList = props => (
</Filter> <List
{...props}
filters={registrationTokenFilters}
filterDefaultValues={{ valid: true }}
pagination={false}
perPage={500}
>
<Datagrid rowClick="edit">
<TextField source="token" sortable={false} />
<NumberField source="uses_allowed" sortable={false} />
<NumberField source="pending" sortable={false} />
<NumberField source="completed" sortable={false} />
<DateField
source="expiry_time"
showTime
options={date_format}
sortable={false}
/>
</Datagrid>
</List>
); );
export const RegistrationTokenList = props => {
return (
<List
{...props}
filters={<RegistrationTokenFilter />}
filterDefaultValues={{ valid: true }}
pagination={false}
perPage={500}
>
<Datagrid rowClick="edit">
<TextField source="token" sortable={false} />
<NumberField source="uses_allowed" sortable={false} />
<NumberField source="pending" sortable={false} />
<NumberField source="completed" sortable={false} />
<DateField
source="expiry_time"
showTime
options={date_format}
sortable={false}
/>
</Datagrid>
</List>
);
};
export const RegistrationTokenCreate = props => ( export const RegistrationTokenCreate = props => (
<Create {...props}> <Create {...props} redirect="list">
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}> <SimpleForm
toolbar={
<Toolbar>
{/* It is possible to create tokens per default without input. */}
<SaveButton alwaysEnable />
</Toolbar>
}
>
<TextInput <TextInput
source="token" source="token"
autoComplete="off" autoComplete="off"
@ -109,24 +111,32 @@ export const RegistrationTokenCreate = props => (
</Create> </Create>
); );
export const RegistrationTokenEdit = props => { export const RegistrationTokenEdit = props => (
return ( <Edit {...props}>
<Edit {...props}> <SimpleForm>
<SimpleForm> <TextInput source="token" disabled />
<TextInput source="token" disabled /> <NumberInput source="pending" disabled />
<NumberInput source="pending" disabled /> <NumberInput source="completed" disabled />
<NumberInput source="completed" disabled /> <NumberInput
<NumberInput source="uses_allowed"
source="uses_allowed" validate={validateUsesAllowed}
validate={validateUsesAllowed} step={1}
step={1} />
/> <DateTimeInput
<DateTimeInput source="expiry_time"
source="expiry_time" parse={dateParser}
parse={dateParser} format={dateFormatter}
format={dateFormatter} />
/> </SimpleForm>
</SimpleForm> </Edit>
</Edit> );
);
const resource = {
name: "registration_tokens",
icon: RegistrationTokenIcon,
list: RegistrationTokenList,
edit: RegistrationTokenEdit,
create: RegistrationTokenCreate,
}; };
export default resource;

View File

@ -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;

View File

@ -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> </>
); );
}; };

View File

@ -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;

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>
);
};

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,
}}
/>
);
};

View File

@ -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> </>
); );
}; };

View File

@ -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" />
@ -138,6 +134,7 @@ export const RoomShow = props => {
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, resource, record) => "/users/" + id} rowClick={(id, resource, record) => "/users/" + id}
bulkActionButtons={false}
> >
<TextField <TextField
source="id" source="id"
@ -222,7 +219,7 @@ export const RoomShow = props => {
target="room_id" target="room_id"
addLabel={false} addLabel={false}
> >
<Datagrid style={{ width: "100%" }}> <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="type" sortable={false} /> <TextField source="type" sortable={false} />
<DateField <DateField
source="origin_server_ts" source="origin_server_ts"
@ -260,7 +257,7 @@ export const RoomShow = props => {
target="room_id" target="room_id"
addLabel={false} addLabel={false}
> >
<Datagrid style={{ width: "100%" }}> <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="id" sortable={false} /> <TextField source="id" sortable={false} />
<DateField <DateField
source="received_ts" source="received_ts"
@ -279,22 +276,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 +296,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 +344,12 @@ export const RoomList = () => {
</List> </List>
); );
}; };
const resource = {
name: "rooms",
icon: RoomIcon,
list: RoomList,
show: RoomShow,
};
export default resource;

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>
);
};

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;

View File

@ -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 (
@ -422,7 +417,7 @@ export const UserEdit = props => {
source="devices[].sessions[0].connections" source="devices[].sessions[0].connections"
label="resources.connections.name" label="resources.connections.name"
> >
<Datagrid style={{ width: "100%" }}> <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="ip" sortable={false} /> <TextField source="ip" sortable={false} />
<DateField <DateField
source="last_seen" source="last_seen"
@ -485,6 +480,7 @@ export const UserEdit = props => {
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, resource, record) => "/rooms/" + id + "/show"} rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
bulkActionButtons={false}
> >
<TextField <TextField
source="id" source="id"
@ -514,7 +510,7 @@ export const UserEdit = props => {
target="user_id" target="user_id"
addLabel={false} addLabel={false}
> >
<Datagrid style={{ width: "100%" }}> <Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="kind" sortable={false} /> <TextField source="kind" sortable={false} />
<TextField source="app_display_name" sortable={false} /> <TextField source="app_display_name" sortable={false} />
<TextField source="app_id" sortable={false} /> <TextField source="app_id" sortable={false} />
@ -530,3 +526,13 @@ export const UserEdit = props => {
</Edit> </Edit>
); );
}; };
const resource = {
name: "users",
icon: UserIcon,
list: UserList,
edit: UserEdit,
create: UserCreate,
};
export default resource;

View File

@ -7,6 +7,7 @@ const de = {
base_url: "Heimserver URL", base_url: "Heimserver URL",
welcome: "Willkommen bei Synapse-admin", welcome: "Willkommen bei Synapse-admin",
server_version: "Synapse Version", server_version: "Synapse Version",
supports_specs: "unterstützt Matrix-Specs",
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'", username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen", protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
url_error: "Keine gültige Matrix Server URL", url_error: "Keine gültige Matrix Server URL",
@ -188,7 +189,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 +211,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",

View File

@ -7,6 +7,7 @@ const en = {
base_url: "Homeserver URL", base_url: "Homeserver URL",
welcome: "Welcome to Synapse-admin", welcome: "Welcome to Synapse-admin",
server_version: "Synapse version", server_version: "Synapse version",
supports_specs: "supports Matrix specs",
username_error: "Please enter fully qualified user ID: '@user:domain'", username_error: "Please enter fully qualified user ID: '@user:domain'",
protocol_error: "URL has to start with 'http://' or 'https://'", protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL", url_error: "Not a valid Matrix server URL",
@ -207,6 +208,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",

382
src/i18n/fa.js Normal file
View File

@ -0,0 +1,382 @@
import farsiMessages from "ra-language-farsi";
const fa = {
...farsiMessages,
synapseadmin: {
auth: {
base_url: "آدرس سرور",
welcome: "به پنل مدیریت سیناپس خوش آمدید!",
server_version: "نسخه",
username_error: "لطفاً شناسه کاربر را وارد کنید: '@user:domain'",
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
url_error: "آدرس وارد شده یک سرور معتبر نیست",
sso_sign_in: "با SSO وارد شوید",
},
users: {
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "اصلی",
members: "اعضا",
detail: "جزئیات",
permission: "مجوزها",
},
},
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
},
import_users: {
error: {
at_entry: "در هنگام ورود %{entry}: %{message}",
error: "Error",
required_field: "فیلد الزامی '%{field}' وجود ندارد",
invalid_value:
"خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
unreasonably_big:
"از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
already_in_progress: "یک بارگذاری از قبل در حال انجام است",
id_exits: "شناسه %{id} موجود است",
},
title: "کاربران را از طریق فایل CSV وارد کنید",
goToPdf: "رفتن به PDF",
cards: {
importstats: {
header: "وارد کردن کاربران",
users_total:
"%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
},
conflicts: {
header: "استراتژی متغارض",
mode: {
stop: "توقف",
skip: "نمایش خطا و رد شدن",
},
},
ids: {
header: "شناسنامه ها",
all_ids_present: "شناسه های موجود در هر ورودی",
count_ids_present:
"%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
mode: {
ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
update: "سوابق موجود را به روز کنید",
},
},
passwords: {
header: "رمز عبور",
all_passwords_present: "رمزهای عبور موجود در هر ورودی",
count_passwords_present:
"%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
use_passwords: "از پسوردهای CSV استفاده کنید",
},
upload: {
header: "Input CSV file",
explanation:
"در اینجا می توانید فایلی را با مقادیر جدا شده با کاما بارگذاری کنید که برای ایجاد یا به روز رسانی کاربران پردازش می شود. فایل باید شامل فیلدهای 'id' و 'displayname' باشد. می توانید یک فایل نمونه را از اینجا دانلود و تطبیق دهید: ",
},
startImport: {
simulate_only: "فقط شبیه سازی",
run_import: "بارگذاری",
},
results: {
header: "بارگذاری نتایج",
total: "%{smart_count} ورودی در کل |||| %{smart_count} ورودی ها در کل",
successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
download_skipped: "دانلود رکوردهای نادیده گرفته شده",
with_error:
"%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
simulated_only: "اجرا فقط شبیه سازی شد",
},
},
},
resources: {
users: {
name: "کاربر |||| کاربران",
email: "ایمیل",
msisdn: "شماره تلفن",
threepid: "ایمیل / شماره تلفن",
fields: {
avatar: "آواتار",
id: "شناسه کاربر",
name: "نام",
is_guest: "مهمان",
admin: "مدیر سرور",
deactivated: "غیرفعال",
guests: "نمایش مهمانان",
show_deactivated: "نمایش کاربران غیرفعال شده",
user_id: "جستجوی کاربر",
displayname: "نام نمایشی",
password: "رمز عبور",
avatar_url: "آواتار سرور",
avatar_src: "آواتار",
medium: "متوسط",
threepids: "سرویس احراز هویت",
address: "آدرس",
creation_ts_ms: "ساخته شده در",
consent_version: "Consent نسخه",
auth_provider: "ارائه دهنده",
user_type: "نوع کاربر",
},
helper: {
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
},
action: {
erase: "پاک کردن اطلاعات کاربر",
},
},
rooms: {
name: "اتاق |||| اتاق ها",
fields: {
room_id: "شناسه اتاق",
name: "نام",
canonical_alias: "نام مستعار",
joined_members: "اعضا",
joined_local_members: "اعضای محلی",
joined_local_devices: "دستگاه های محلی",
state_events: "رویدادهای حالت / پیچیدگی",
version: "نسخه",
is_encrypted: "رمزگذاری شده است",
encryption: "رمزگذاری",
federatable: "Federatable",
public: "قابل مشاهده در فهرست اتاق",
creator: "سازنده",
join_rules: "به قوانین بپیوندید",
guest_access: "دسترسی مهمان",
history_visibility: "مشاهده تاریخچه",
topic: "موضوع",
avatar: "آواتار",
},
helper: {
forward_extremities:
"اندام های رو به جلو، رویدادهای برگ در انتهای نمودار غیر چرخه ای جهت دار (DAG) در یک اتاق هستند، رویدادهایی که فرزند ندارند. هر چه تعداد بیشتری در یک اتاق وجود داشته باشد، وضوح حالت بیشتری را که سیناپس باید انجام دهد (نکته: این یک عملیات گران است). در حالی که Synapse کدی برای جلوگیری از وجود تعداد زیادی از این موارد در یک زمان در اتاق دارد، گاهی اوقات باگ‌ها می‌توانند دوباره ظاهر شوند. اگر اتاقی بیش از 10 انتهای رو به جلو دارد، بهتر است بررسی کنید که کدام اتاق مقصر است و احتمالاً آنها را با استفاده از جستارهای SQL ذکر شده در آن حذف کنید. #1760.",
},
enums: {
join_rules: {
public: "عمومی",
knock: "در زدن",
invite: "دعوت کردن",
private: "خصوصی",
},
guest_access: {
can_join: "مهمانان می توانند ملحق شوند",
forbidden: "مهمانان نمی توانند ملحق شوند",
},
history_visibility: {
invited: "از آنجایی که دعوت شده است",
joined: "از زمانی که پیوست",
shared: "از آنجایی که به اشتراک گذاشته شده است",
world_readable: "هر کسی",
},
unencrypted: "رمزگذاری نشده",
},
action: {
erase: {
title: "حذف اتاق",
content:
"آیا مطمئن هستید که می خواهید اتاق را حذف کنید؟ این قابل بازگشت نیست. همه پیام ها و رسانه های مشترک در اتاق از سرور حذف می شوند!",
},
},
},
reports: {
name: "رویداد گزارش شده |||| رویدادهای گزارش شده",
fields: {
id: "شناسه",
received_ts: "زمان گزارش",
user_id: "گوینده",
name: "نام اتاق",
score: "نمره",
reason: "دلیل",
event_id: "شناسه رویداد",
event_json: {
origin: "سرور مبدا",
origin_server_ts: "زمان ارسال",
type: "نوع رویداد",
content: {
msgtype: "نوع محتوا",
body: "محتوا",
format: "قالب",
formatted_body: "محتوای قالب بندی شده",
algorithm: "الگوریتم",
},
},
},
},
connections: {
name: "اتصالات",
fields: {
last_seen: "تاریخ",
ip: "آدرس آی پی",
user_agent: "نماینده کاربر",
},
},
devices: {
name: "دستگاه |||| دستگاه ها",
fields: {
device_id: "شناسه دستگاه",
display_name: "نام دستگاه",
last_seen_ts: "مهر زمان",
last_seen_ip: "آدرس آی پی",
},
action: {
erase: {
title: "حذف کردن %{id}",
content:
'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
success: "دستگاه با موفقیت حذف شد.",
failure: "خطایی رخ داده است.",
},
},
},
users_media: {
name: "رسانه ها",
fields: {
media_id: "شناسه رسانه",
media_length: "اندازه فایل (به بایت)",
media_type: "نوع",
upload_name: "نام فایل",
quarantined_by: "قرنطینه شده توسط",
safe_from_quarantine: "امان از قرنطینه",
created_ts: "ایجاد شده",
last_access_ts: "آخرین دسترسی",
},
},
delete_media: {
name: "رسانه ها",
fields: {
before_ts: "آخرین دسترسی قبل",
size_gt: "بزرگتر از آن (به بایت)",
keep_profiles: "تصاویر پروفایل را نگه دارید",
},
action: {
send: "حذف رسانه ها",
send_success: "درخواست با موفقیت ارسال شد.",
send_failure: "خطایی رخ داده است.",
},
helper: {
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانه‌هایی که در مخازن رسانه خارجی آپلود شده‌اند تأثیری نخواهد گذاشت.",
},
},
protect_media: {
action: {
create: "محافظت نشده، حفاظت ایجاد کنید",
delete: "محافظت شده، حفاظت را بردارید",
none: "در قرنطینه",
send_success: "وضعیت حفاظت با موفقیت تغییر کرد.",
send_failure: "خطایی رخ داده است.",
},
},
quarantine_media: {
action: {
name: "قرنطینه",
create: "به قرنطینه اضافه کنید",
delete: "در قرنطینه، غیر قرنطینه",
none: "از قرنطینه محافظت می شود",
send_success: "وضعیت قرنطینه با موفقیت تغییر کرد.",
send_failure: "خطایی رخ داده است.",
},
},
pushers: {
name: "هل دهنده |||| هل دهنده ها",
fields: {
app: "برنامه",
app_display_name: "نام نمایش برنامه",
app_id: "شناسه برنامه",
device_display_name: "نام نمایشی برنامه",
kind: "نوع",
lang: "زبان",
profile_tag: "برچسب پروفایل",
pushkey: "کلید",
data: { url: "URL" },
},
},
servernotices: {
name: "اطلاعیه های سرور",
send: "ارسال اعلانات سرور",
fields: {
body: "پیام",
},
action: {
send: "ارسال یادداشت",
send_success: "اعلان سرور با موفقیت ارسال شد.",
send_failure: "خطایی رخ داده است.",
},
helper: {
send: "اعلان سرور را برای کاربران انتخاب شده ارسال می کند. ویژگی 'اعلامیه های سرور' باید در سرور فعال شود.",
},
},
user_media_statistics: {
name: "رسانه کاربران",
fields: {
media_count: "شمارش رسانه ها",
media_length: "طول رسانه",
},
},
forward_extremities: {
name: "Forward Extremities",
fields: {
id: "شناسه رویداد",
received_ts: "مهر زمان",
depth: "عمق",
state_group: "گروه دولتی",
},
},
room_state: {
name: "رویدادهای وضعیت",
fields: {
type: "نوع",
content: "محتوا",
origin_server_ts: "زمان ارسال",
sender: "فرستنده",
},
},
room_directory: {
name: "راهنمای اتاق",
fields: {
world_readable: "کاربران مهمان می توانند بدون عضویت مشاهده کنند",
guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
},
action: {
title:
"اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
content:
"آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{smart_count} از راهنمای اتاق ها حذف کنید؟",
erase: "حذف از فهرست اتاق",
create: "انتشار در راهنما اتاق",
send_success: "اتاق با موفقیت منتشر شد.",
send_failure: "خطایی رخ داده است.",
},
},
destinations: {
name: "سرور های مرتبط",
fields: {
destination: "آدرس",
failure_ts: "زمان شکست",
retry_last_ts: "آخرین زمان اتصال",
retry_interval: "بازه امتحان مجدد",
last_successful_stream_ordering: "آخرین جریان موفق",
stream_ordering: "جریان",
},
action: { reconnect: "دوباره وصل شوید" },
},
},
registration_tokens: {
name: "توکن های ثبت نام",
fields: {
token: "توکن",
valid: "توکن معتبر",
uses_allowed: "موارد استفاده مجاز",
pending: "انتظار",
completed: "تکمیل شد",
expiry_time: "زمان انقضا",
length: "طول",
},
helper: { length: "طول توکن در صورت عدم ارائه توکن." },
},
};
export default fa;

390
src/i18n/ru.js Normal file
View File

@ -0,0 +1,390 @@
import russianMessages from "ra-language-russian";
const ru = {
...russianMessages,
synapseadmin: {
auth: {
base_url: "Домашняя страница",
welcome: "Добро пожаловать в Synapse-admin",
server_version: "Версия Synapse",
supports_specs: "поддерживает спецификации Matrix",
username_error: "Введите полный идентификатор пользователя: '@user:domain'",
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
url_error: "Некорректный сервер Matrix",
sso_sign_in: "Присоединиться с помощью SSO",
},
users: {
invalid_user_id: "Локальная часть идентификатора пользователя Matrix без домашнего сервера.",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "Основное",
members: "Участники",
detail: "Подробности",
permission: "Права",
},
},
reports: { tabs: { basic: "Основное", detail: "Подробности" } },
},
import_users: {
error: {
at_entry: "При входе %{entry}: %{message}",
error: "Ошибка",
required_field: "Обязательное поле '%{field}' не представленно",
invalid_value:
"Недопустимое значение в строке %{row}. '%{field}' поле может быть только 'true' или 'false'",
unreasonably_big:
"Отказался загружать неоправданно большой файл %{size} Мбайт",
already_in_progress: "Импорт уже запущен",
id_exits: "Идентификатор %{id} уже существует",
},
title: "Импорт пользователей из CSV",
goToPdf: "В PDF",
cards: {
importstats: {
header: "Импорт пользователей",
users_total:
"%{smart_count} пользователь в CSV файл |||| %{smart_count} пользователей в CSV файл",
guest_count: "%{smart_count} гость |||| %{smart_count} гостей",
admin_count: "%{smart_count} администратор |||| %{smart_count} администраторов",
},
conflicts: {
header: "Решение конфликтов",
mode: {
stop: "Остановиться, если конфликт произошел",
skip: "Вывести ошибку и пропустить конфликт",
},
},
ids: {
header: "Идентификаторы",
all_ids_present: "Идентификаторы представлены для каждой записи",
count_ids_present:
"%{smart_count} запись с идентификатором |||| %{smart_count} записей с идентификатором",
mode: {
ignore: "Игнорировать идентификаторы в CSV и создавать новые",
update: "Обновлять существующие записи",
},
},
passwords: {
header: "Пароли",
all_passwords_present: "Пароли представлены для каждой записи",
count_passwords_present:
"%{smart_count} запись с паролем |||| %{smart_count} записей с паролем",
use_passwords: "Использовать пароли из CSV",
},
upload: {
header: "Загрузка CSV файла",
explanation:
"Здесь вы можете загрузить файл со значениями, разделенными запятыми, который будет обработан для создания или обновления пользователей. Файл должен содержать поля 'id' и 'displayname'. Вы можете загрузить пример здесь:",
},
startImport: {
simulate_only: "Не выполнять реальных действий",
run_import: "Импорт",
},
results: {
header: "Импорт результатов",
total:
"Всего %{smart_count} запись |||| Всего %{smart_count} записей",
successful: "%{smart_count} записей успешно импортировано",
skipped: "%{smart_count} записей пропущено",
download_skipped: "Загрузить пропущенные записи",
with_error:
"%{smart_count} запись с ошибками ||| %{smart_count} записей с ошибками",
simulated_only: "Результат не будет сохранен",
},
},
},
resources: {
users: {
name: "Пользователей |||| Пользователи",
email: "Почта",
msisdn: "Телефон",
threepid: "Почта / Телефон",
fields: {
avatar: "Аватар",
id: "Идентификатор пользователя",
name: "Имя",
is_guest: "Гость",
admin: "Администратор",
deactivated: "Деактивирован",
guests: "Показать гостей",
show_deactivated: "Показать деактивированных пользователей",
user_id: "Найти пользователя",
displayname: "Отображаемое имя",
password: "Пароль",
avatar_url: "Ссылка на аватар",
avatar_src: "Аватар",
medium: "Тип",
threepids: "Иной идентификатор",
address: "Адрес",
creation_ts_ms: "Время создания",
consent_version: "Версия соглашения",
auth_provider: "Поставщик",
user_type: "Тип пользователя",
},
helper: {
password: "Изменение пароля приведет к выходу пользователя из всех сеансов.",
deactivate: "Вы должны ввести пароль для повторной активации учетной записи.",
erase: "Пометить пользователя как удаленного в связи с защитой персональных данных",
},
action: {
erase: "Удаление пользовательских данных",
},
},
rooms: {
name: "Комнат |||| Комнаты",
fields: {
room_id: "Идентификатор комнаты",
name: "Название",
canonical_alias: "Псевдоним",
joined_members: "Участники",
joined_local_members: "Внутренние участники",
joined_local_devices: "Используемые устройства",
state_events: "События изменения состояния",
version: "Версия",
is_encrypted: "Зашифрованно",
encryption: "Шифрование",
federatable: "Федерация",
public: "Видимость в списке комнат",
creator: "Создатель",
join_rules: "Правила присоединения",
guest_access: "Гостевой доступ",
history_visibility: "Видимость истории",
topic: "Тема",
avatar: "Аватар",
},
helper: {
forward_extremities:
"Перенаправленные заключения, это такие события, которые не имеют потомков в рамках графа, отвечающего за их репрезентацию. Чем больше пользователей находится в комнате, тем больше операций требуется выполнить Synapse для разрешения коллиций, которые возникают при их проверке наступления событий (это дорогостоящая операция). Хотя в Synapse есть код, предотвращающий одновременное присутствие слишком большого количества таких объектов в комнате, ошибки иногда могут привести к их повторному появлению. Если в комнате содержится >10 перенаправленных заключений, имеет смысл выяснить, какая комната стала причиной их появления и, возможно, удалить их, используя SQL-запросы, упомянутые issue #1760.",
},
enums: {
join_rules: {
public: "Публичный",
knock: "По запросу",
invite: "По приглашению",
private: "Приватный",
},
guest_access: {
can_join: "Гости могут присоединиться",
forbidden: "Гости не могут присоединиться",
},
history_visibility: {
invited: "С момента приглашения",
joined: "С момента присоединения",
shared: "С момента разрешения",
world_readable: "Всегда",
},
unencrypted: "Не зашифрованно",
},
action: {
erase: {
title: "Удалить комнату",
content:
"Вы уверены, что хотите удалить комнату? Это невозможно отменить. Все сообщения и медиафайлы, находящиеся в общем доступе в комнате, будут удалены с сервера!",
},
},
},
reports: {
name: "Жалоб |||| Жалобы",
fields: {
id: "идентификатор",
received_ts: "время жалобы",
user_id: "коментатор",
name: "название комнаты",
score: "оценка",
reason: "причина",
event_id: "идентификатор события",
event_json: {
origin: "сервер",
origin_server_ts: "время отправки",
type: "тип события",
content: {
msgtype: "тип содержания",
body: "содержание",
format: "формат",
formatted_body: "форматированное содержание",
algorithm: "алгоритм",
},
},
},
action: {
erase: {
title: "Удалить жалобу",
content:
"Вы уверены, что хотите удалить жалобу? Это невозможно отменить.",
},
},
},
connections: {
name: "Подключения",
fields: {
last_seen: "Дата",
ip: "IP адрес",
user_agent: "User agent",
},
},
devices: {
name: "Устройств |||| Устройства",
fields: {
device_id: "Идентификатор устройства",
display_name: "Название устройства",
last_seen_ts: "Метка времени",
last_seen_ip: "IP адрес",
},
action: {
erase: {
title: "Удаление %{id}",
content: 'Вы уверены, что хотите удалить устройство "%{name}"?',
success: "Устройство успешно удалено.",
failure: "Произошла ошибка.",
},
},
},
users_media: {
name: "Медиа",
fields: {
media_id: "Идентификатор медиа",
media_length: "Размер файла (в байтах)",
media_type: "Тип",
upload_name: "Имя файла",
quarantined_by: "Отправлен в карантин",
safe_from_quarantine: "Защищен от карантина",
created_ts: "Создан",
last_access_ts: "Последнее обращение",
},
},
delete_media: {
name: "Медиа",
fields: {
before_ts: "последнее обращение",
size_gt: "Больше чем (в байтах)",
keep_profiles: "Сохранить аватары",
},
action: {
send: "Удалить медиафайлы",
send_success: "Запрос отправлен.",
send_failure: "Произошла ошибка.",
},
helper: {
send: "Этот метод удаляет медиафайлы с диска сервера. Это включает в себя любые миниатюры и копии загруженных файлов. Этот метод не повлияет на файлы, загруженные во внешние хранилища.",
},
},
protect_media: {
action: {
create: "Не защищено, установить защиту",
delete: "Защищено, удалить защиту",
none: "В карантине",
send_success: "Успешно изменен статус защиты.",
send_failure: "Произошла ошибка.",
},
},
quarantine_media: {
action: {
name: "Карантин",
create: "Добавить в карантин",
delete: "В карантине, вывести из карантина",
none: "Защищено от карантина",
send_success: "Успешно изменен статус карантина.",
send_failure: "Произошла ошибка.",
},
},
pushers: {
name: "Уведомление |||| Уведомления",
fields: {
app: "Приложение",
app_display_name: "Отображаемое имя приложения",
app_id: "Идентификатор приложения",
device_display_name: "Отображаемое имя устройства",
kind: "Тип",
lang: "Язык",
profile_tag: "Тег профиля",
pushkey: "Токен доступа",
data: { url: "URL" },
},
},
servernotices: {
name: "Уведомления",
send: "Отправить уведомление",
fields: {
body: "Сообщение",
},
action: {
send: "Отправить",
send_success: "Уведомление успешно отправлено.",
send_failure: "Произошла ошибка.",
},
helper: {
send: 'Отправляет уведомление выбранным пользователям. Функция "Server Notices" должна быть активирована на сервере.',
},
},
user_media_statistics: {
name: "Медиа",
fields: {
media_count: "Количество медиафайлов",
media_length: "Длина медиафайлов",
},
},
forward_extremities: {
name: "Перенаправленные заключения",
fields: {
id: "Идентификатор события",
received_ts: "Время",
depth: "Глубина",
state_group: "Группа состояния",
},
},
room_state: {
name: "События изменения состояния",
fields: {
type: "Тип",
content: "Содержание",
origin_server_ts: "время отправления",
sender: "Отправитель",
},
},
room_directory: {
name: "Публичных комнат |||| Публичные комнаты",
fields: {
world_readable: "Видимо для гостей",
guest_can_join: "Гости могут присоединиться",
},
action: {
title:
"Удалить комнату |||| Удалить %{smart_count} комнат",
content:
"Вы уверены, что хотите удалить комнату? |||| Вы уверены, что хотите удалить %{smart_count} комнат?",
erase: "Удалить комнату",
create: "Опубликовать комнату",
send_success: "Комната успешно опубликована.",
send_failure: "Произошла ошибка.",
},
},
destinations: {
name: "Федераций |||| Федерация",
fields: {
destination: "Назначение",
failure_ts: "Время сбоя",
retry_last_ts: "Время последней попытки",
retry_interval: "Интервал повторения",
last_successful_stream_ordering: "Последнее успешное соединение",
stream_ordering: "Соединение",
},
action: { reconnect: "Переподключиться" },
},
},
registration_tokens: {
name: "Токены регистрации",
fields: {
token: "Токен",
valid: "Допустимый токен",
uses_allowed: "Разрешено",
pending: "Ожидается",
completed: "Использован",
expiry_time: "Время истечения срока действия",
length: "Длина",
},
helper: { length: "Длина токена, если токен не указан." },
},
};
export default ru;

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"));

9
src/index.jsx Normal file
View File

@ -0,0 +1,9 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -2,7 +2,7 @@ import { fetchUtils } from "react-admin";
const authProvider = { const authProvider = {
// called when the user attempts to log in // called when the user attempts to log in
login: ({ base_url, username, password, loginToken }) => { login: async ({ base_url, username, password, loginToken }) => {
// force homeserver for protection in case the form is manipulated // force homeserver for protection in case the form is manipulated
base_url = process.env.REACT_APP_SERVER || base_url; base_url = process.env.REACT_APP_SERVER || base_url;
@ -38,15 +38,14 @@ const authProvider = {
const decoded_base_url = window.decodeURIComponent(base_url); const decoded_base_url = window.decodeURIComponent(base_url);
const login_api_url = decoded_base_url + "/_matrix/client/r0/login"; const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => { const { json } = await fetchUtils.fetchJson(login_api_url, options);
localStorage.setItem("home_server", json.home_server); localStorage.setItem("home_server", json.home_server);
localStorage.setItem("user_id", json.user_id); localStorage.setItem("user_id", json.user_id);
localStorage.setItem("access_token", json.access_token); localStorage.setItem("access_token", json.access_token);
localStorage.setItem("device_id", json.device_id); localStorage.setItem("device_id", json.device_id);
});
}, },
// called when the user clicks on the logout button // called when the user clicks on the logout button
logout: () => { logout: async () => {
console.log("logout"); console.log("logout");
const logout_api_url = const logout_api_url =
@ -62,11 +61,9 @@ const authProvider = {
}; };
if (typeof access_token === "string") { if (typeof access_token === "string") {
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => { await fetchUtils.fetchJson(logout_api_url, options);
localStorage.removeItem("access_token"); localStorage.removeItem("access_token");
});
} }
return Promise.resolve();
}, },
// called when the API returns an error // called when the API returns an error
checkError: ({ status }) => { checkError: ({ status }) => {

View File

@ -0,0 +1,135 @@
import authProvider from "./authProvider";
describe("authProvider", () => {
beforeEach(() => {
fetch.resetMocks();
localStorage.clear();
});
describe("login", () => {
it("should successfully login with username and password", async () => {
fetch.once(
JSON.stringify({
home_server: "example.com",
user_id: "@user:example.com",
access_token: "foobar",
device_id: "some_device",
})
);
const ret = await authProvider.login({
base_url: "http://example.com",
username: "@user:example.com",
password: "secret",
});
expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith(
"http://example.com/_matrix/client/r0/login",
{
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
headers: new Headers({
Accept: ["application/json"],
"Content-Type": ["application/json"],
}),
method: "POST",
}
);
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
expect(localStorage.getItem("device_id")).toEqual("some_device");
});
});
it("should successfully login with token", async () => {
fetch.once(
JSON.stringify({
home_server: "example.com",
user_id: "@user:example.com",
access_token: "foobar",
device_id: "some_device",
})
);
const ret = await authProvider.login({
base_url: "https://example.com/",
loginToken: "login_token",
});
expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith(
"https://example.com/_matrix/client/r0/login",
{
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({
Accept: ["application/json"],
"Content-Type": ["application/json"],
}),
method: "POST",
}
);
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
expect(localStorage.getItem("device_id")).toEqual("some_device");
});
describe("logout", () => {
it("should remove the access_token from localStorage", async () => {
localStorage.setItem("base_url", "example.com");
localStorage.setItem("access_token", "foo");
fetch.mockResponse(JSON.stringify({}));
await authProvider.logout();
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
headers: new Headers({
Accept: ["application/json"],
Authorization: ["Bearer foo"],
}),
method: "POST",
user: { authenticated: true, token: "Bearer foo" },
});
expect(localStorage.getItem("access_token")).toBeNull();
});
});
describe("checkError", () => {
it("should resolve if error.status is not 401 or 403", async () => {
await expect(
authProvider.checkError({ status: 200 })
).resolves.toBeUndefined();
});
it("should reject if error.status is 401", async () => {
await expect(
authProvider.checkError({ status: 401 })
).rejects.toBeUndefined();
});
it("should reject if error.status is 403", async () => {
await expect(
authProvider.checkError({ status: 403 })
).rejects.toBeUndefined();
});
});
describe("checkAuth", () => {
it("should reject when not logged in", async () => {
await expect(authProvider.checkAuth({})).rejects.toBeUndefined();
});
it("should resolve when logged in", async () => {
localStorage.setItem("access_token", "foobar");
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
});
});
describe("getPermissions", () => {
it("should do nothing", async () => {
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
});
});
});

View File

@ -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",
}), }),
}, },
@ -348,7 +348,7 @@ function getSearchOrder(order) {
} }
const dataProvider = { const dataProvider = {
getList: (resource, params) => { getList: async (resource, params) => {
console.log("getList " + resource); console.log("getList " + resource);
const { const {
user_id, user_id,
@ -383,13 +383,14 @@ const dataProvider = {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const url = `${endpoint_url}?${stringify(query)}`; const url = `${endpoint_url}?${stringify(query)}`;
return jsonClient(url).then(({ json }) => ({ const { json } = await jsonClient(url);
return {
data: json[res.data].map(res.map), data: json[res.data].map(res.map),
total: res.total(json, from, perPage), total: res.total(json, from, perPage),
})); };
}, },
getOne: (resource, params) => { getOne: async (resource, params) => {
console.log("getOne " + resource); console.log("getOne " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -397,14 +398,13 @@ 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.id)}`).then( const { json } = await jsonClient(
({ json }) => ({ `${endpoint_url}/${encodeURIComponent(params.id)}`
data: res.map(json),
})
); );
return { data: res.map(json) };
}, },
getMany: (resource, params) => { getMany: async (resource, params) => {
console.log("getMany " + resource); console.log("getMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -412,17 +412,18 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return Promise.all( const responses = await Promise.all(
params.ids.map(id => params.ids.map(id =>
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`) jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
) )
).then(responses => ({ );
return {
data: responses.map(({ json }) => res.map(json)), data: responses.map(({ json }) => res.map(json)),
total: responses.length, total: responses.length,
})); };
}, },
getManyReference: (resource, params) => { getManyReference: async (resource, params) => {
console.log("getManyReference " + resource); console.log("getManyReference " + resource);
const { page, perPage } = params.pagination; const { page, perPage } = params.pagination;
const { field, order } = params.sort; const { field, order } = params.sort;
@ -442,13 +443,14 @@ const dataProvider = {
const ref = res["reference"](params.id); const ref = res["reference"](params.id);
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`; const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
return jsonClient(endpoint_url).then(({ headers, json }) => ({ const { json } = await jsonClient(endpoint_url);
return {
data: json[res.data].map(res.map), data: json[res.data].map(res.map),
total: res.total(json, from, perPage), total: res.total(json, from, perPage),
})); };
}, },
update: (resource, params) => { update: async (resource, params) => {
console.log("update " + resource); console.log("update " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -456,15 +458,17 @@ 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)}`, { const { json } = await jsonClient(
method: "PUT", `${endpoint_url}/${encodeURIComponent(params.id)}`,
body: JSON.stringify(params.data, filterNullValues), {
}).then(({ json }) => ({ method: "PUT",
data: res.map(json), body: JSON.stringify(params.data, filterNullValues),
})); }
);
return { data: res.map(json) };
}, },
updateMany: (resource, params) => { updateMany: async (resource, params) => {
console.log("updateMany " + resource); console.log("updateMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -472,7 +476,7 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return Promise.all( const responses = await Promise.all(
params.ids.map( params.ids.map(
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`), id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
{ {
@ -480,12 +484,11 @@ const dataProvider = {
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(params.data, filterNullValues),
} }
) )
).then(responses => ({ );
data: responses.map(({ json }) => json), return { data: responses.map(({ json }) => json) };
}));
}, },
create: (resource, params) => { create: async (resource, params) => {
console.log("create " + resource); console.log("create " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -495,15 +498,14 @@ const dataProvider = {
const create = res["create"](params.data); const create = res["create"](params.data);
const endpoint_url = homeserver + create.endpoint; const endpoint_url = homeserver + create.endpoint;
return jsonClient(endpoint_url, { const { json } = await jsonClient(endpoint_url, {
method: create.method, method: create.method,
body: JSON.stringify(create.body, filterNullValues), body: JSON.stringify(create.body, filterNullValues),
}).then(({ json }) => ({ });
data: res.map(json), return { data: res.map(json) };
}));
}, },
createMany: (resource, params) => { createMany: async (resource, params) => {
console.log("createMany " + resource); console.log("createMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -511,7 +513,7 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
if (!("create" in res)) return Promise.reject(); if (!("create" in res)) return Promise.reject();
return Promise.all( const responses = await Promise.all(
params.ids.map(id => { params.ids.map(id => {
params.data.id = id; params.data.id = id;
const cre = res["create"](params.data); const cre = res["create"](params.data);
@ -521,12 +523,11 @@ const dataProvider = {
body: JSON.stringify(cre.body, filterNullValues), body: JSON.stringify(cre.body, filterNullValues),
}); });
}) })
).then(responses => ({ );
data: responses.map(({ json }) => json), return { data: responses.map(({ json }) => json) };
}));
}, },
delete: (resource, params) => { delete: async (resource, params) => {
console.log("delete " + resource); console.log("delete " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -536,24 +537,22 @@ const dataProvider = {
if ("delete" in res) { if ("delete" in res) {
const del = res["delete"](params); const del = res["delete"](params);
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
return jsonClient(endpoint_url, { const { json } = await jsonClient(endpoint_url, {
method: "method" in del ? del.method : "DELETE", method: "method" in del ? del.method : "DELETE",
body: "body" in del ? JSON.stringify(del.body) : null, body: "body" in del ? JSON.stringify(del.body) : null,
}).then(({ json }) => ({ });
data: json, return { data: json };
}));
} else { } else {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.id}`, { const { json } = await jsonClient(`${endpoint_url}/${params.id}`, {
method: "DELETE", method: "DELETE",
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(params.previousData, filterNullValues),
}).then(({ json }) => ({ });
data: json, return { data: json };
}));
} }
}, },
deleteMany: (resource, params) => { deleteMany: async (resource, params) => {
console.log("deleteMany " + resource); console.log("deleteMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -561,7 +560,7 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
if ("delete" in res) { if ("delete" in res) {
return Promise.all( const responses = await Promise.all(
params.ids.map(id => { params.ids.map(id => {
const del = res["delete"]({ ...params, id: id }); const del = res["delete"]({ ...params, id: id });
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
@ -570,21 +569,21 @@ const dataProvider = {
body: "body" in del ? JSON.stringify(del.body) : null, body: "body" in del ? JSON.stringify(del.body) : null,
}); });
}) })
).then(responses => ({ );
return {
data: responses.map(({ json }) => json), data: responses.map(({ json }) => json),
})); };
} else { } else {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return Promise.all( const responses = await Promise.all(
params.ids.map(id => params.ids.map(id =>
jsonClient(`${endpoint_url}/${id}`, { jsonClient(`${endpoint_url}/${id}`, {
method: "DELETE", method: "DELETE",
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(params.data, filterNullValues),
}) })
) )
).then(responses => ({ );
data: responses.map(({ json }) => json), return { data: responses.map(({ json }) => json) };
}));
} }
}, },
}; };

55
src/synapse/synapse.js Normal file
View File

@ -0,0 +1,55 @@
import { fetchUtils } from "react-admin";
export const splitMxid = mxid => {
const re =
/^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
return re.exec(mxid)?.groups;
};
export const isValidBaseUrl = baseUrl =>
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
/**
* Resolve the homeserver URL using the well-known lookup
* @param domain the domain part of an MXID
* @returns homeserver base URL
*/
export const getWellKnownUrl = async domain => {
const wellKnownUrl = `https://${domain}/.well-known/matrix/client`;
try {
const json = await fetchUtils.fetchJson(wellKnownUrl, { method: "GET" });
return json["m.homeserver"].base_url;
} catch {
// if there is no .well-known entry, return the domain itself
return `https://${domain}`;
}
};
/**
* Get synapse server version
* @param base_url the base URL of the homeserver
* @returns server version
*/
export const getServerVersion = async baseUrl => {
const versionUrl = `${baseUrl}/_synapse/admin/v1/server_version`;
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
return response.json.server_version;
};
/** Get supported Matrix features */
export const getSupportedFeatures = async baseUrl => {
const versionUrl = `${baseUrl}/_matrix/client/versions`;
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
return response.json;
};
/**
* Get supported login flows
* @param baseUrl the base URL of the homeserver
* @returns array of supported login flows
*/
export const getSupportedLoginFlows = async baseUrl => {
const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`;
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
return response.json.flows;
};

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());
});

7595
yarn.lock

File diff suppressed because it is too large Load Diff