Compare commits

...

30 commits

Author SHA1 Message Date
Dirk Klimpel
9fc005032c
Fix for empty user default tab after creation (#628) 2024-10-08 09:20:55 +02:00
Manuel Stahl
dbcb4f92dc Use central defintion of storage system
Change-Id: Ibf31c650b08920bf82827607c3421556ac90ae61
2024-08-17 10:50:29 +02:00
dependabot[bot]
035baa786a
Bump softprops/action-gh-release from 2.0.6 to 2.0.8 (#588)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.6 to 2.0.8.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](a74c6b72af...c062e08bd5)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  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-07-24 14:13:08 +02:00
Manuel Stahl
31fe23d688 Upgrade dependencies
Change-Id: I0ebdf0f111a10f3934059966b5ea90cc5aefede8
2024-07-24 14:03:08 +02:00
Manuel Stahl
d3e623e578 Upgrade react-admin
Change-Id: Id355a462f3533cace724b4d4b88921c9aafadfda
2024-07-24 14:00:47 +02:00
Manuel Stahl
a38bc442cb Upgrade vite, react and jest
Change-Id: I9501f8dc3a9759255fa94434f5c0d4eb8efc01d7
2024-07-24 13:46:25 +02:00
Manuel Stahl
f88eacee2a Upgrade eslint plugins
Change-Id: I8c7a21ccadaefa6fe740be6d903b9b6915c8bd4e
2024-07-22 13:52:04 +02:00
rkfg
77cc936710
Fix a few undefined records (#580) 2024-07-18 20:38:46 +02:00
rkfg
eb626a7e9e
Add Russian language support (#581) 2024-07-18 20:32:22 +02:00
Manuel Stahl
ce5d6587c1 Bump version to 0.10.3
Change-Id: If0ebb483c0ef30e2331649f903a602b9e22e98f3
2024-07-18 20:27:39 +02:00
Manuel Stahl
4adf2c2bca Fix getWellKnownUrl()
Change-Id: I494831a7608e80c4d9fa1a2455755d915607a22d
2024-07-09 13:03:53 +02:00
Manuel Stahl
fce6e03fc5 Regroup source code
- components directory contains react components
- pages directory contains all custom pages
- resources directory contains everything that exports ResourceProps

Change-Id: I5b9b68f67e232044fabf11810482873ce5b32053
2024-07-09 13:03:53 +02:00
Manuel Stahl
ec0fc14b68 Use custom data provider method for "delete_media"
This is not a REST endpoint, so it's better to use a custom method, see
https://marmelab.com/react-admin/DataProviders.html#adding-custom-methods

Change-Id: I256286949e77b998f759f671b2d4e9790f8ca39c
2024-07-09 13:03:53 +02:00
Manuel Stahl
f55e02730e Dedupe yarn.lock
Change-Id: Ib7177d237e5411634fa8d0ab07220e9990f526c0
2024-07-09 13:03:53 +02:00
dependabot[bot]
c48f560fb0
Bump @mui/material from 5.15.16 to 5.16.0 (#571)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.15.16 to 5.16.0.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v5.16.0/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.16.0/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  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-07-09 11:24:42 +02:00
dependabot[bot]
a5714386f4
Bump @types/node from 20.14.0 to 20.14.10 (#568)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.14.0 to 20.14.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  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-07-09 11:24:33 +02:00
dependabot[bot]
659730ce2e
Bump ts-jest from 29.1.2 to 29.2.0 (#570)
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.1.2 to 29.2.0.
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.1.2...v29.2.0)

---
updated-dependencies:
- dependency-name: ts-jest
  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-07-09 11:21:42 +02:00
dependabot[bot]
cc51b3edbe
Bump react-hook-form from 7.51.3 to 7.52.1 (#569)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.51.3 to 7.52.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.51.3...v7.52.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  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-07-09 11:21:21 +02:00
dependabot[bot]
c07ec04b4e
Bump JamesIves/github-pages-deploy-action from 4.6.0 to 4.6.3 (#572)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.0 to 4.6.3.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.0...v4.6.3)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  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-07-09 11:17:46 +02:00
dependabot[bot]
c7f3fa9212
Bump eslint-plugin-jsx-a11y from 6.8.0 to 6.9.0 (#567)
Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.8.0...v6.9.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsx-a11y
  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-07-09 11:13:20 +02:00
dependabot[bot]
9c5e755f3f
Bump softprops/action-gh-release from 2.0.5 to 2.0.6 (#561)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.5 to 2.0.6.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](69320dbe05...a74c6b72af)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  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-07-09 11:12:58 +02:00
dependabot[bot]
048a43f404
Bump docker/build-push-action from 5 to 6 (#554)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  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-07-09 11:12:34 +02:00
dependabot[bot]
f8ac0403a9
Bump @typescript-eslint/parser from 7.8.0 to 7.15.0 (#565)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.8.0 to 7.15.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.15.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-07-03 08:32:53 +02:00
dependabot[bot]
cbfdc1d6f6
Bump @eslint/js from 9.1.1 to 9.6.0 (#563)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.1.1 to 9.6.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/commits/v9.6.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  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-07-03 08:32:40 +02:00
Dirk Klimpel
8cadfbd3af
Support darkTheme (#459)
* add darkTheme

* set `palette` to `darkTheme`
2024-07-03 08:31:24 +02:00
dependabot[bot]
dbc7d328cc
Bump the npm_and_yarn group with 2 updates (#560)
Bumps the npm_and_yarn group with 2 updates: [braces](https://github.com/micromatch/braces) and [ws](https://github.com/websockets/ws).


Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `ws` from 8.16.0 to 8.17.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: ws
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 08:30:20 +02:00
dependabot[bot]
e21e44362c
Bump @types/node from 20.12.7 to 20.14.0 (#547)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.12.7 to 20.14.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  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-06-04 09:28:56 +02:00
dependabot[bot]
38057de5c8
Bump softprops/action-gh-release from 2.0.4 to 2.0.5 (#530)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](9d7c94cfd0...69320dbe05)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  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-06-04 09:26:35 +02:00
dependabot[bot]
882fe264b2
Bump vite from 5.2.9 to 5.2.12 (#545)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.9 to 5.2.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.2.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  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-06-04 09:21:45 +02:00
Dirk Klimpel
002b63acad
Set relative base path in vite.config.ts (#536) 2024-06-04 09:21:17 +02:00
31 changed files with 1377 additions and 841 deletions

View file

@ -54,7 +54,7 @@ jobs:
ghcr.io/${{ github.repository }}
- name: Build and Push Tag
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: true

View file

@ -23,7 +23,7 @@ jobs:
yarn build --base=/synapse-admin
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.6.0
uses: JamesIves/github-pages-deploy-action@v4.6.3
with:
branch: gh-pages
folder: dist

View file

@ -23,7 +23,7 @@ jobs:
version=`git describe --dirty --tags || echo unknown`
cp -r dist synapse-admin-$version
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
- uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564
- uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191
with:
files: dist/*.tar.gz
env:

View file

@ -1,6 +1,6 @@
{
"name": "synapse-admin",
"version": "0.10.1",
"version": "0.10.3",
"description": "Admin GUI for the Matrix.org server Synapse",
"type": "module",
"author": "Awesome Technologies Innovationslabor GmbH",
@ -12,63 +12,64 @@
},
"packageManager": "yarn@4.1.1",
"devDependencies": {
"@eslint/js": "^9.1.1",
"@eslint/js": "^9.7.0",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.2",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.0",
"@types/node": "^20.12.7",
"@types/lodash": "^4.17.7",
"@types/node": "^20.14.12",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.8.0",
"@types/react": "^18.3.3",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^3.2.0",
"eslint-plugin-yaml": "^0.5.0",
"eslint-plugin-yaml": "^1.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"prettier": "^3.2.5",
"prettier": "^3.3.3",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.1.2",
"ts-jest": "^29.2.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0",
"vite": "^5.0.0",
"vite-plugin-version-mark": "^0.0.13"
"typescript-eslint": "^7.16.1",
"vite": "^5.3.4",
"vite-plugin-version-mark": "^0.1.0"
},
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@haleos/ra-language-german": "^1.0.0",
"@haxqer/ra-language-chinese": "^4.16.2",
"@mui/icons-material": "^5.15.16",
"@mui/material": "^5.15.16",
"history": "^5.1.0",
"@mui/icons-material": "^5.16.4",
"@mui/material": "^5.16.4",
"history": "^5.3.0",
"lodash": "^4.17.21",
"papaparse": "^5.4.1",
"query-string": "^7.1.1",
"ra-core": "^4.16.17",
"ra-i18n-polyglot": "^4.16.17",
"ra-language-english": "^4.16.17",
"query-string": "^7.1.3",
"ra-core": "^4.16.20",
"ra-i18n-polyglot": "^4.16.20",
"ra-language-english": "^4.16.20",
"ra-language-farsi": "^4.2.0",
"ra-language-french": "^4.16.17",
"ra-language-french": "^4.16.20",
"ra-language-italian": "^3.13.1",
"ra-language-russian": "^4.14.2",
"react": "^18.3.1",
"react-admin": "^4.16.17",
"react-admin": "^4.16.20",
"react-dom": "^18.3.1",
"react-hook-form": "^7.43.9",
"react-hook-form": "^7.52.1",
"react-is": "^18.3.1",
"react-query": "^3.32.1",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.0"
"react-query": "^3.39.3",
"react-router": "^6.25.1",
"react-router-dom": "^6.25.1"
},
"scripts": {
"start": "vite serve",
@ -94,7 +95,7 @@
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic",
"plugin:import/typescript",
"plugin:yaml/recommended"
"plugin:yaml/legacy"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {

View file

@ -4,20 +4,21 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
import { Route } from "react-router-dom";
import reports from "./components/EventReports";
import { ImportFeature } from "./components/ImportFeature";
import LoginPage from "./components/LoginPage";
import registrationToken from "./components/RegistrationTokens";
import roomDirectory from "./components/RoomDirectory";
import destinations from "./components/destinations";
import rooms from "./components/rooms";
import userMediaStats from "./components/statistics";
import users from "./components/users";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr";
import italianMessages from "./i18n/it";
import russianMessages from "./i18n/ru";
import chineseMessages from "./i18n/zh";
import LoginPage from "./pages/LoginPage";
import destinations from "./resources/destinations";
import registrationToken from "./resources/registration_tokens";
import reports from "./resources/reports";
import roomDirectory from "./resources/room_directory";
import rooms from "./resources/rooms";
import userMediaStats from "./resources/user_media_statistics";
import users from "./resources/users";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
@ -27,6 +28,7 @@ const messages = {
en: englishMessages,
fr: frenchMessages,
it: italianMessages,
ru: russianMessages,
zh: chineseMessages,
};
const i18nProvider = polyglotI18nProvider(
@ -38,6 +40,7 @@ const i18nProvider = polyglotI18nProvider(
{ locale: "fr", name: "Français" },
{ locale: "it", name: "Italiano" },
{ locale: "fa", name: "Persian(فارسی)" },
{ locale: "ru", name: "Russian(Русский)" },
{ locale: "zh", name: "简体中文" },
]
);
@ -50,6 +53,7 @@ const App = () => (
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
darkTheme={{ palette: { mode: "dark" } }}
>
<CustomRoutes>
<Route path="/import_users" element={<ImportFeature />} />

View file

@ -21,23 +21,27 @@ import {
Toolbar,
ToolbarProps,
useCreate,
useDataProvider,
useDelete,
useNotify,
useRecordContext,
useRefresh,
useTranslate,
} from "react-admin";
import { useMutation } from "react-query";
import { Link } from "react-router-dom";
import { dateParser } from "./date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
import { getMediaUrl } from "../synapse/synapse";
import storage from "../storage";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate();
const DeleteMediaToolbar = (props: ToolbarProps) => (
<Toolbar {...props}>
<SaveButton label="resources.delete_media.action.send" icon={<DeleteSweepIcon />} />
<SaveButton label="delete_media.action.send" icon={<DeleteSweepIcon />} />
<Button label="ra.action.cancel" onClick={onClose}>
<IconCancel />
</Button>
@ -46,21 +50,21 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{translate("resources.delete_media.action.send")}</DialogTitle>
<DialogTitle>{translate("delete_media.action.send")}</DialogTitle>
<DialogContent>
<DialogContentText>{translate("resources.delete_media.helper.send")}</DialogContentText>
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
<DateTimeInput
fullWidth
source="before_ts"
label="resources.delete_media.fields.before_ts"
label="delete_media.fields.before_ts"
defaultValue={0}
parse={dateParser}
/>
<NumberInput
fullWidth
source="size_gt"
label="resources.delete_media.fields.size_gt"
label="delete_media.fields.size_gt"
defaultValue={0}
min={0}
step={1024}
@ -68,7 +72,7 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
<BooleanInput
fullWidth
source="keep_profiles"
label="resources.delete_media.fields.keep_profiles"
label="delete_media.fields.keep_profiles"
defaultValue={true}
/>
</SimpleForm>
@ -81,34 +85,30 @@ export const DeleteMediaButton = (props: ButtonProps) => {
const theme = useTheme();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [deleteOne, { isLoading }] = useDelete();
const dataProvider = useDataProvider<SynapseDataProvider>();
const { mutate: deleteMedia, isLoading } = useMutation(
(values: DeleteMediaParams) => dataProvider.deleteMedia(values),
{
onSuccess: () => {
notify("delete_media.action.send_success");
closeDialog();
},
onError: () => {
notify("delete_media.action.send_failure", {
type: "error",
});
},
}
);
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const deleteMedia = (values: { before_ts: string; size_gt: number; keep_profiles: boolean }) => {
deleteOne(
"delete_media",
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
{ meta: values },
{
onSuccess: () => {
notify("resources.delete_media.action.send_success");
closeDialog();
},
onError: () =>
notify("resources.delete_media.action.send_failure", {
type: "error",
}),
}
);
};
return (
<>
<Button
{...props}
label="resources.delete_media.action.send"
label="delete_media.action.send"
onClick={openDialog}
disabled={isLoading}
sx={{
@ -340,7 +340,7 @@ export const ViewMediaButton = ({ media_id, label }) => {
};
export const MediaIDField = ({ source }) => {
const homeserver = localStorage.getItem("home_server");
const homeserver = storage.getItem("home_server");
const record = useRecordContext();
if (!record) return null;

View file

@ -92,6 +92,22 @@ const de: SynapseTranslationMessages = {
},
},
},
delete_media: {
name: "Medien",
fields: {
before_ts: "Letzter Zugriff vor",
size_gt: "Größer als (in Bytes)",
keep_profiles: "Behalte Profilbilder",
},
action: {
send: "Medien löschen",
send_success: "Anfrage erfolgreich versendet.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
helper: {
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
},
},
resources: {
users: {
name: "Benutzer",
@ -260,22 +276,6 @@ const de: SynapseTranslationMessages = {
open: "Mediendatei in neuem Fenster öffnen",
},
},
delete_media: {
name: "Medien",
fields: {
before_ts: "Letzter Zugriff vor",
size_gt: "Größer als (in Bytes)",
keep_profiles: "Behalte Profilbilder",
},
action: {
send: "Medien löschen",
send_success: "Anfrage erfolgreich versendet.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
helper: {
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
},
},
protect_media: {
action: {
create: "Ungeschützt, Schutz erstellen",

View file

@ -86,11 +86,27 @@ const en: SynapseTranslationMessages = {
successful: "%{smart_count} entries successfully imported",
skipped: "%{smart_count} entries skipped",
download_skipped: "Download skipped records",
with_error: "%{smart_count} entry with errors ||| %{smart_count} entries with errors",
with_error: "%{smart_count} entry with errors |||| %{smart_count} entries with errors",
simulated_only: "Run was only simulated",
},
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "last access before",
size_gt: "Larger then (in bytes)",
keep_profiles: "Keep profile images",
},
action: {
send: "Delete media",
send_success: "Request successfully sent.",
send_failure: "An error has occurred.",
},
helper: {
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
},
},
resources: {
users: {
name: "User |||| Users",
@ -258,22 +274,6 @@ const en: SynapseTranslationMessages = {
open: "Open media file in new window",
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "last access before",
size_gt: "Larger then (in bytes)",
keep_profiles: "Keep profile images",
},
action: {
send: "Delete media",
send_success: "Request successfully sent.",
send_failure: "An error has occurred.",
},
helper: {
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
},
},
protect_media: {
action: {
create: "Unprotected, create protection",

View file

@ -89,6 +89,22 @@ const fa: SynapseTranslationMessages = {
},
},
},
delete_media: {
name: "رسانه ها",
fields: {
before_ts: "آخرین دسترسی قبل",
size_gt: "بزرگتر از آن (به بایت)",
keep_profiles: "تصاویر پروفایل را نگه دارید",
},
action: {
send: "حذف رسانه ها",
send_success: "درخواست با موفقیت ارسال شد.",
send_failure: "خطایی رخ داده است.",
},
helper: {
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانه‌هایی که در مخازن رسانه خارجی آپلود شده‌اند تأثیری نخواهد گذاشت.",
},
},
resources: {
users: {
name: "کاربر |||| کاربران",
@ -241,22 +257,6 @@ const fa: SynapseTranslationMessages = {
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: "محافظت نشده، حفاظت ایجاد کنید",

View file

@ -92,6 +92,22 @@ const fr: SynapseTranslationMessages = {
},
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "Dernier accès avant",
size_gt: "Plus grand que (en octets)",
keep_profiles: "Conserver les images de profil",
},
action: {
send: "Supprimer le média",
send_success: "Requête envoyée avec succès",
send_failure: "Une erreur s'est produite",
},
helper: {
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
},
},
resources: {
users: {
name: "Utilisateur |||| Utilisateurs",
@ -243,22 +259,6 @@ const fr: SynapseTranslationMessages = {
last_access_ts: "Dernier accès",
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "Dernier accès avant",
size_gt: "Plus grand que (en octets)",
keep_profiles: "Conserver les images de profil",
},
action: {
send: "Supprimer le média",
send_success: "Requête envoyée avec succès",
send_failure: "Une erreur s'est produite",
},
helper: {
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
},
},
protect_media: {
action: {
create: "Protéger",

32
src/i18n/index.d.ts vendored
View file

@ -87,6 +87,22 @@ interface SynapseTranslationMessages extends TranslationMessages {
};
};
};
delete_media: {
name: string;
fields: {
before_ts: string;
size_gt: string;
keep_profiles: string;
};
action: {
send: string;
send_success: string;
send_failure: string;
};
helper: {
send: string;
};
};
resources: {
users: {
name: string;
@ -252,22 +268,6 @@ interface SynapseTranslationMessages extends TranslationMessages {
open: string;
};
};
delete_media: {
name: string;
fields: {
before_ts: string;
size_gt: string;
keep_profiles: string;
};
action: {
send: string;
send_success: string;
send_failure: string;
};
helper: {
send: string;
};
};
protect_media?: {
action: {
create: string;

View file

@ -89,6 +89,22 @@ const it: SynapseTranslationMessages = {
},
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "ultimo accesso effettuato prima",
size_gt: "Più grande di (in byte)",
keep_profiles: "Mantieni le immagini del profilo",
},
action: {
send: "Cancella media",
send_success: "Richiesta inviata con successo.",
send_failure: "C'è stato un errore.",
},
helper: {
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
},
},
resources: {
users: {
name: "Utente |||| Utenti",
@ -242,22 +258,6 @@ const it: SynapseTranslationMessages = {
last_access_ts: "Ultimo accesso",
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "ultimo accesso effettuato prima",
size_gt: "Più grande di (in byte)",
keep_profiles: "Mantieni le immagini del profilo",
},
action: {
send: "Cancella media",
send_success: "Richiesta inviata con successo.",
send_failure: "C'è stato un errore.",
},
helper: {
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
},
},
protect_media: {
action: {
create: "Non protetto, proteggi",

406
src/i18n/ru.ts Normal file
View file

@ -0,0 +1,406 @@
import russianMessages from "ra-language-russian";
import { SynapseTranslationMessages } from ".";
const ru: SynapseTranslationMessages = {
...russianMessages,
synapseadmin: {
auth: {
base_url: "Адрес домашнего сервера",
welcome: "Добро пожаловать в Synapse-admin",
server_version: "Версия Synapse",
supports_specs: "поддерживает спецификации Matrix",
username_error: "Пожалуйста, укажите полный ID пользователя: '@user:domain'",
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
url_error: "Неверный адрес сервера Matrix",
sso_sign_in: "Вход через SSO",
},
users: {
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
tabs: { sso: "SSO" },
},
rooms: {
details: "Данные комнаты",
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 %{id} уже существует",
},
title: "Импорт пользователей из CSV",
goToPdf: "Перейти к PDF",
cards: {
importstats: {
header: "Импорт пользователей",
users_total:
"%{smart_count} пользователь в CSV файле |||| %{smart_count} пользователя в CSV файле |||| %{smart_count} пользователей в CSV файле",
guest_count: "%{smart_count} гость |||| %{smart_count} гостя |||| %{smart_count} гостей",
admin_count:
"%{smart_count} администратор |||| %{smart_count} администратора |||| %{smart_count} администраторов",
},
conflicts: {
header: "Стратегия разрешения конфликтов",
mode: {
stop: "Остановка при конфликте",
skip: "Показать ошибку и пропустить при конфликте",
},
},
ids: {
header: "Идентификаторы",
all_ids_present: "Идентификаторы присутствуют в каждой записи",
count_ids_present:
"%{smart_count} запись с ID |||| %{smart_count} записи с ID |||| %{smart_count} записей с ID",
mode: {
ignore: "Игнорировать идентификаторы в CSV и создать новые",
update: "Обновить существующие записи",
},
},
passwords: {
header: "Пароли",
all_passwords_present: "Пароли присутствуют в каждой записи",
count_passwords_present:
"%{smart_count} запись с паролем |||| %{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} записи всего |||| %{smart_count} записей всего",
successful:
"%{smart_count} запись успешно импортирована |||| %{smart_count} записи успешно импортированы |||| %{smart_count} записей успешно импортированы",
skipped:
"%{smart_count} запись пропущена |||| %{smart_count} записи пропущены |||| %{smart_count} записей пропущено",
download_skipped: "Скачать пропущенные записи",
with_error:
"%{smart_count} запись с ошибкой |||| %{smart_count} записи с ошибками |||| %{smart_count} записей с ошибками",
simulated_only: "Импорт был симулирован",
},
},
},
delete_media: {
name: "Файлы",
fields: {
before_ts: "Последнее обращение до",
size_gt: "Более чем (в байтах)",
keep_profiles: "Сохранить аватары",
},
action: {
send: "Удалить файлы",
send_success: "Запрос успешно отправлен.",
send_failure: "Произошла ошибка.",
},
helper: {
send: "Это API удаляет локальные файлы с вашего собственного сервера, включая локальные миниатюры и копии скачанных файлов. \
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
},
},
resources: {
users: {
name: "Пользователь |||| Пользователи",
email: "Почта",
msisdn: "Телефон",
threepid: "Почта / Телефон",
fields: {
avatar: "Аватар",
id: "ID пользователя",
name: "Имя",
is_guest: "Гость",
admin: "Администратор сервера",
locked: "Заблокирован",
deactivated: "Деактивирован",
erased: "Удалён",
guests: "Показывать гостей",
show_deactivated: "Показывать деактивированных",
user_id: "Поиск пользователя",
displayname: "Отображаемое имя",
password: "Пароль",
avatar_url: "Адрес аватары",
avatar_src: "Аватар",
medium: "Тип",
threepids: "3PID'ы",
address: "Адрес",
creation_ts_ms: "Дата создания",
consent_version: "Версия соглашения",
auth_provider: "Провайдер",
user_type: "Тип пользователя",
},
helper: {
password: "Смена пароля завершит все сессии пользователя.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
},
action: {
erase: "Удалить данные пользователя",
},
},
rooms: {
name: "Комната |||| Комнаты",
fields: {
room_id: "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:
"Оконечности это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
Хотя 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: "ID",
received_ts: "Дата и время жалобы",
user_id: "Автор жалобы",
name: "Название комнаты",
score: "Баллы",
reason: "Причина",
event_id: "ID события",
event_json: {
origin: "Исходнный сервер",
origin_server_ts: "Дата и время отправки",
type: "Тип события",
content: {
msgtype: "Тип содержимого",
body: "Содержимое",
format: "Формат",
formatted_body: "Форматированное содержимое",
algorithm: "Алгоритм",
url: "Ссылка",
info: {
mimetype: "Тип",
},
},
},
},
action: {
erase: {
title: "Удалить жалобу",
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
},
},
},
connections: {
name: "Подключения",
fields: {
last_seen: "Дата",
ip: "IP адрес",
user_agent: "Юзер-агент",
},
},
devices: {
name: "Устройство |||| Устройства",
fields: {
device_id: "ID устройства",
display_name: "Название",
last_seen_ts: "Дата и время",
last_seen_ip: "IP адрес",
},
action: {
erase: {
title: "Удаление %{id}",
content: 'Действительно удалить устройство "%{name}"?',
success: "Устройство успешно удалено.",
failure: "Произошла ошибка.",
},
},
},
users_media: {
name: "Файлы",
fields: {
media_id: "ID файла",
media_length: "Размер файла (в байтах)",
media_type: "Тип",
upload_name: "Имя файла",
quarantined_by: "На карантине",
safe_from_quarantine: "Защитить от карантина",
created_ts: "Создано",
last_access_ts: "Последний доступ",
},
action: {
open: "Открыть файл в новом окне",
},
},
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: "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: "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} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
content:
"Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{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

@ -89,6 +89,22 @@ const zh: SynapseTranslationMessages = {
},
},
},
delete_media: {
name: "媒体文件",
fields: {
before_ts: "最后访问时间",
size_gt: "大于 (字节)",
keep_profiles: "保留头像",
},
action: {
send: "删除媒体",
send_success: "请求发送成功。",
send_failure: "出现了一个错误。",
},
helper: {
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
},
},
resources: {
users: {
name: "用户",
@ -224,22 +240,6 @@ const zh: SynapseTranslationMessages = {
last_access_ts: "上一次访问",
},
},
delete_media: {
name: "媒体文件",
fields: {
before_ts: "最后访问时间",
size_gt: "大于 (字节)",
keep_profiles: "保留头像",
},
action: {
send: "删除媒体",
send_success: "请求发送成功。",
send_failure: "出现了一个错误。",
},
helper: {
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
},
},
pushers: {
name: "发布者",
fields: {

View file

@ -27,6 +27,7 @@ import {
isValidBaseUrl,
splitMxid,
} from "../synapse/synapse";
import storage from "../storage";
const FormBox = styled(Box)(({ theme }) => ({
display: "flex",
@ -94,7 +95,7 @@ const LoginPage = () => {
const [locale, setLocale] = useLocaleState();
const locales = useLocales();
const translate = useTranslate();
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
@ -103,8 +104,8 @@ const LoginPage = () => {
console.log("SSO token is", ssoToken);
// Prevent further requests
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
const baseUrl = localStorage.getItem("sso_base_url");
localStorage.removeItem("sso_base_url");
const baseUrl = storage.getItem("sso_base_url");
storage.removeItem("sso_base_url");
if (baseUrl) {
const auth = {
base_url: baseUrl,
@ -154,7 +155,7 @@ const LoginPage = () => {
};
const handleSSO = () => {
localStorage.setItem("sso_base_url", ssoBaseUrl);
storage.setItem("sso_base_url", ssoBaseUrl);
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
window.location.href
)}`;

View file

@ -29,7 +29,7 @@ import {
useTranslate,
} from "react-admin";
import { DATE_FORMAT } from "./date";
import { DATE_FORMAT } from "../components/date";
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@ -87,7 +87,7 @@ const DestinationTitle = () => {
const translate = useTranslate();
return (
<span>
{translate("resources.destinations.name", 1)} {record.destination}
{translate("resources.destinations.name", 1)} {record?.destination}
</span>
);
};

View file

@ -23,7 +23,7 @@ import {
Toolbar,
} from "react-admin";
import { DATE_FORMAT, dateFormatter, dateParser } from "./date";
import { DATE_FORMAT, dateFormatter, dateParser } from "../components/date";
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
const validateUsesAllowed = [number()];

View file

@ -21,8 +21,8 @@ import {
useTranslate,
} from "react-admin";
import { DATE_FORMAT } from "./date";
import { MXCField } from "./media";
import { DATE_FORMAT } from "../components/date";
import { MXCField } from "../components/media";
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;

View file

@ -27,7 +27,7 @@ import {
} from "react-admin";
import { useMutation } from "react-query";
import AvatarField from "./AvatarField";
import AvatarField from "../components/AvatarField";
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;

View file

@ -43,8 +43,8 @@ import {
RoomDirectoryBulkPublishButton,
RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton,
} from "./RoomDirectory";
import { DATE_FORMAT } from "./date";
} from "./room_directory";
import { DATE_FORMAT } from "../components/date";
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@ -65,7 +65,7 @@ const RoomTitle = () => {
const RoomShowActions = () => {
const record = useRecordContext();
const publishButton = record.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
const publishButton = record?.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
// FIXME: refresh after (un)publish
return (
<TopToolbar>

View file

@ -13,7 +13,7 @@ import {
useListContext,
} from "react-admin";
import { DeleteMediaButton } from "./media";
import { DeleteMediaButton } from "../components/media";
const ListActions = () => {
const { isLoading, total } = useListContext();

View file

@ -47,14 +47,15 @@ import {
TopToolbar,
NumberField,
useListContext,
Identifier,
} from "react-admin";
import { Link } from "react-router-dom";
import AvatarField from "./AvatarField";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DATE_FORMAT } from "./date";
import { DeviceRemoveButton } from "./devices";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "./media";
import AvatarField from "../components/AvatarField";
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { DATE_FORMAT } from "../components/date";
import { DeviceRemoveButton } from "../components/devices";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
const choices_medium = [
{ id: "email", name: "resources.users.email" },
@ -112,7 +113,10 @@ export const UserList = (props: ListProps) => (
actions={<UserListActions />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
<Datagrid
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
bulkActionButtons={<UserBulkActionButtons />}
>
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
@ -128,8 +132,8 @@ export const UserList = (props: ListProps) => (
// https://matrix.org/docs/spec/appendices#user-identifiers
// here only local part of user_id
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
// localStorage.getItem("home_server").length is not valid here
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
// storage.getItem("home_server").length is not valid here
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
const validateAddress = [required(), maxLength(255)];
@ -140,7 +144,7 @@ const UserEditActions = () => {
return (
<TopToolbar>
{!record.deactivated && <ServerNoticeButton />}
{!record?.deactivated && <ServerNoticeButton />}
<DeleteButton
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
@ -153,7 +157,12 @@ const UserEditActions = () => {
};
export const UserCreate = (props: CreateProps) => (
<Create {...props}>
<Create
{...props}
redirect={(resource: string | undefined, id: Identifier | undefined) => {
return `${resource}/${id}`;
}}
>
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" validate={maxLength(256)} />

3
src/storage.ts Normal file
View file

@ -0,0 +1,3 @@
const storage = localStorage;
export default storage;

View file

@ -1,13 +1,14 @@
import fetchMock from "jest-fetch-mock";
import authProvider from "./authProvider";
import storage from "../storage";
fetchMock.enableMocks();
describe("authProvider", () => {
beforeEach(() => {
fetchMock.resetMocks();
localStorage.clear();
storage.clear();
});
describe("login", () => {
@ -36,10 +37,10 @@ describe("authProvider", () => {
}),
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");
expect(storage.getItem("base_url")).toEqual("http://example.com");
expect(storage.getItem("user_id")).toEqual("@user:example.com");
expect(storage.getItem("access_token")).toEqual("foobar");
expect(storage.getItem("device_id")).toEqual("some_device");
});
});
@ -67,16 +68,16 @@ describe("authProvider", () => {
}),
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");
expect(storage.getItem("base_url")).toEqual("https://example.com");
expect(storage.getItem("user_id")).toEqual("@user:example.com");
expect(storage.getItem("access_token")).toEqual("foobar");
expect(storage.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");
it("should remove the access_token from storage", async () => {
storage.setItem("base_url", "example.com");
storage.setItem("access_token", "foo");
fetchMock.mockResponse(JSON.stringify({}));
await authProvider.logout(null);
@ -89,7 +90,7 @@ describe("authProvider", () => {
method: "POST",
user: { authenticated: true, token: "Bearer foo" },
});
expect(localStorage.getItem("access_token")).toBeNull();
expect(storage.getItem("access_token")).toBeNull();
});
});
@ -113,7 +114,7 @@ describe("authProvider", () => {
});
it("should resolve when logged in", async () => {
localStorage.setItem("access_token", "foobar");
storage.setItem("access_token", "foobar");
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
});

View file

@ -1,5 +1,7 @@
import { AuthProvider, Options, fetchUtils } from "react-admin";
import storage from "../storage";
const authProvider: AuthProvider = {
// called when the user attempts to log in
login: async ({
@ -19,7 +21,7 @@ const authProvider: AuthProvider = {
body: JSON.stringify(
Object.assign(
{
device_id: localStorage.getItem("device_id"),
device_id: storage.getItem("device_id"),
initial_device_display_name: "Synapse Admin",
},
loginToken
@ -40,23 +42,23 @@ const authProvider: AuthProvider = {
// server, since the admin might want to access the admin API via some
// private address
base_url = base_url.replace(/\/+$/g, "");
localStorage.setItem("base_url", base_url);
storage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url);
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
const { json } = await fetchUtils.fetchJson(login_api_url, options);
localStorage.setItem("home_server", json.home_server);
localStorage.setItem("user_id", json.user_id);
localStorage.setItem("access_token", json.access_token);
localStorage.setItem("device_id", json.device_id);
storage.setItem("home_server", json.home_server);
storage.setItem("user_id", json.user_id);
storage.setItem("access_token", json.access_token);
storage.setItem("device_id", json.device_id);
},
// called when the user clicks on the logout button
logout: async () => {
console.log("logout");
const logout_api_url = localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
const access_token = localStorage.getItem("access_token");
const logout_api_url = storage.getItem("base_url") + "/_matrix/client/r0/logout";
const access_token = storage.getItem("access_token");
const options: Options = {
method: "POST",
@ -68,7 +70,7 @@ const authProvider: AuthProvider = {
if (typeof access_token === "string") {
await fetchUtils.fetchJson(logout_api_url, options);
localStorage.removeItem("access_token");
storage.removeItem("access_token");
}
},
// called when the API returns an error
@ -81,7 +83,7 @@ const authProvider: AuthProvider = {
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
const access_token = localStorage.getItem("access_token");
const access_token = storage.getItem("access_token");
console.log("checkAuth " + access_token);
return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
},

View file

@ -1,6 +1,7 @@
import fetchMock from "jest-fetch-mock";
import dataProvider from "./dataProvider";
import storage from "../storage";
fetchMock.enableMocks();
@ -9,8 +10,8 @@ beforeEach(() => {
});
describe("dataProvider", () => {
localStorage.setItem("base_url", "http://localhost");
localStorage.setItem("access_token", "access_token");
storage.setItem("base_url", "http://localhost");
storage.setItem("access_token", "access_token");
it("fetches all users", async () => {
fetchMock.mockResponseOnce(

View file

@ -2,9 +2,11 @@ import { stringify } from "query-string";
import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
import storage from "../storage";
// Adds the access token to all requests
const jsonClient = (url: string, options: Options = {}) => {
const token = localStorage.getItem("access_token");
const token = storage.getItem("access_token");
console.log("httpClient " + url);
if (token != null) {
options.user = {
@ -16,7 +18,7 @@ const jsonClient = (url: string, options: Options = {}) => {
};
const mxcUrlToHttp = (mxcUrl: string) => {
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
const ret = re.exec(mxcUrl);
console.log("mxcClient " + ret);
@ -201,6 +203,21 @@ interface DestinationRoom {
stream_ordering: number;
}
export interface DeleteMediaParams {
before_ts: string;
size_gt: number;
keep_profiles: boolean;
}
export interface DeleteMediaResult {
deleted_media: Identifier[];
total: number;
}
export interface SynapseDataProvider extends DataProvider {
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
}
const resourceMap = {
users: {
path: "/_synapse/admin/v2/users",
@ -217,7 +234,7 @@ const resourceMap = {
data: "users",
total: json => json.total,
create: (data: RaRecord) => ({
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${localStorage.getItem("home_server")}`,
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${storage.getItem("home_server")}`,
body: data,
method: "PUT",
}),
@ -326,17 +343,7 @@ const resourceMap = {
data: "media",
total: json => json.total,
delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
}),
},
delete_media: {
delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
"home_server"
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
params.meta.size_gt
}&keep_profiles=${params.meta.keep_profiles}`,
method: "POST",
endpoint: `/_synapse/admin/v1/media/${storage.getItem("home_server")}/${params.id}`,
}),
},
protect_media: {
@ -353,11 +360,11 @@ const resourceMap = {
quarantine_media: {
map: (qm: UserMedia) => ({ id: qm.media_id }),
create: (params: UserMedia) => ({
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem("home_server")}/${params.media_id}`,
endpoint: `/_synapse/admin/v1/media/quarantine/${storage.getItem("home_server")}/${params.media_id}`,
method: "POST",
}),
delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem("home_server")}/${params.id}`,
endpoint: `/_synapse/admin/v1/media/unquarantine/${storage.getItem("home_server")}/${params.id}`,
method: "POST",
}),
},
@ -481,7 +488,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
}
}
const dataProvider: DataProvider = {
const dataProvider: SynapseDataProvider = {
getList: async (resource, params) => {
console.log("getList " + resource);
const { user_id, name, guests, deactivated, search_term, destination, valid } = params.filter;
@ -501,7 +508,7 @@ const dataProvider: DataProvider = {
order_by: field,
dir: getSearchOrder(order),
};
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -518,7 +525,7 @@ const dataProvider: DataProvider = {
getOne: async (resource, params) => {
console.log("getOne " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -530,7 +537,7 @@ const dataProvider: DataProvider = {
getMany: async (resource, params) => {
console.log("getMany " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
const res = resourceMap[resource];
@ -555,7 +562,7 @@ const dataProvider: DataProvider = {
dir: getSearchOrder(order),
};
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -572,7 +579,7 @@ const dataProvider: DataProvider = {
update: async (resource, params) => {
console.log("update " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -587,7 +594,7 @@ const dataProvider: DataProvider = {
updateMany: async (resource, params) => {
console.log("updateMany " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -604,7 +611,7 @@ const dataProvider: DataProvider = {
create: async (resource, params) => {
console.log("create " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -621,7 +628,7 @@ const dataProvider: DataProvider = {
createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
console.log("createMany " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -643,7 +650,7 @@ const dataProvider: DataProvider = {
delete: async (resource, params) => {
console.log("delete " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -668,7 +675,7 @@ const dataProvider: DataProvider = {
deleteMany: async (resource, params) => {
console.log("deleteMany " + resource);
const homeserver = localStorage.getItem("base_url");
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -700,6 +707,28 @@ const dataProvider: DataProvider = {
return { data: responses.map(({ json }) => json) };
}
},
// Custom methods (https://marmelab.com/react-admin/DataProviders.html#adding-custom-methods)
/**
* Delete media by date or size
*
* @link https://matrix-org.github.io/synapse/latest/admin_api/media_admin_api.html#delete-local-media-by-date-or-size
*
* @param before_ts Unix timestamp in milliseconds. Files that were last used before this timestamp will be deleted. It is the timestamp of last access, not the timestamp when the file was created.
* @param size_gt Size of the media in bytes. Files that are larger will be deleted.
* @param keep_profiles Switch to also delete files that are still used in image data (e.g user profile, room avatar). If false these files will be deleted.
* @returns
*/
deleteMedia: async ({ before_ts, size_gt = 0, keep_profiles = true }) => {
const homeserver = storage.getItem("home_server"); // TODO only required for synapse < 1.78.0
const endpoint = `/_synapse/admin/v1/media/${homeserver}/delete?before_ts=${before_ts}&size_gt=${size_gt}&keep_profiles=${keep_profiles}`;
const base_url = storage.getItem("base_url");
const endpoint_url = base_url + endpoint;
const { json } = await jsonClient(endpoint_url, { method: "POST" });
return json as DeleteMediaResult;
},
};
export default dataProvider;

View file

@ -1,5 +1,7 @@
import { fetchUtils } from "react-admin";
import storage from "../storage";
export const splitMxid = mxid => {
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
return re.exec(mxid)?.groups;
@ -15,8 +17,8 @@ export const isValidBaseUrl = baseUrl => /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{
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;
const response = await fetchUtils.fetchJson(wellKnownUrl, { method: "GET" });
return response.json["m.homeserver"].base_url;
} catch {
// if there is no .well-known entry, return the domain itself
return `https://${domain}`;
@ -53,7 +55,7 @@ export const getSupportedLoginFlows = async baseUrl => {
};
export const getMediaUrl = media_id => {
const baseUrl = localStorage.getItem("base_url");
const baseUrl = storage.getItem("base_url");
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
};
@ -62,7 +64,7 @@ export const getMediaUrl = media_id => {
* @returns full MXID as string
*/
export function generateRandomMxId(): string {
const homeserver = localStorage.getItem("home_server");
const homeserver = storage.getItem("home_server");
const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
.map(x => characters[x % characters.length])

View file

@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
base: "./",
plugins: [
react(),
vitePluginVersionMark({

1230
yarn.lock

File diff suppressed because it is too large Load diff