Compare commits

260 Commits

Author SHA1 Message Date
Aine
3b61e6e02c update yarn.lock 2025-06-02 17:50:09 +03:00
Aine
a4e4d37413 Merge pull request #589 from etkecc/dependabot/npm_and_yarn/react-hook-form-7.57.0
Bump react-hook-form from 7.56.4 to 7.57.0
2025-06-02 14:47:06 +00:00
Aine
4127954c1d Merge pull request #592 from etkecc/dependabot/npm_and_yarn/eslint-plugin-prettier-5.4.1
Bump eslint-plugin-prettier from 5.4.0 to 5.4.1
2025-06-02 14:39:47 +00:00
Aine
554d2cd132 Merge pull request #594 from etkecc/dependabot/npm_and_yarn/typescript-eslint-8.33.0
Bump typescript-eslint from 8.32.1 to 8.33.0
2025-06-02 14:39:34 +00:00
Aine
d2326a4cf3 Merge pull request #595 from etkecc/dependabot/npm_and_yarn/mui/material-7.1.1
Bump @mui/material from 7.1.0 to 7.1.1
2025-06-02 14:39:14 +00:00
Aine
7c0c9f922d Merge pull request #599 from etkecc/dependabot/npm_and_yarn/ra-language-russian-5.4.4
Bump ra-language-russian from 5.4.3 to 5.4.4
2025-06-02 14:38:55 +00:00
Aine
4dd3aa2375 Merge pull request #602 from etkecc/dependabot/npm_and_yarn/ra-language-french-5.8.3
Bump ra-language-french from 5.8.2 to 5.8.3
2025-06-02 14:38:42 +00:00
Aine
05d3740b80 Merge pull request #603 from etkecc/dependabot/npm_and_yarn/eslint-9.28.0
Bump eslint from 9.27.0 to 9.28.0
2025-06-02 14:38:29 +00:00
dependabot[bot]
e425efa26a Bump eslint from 9.27.0 to 9.28.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.27.0 to 9.28.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/v9.27.0...v9.28.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 14:19:59 +00:00
dependabot[bot]
cd8e35ba27 Bump ra-language-russian from 5.4.3 to 5.4.4
Bumps [ra-language-russian](https://github.com/Klucherev/ra-language-russian) from 5.4.3 to 5.4.4.
- [Commits](https://github.com/Klucherev/ra-language-russian/commits)

---
updated-dependencies:
- dependency-name: ra-language-russian
  dependency-version: 5.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 14:19:32 +00:00
dependabot[bot]
710cc57c4f Bump react-hook-form from 7.56.4 to 7.57.0
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.4 to 7.57.0.
- [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.56.4...v7.57.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 14:19:28 +00:00
dependabot[bot]
249bc4d0c2 Bump @mui/material from 7.1.0 to 7.1.1
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 7.1.0 to 7.1.1.
- [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/v7.1.1/packages/mui-material)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 14:18:34 +00:00
dependabot[bot]
aa6567a490 Bump typescript-eslint from 8.32.1 to 8.33.0
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.32.1 to 8.33.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.33.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 14:16:58 +00:00
Aine
f40cb4839e Merge pull request #590 from etkecc/dependabot/npm_and_yarn/types/node-22.15.29
Bump @types/node from 22.15.21 to 22.15.29
2025-06-02 14:15:34 +00:00
Aine
d5df57b7ec Merge pull request #586 from etkecc/dependabot/github_actions/docker/build-push-action-6.18.0
Bump docker/build-push-action from 6.17.0 to 6.18.0
2025-06-02 14:15:17 +00:00
Aine
5f89026db3 Merge pull request #600 from etkecc/dependabot/npm_and_yarn/types/react-19.1.6
Bump @types/react from 19.1.5 to 19.1.6
2025-06-02 14:14:49 +00:00
Aine
10aaef98b0 Merge pull request #597 from etkecc/dependabot/npm_and_yarn/tanstack/react-query-5.79.0
Bump @tanstack/react-query from 5.77.2 to 5.79.0
2025-06-02 14:14:31 +00:00
Aine
c323e170ac Merge pull request #598 from etkecc/dependabot/npm_and_yarn/react-admin-5.8.3
Bump react-admin from 5.8.2 to 5.8.3
2025-06-02 14:14:12 +00:00
Aine
797b924a62 Merge pull request #604 from etkecc/upstream-update
Upstream update
2025-06-02 14:13:38 +00:00
Aine
f1023ebc64 update from upstream (ref: e15411a04c ) 2025-06-02 17:12:49 +03:00
Dirk Klimpel
e15411a04c chore: bump react-admin to 5.8 and mui to 7.1 (#679) 2025-06-02 14:50:28 +02:00
Dirk Klimpel
3902dcd3d1 fix(Dockerfile): FROM and AS both in uppercase, FromAsCasing (#680) 2025-06-02 14:48:44 +02:00
dependabot[bot]
9c911abfb7 Bump ra-language-french from 5.8.2 to 5.8.3
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.8.2 to 5.8.3.
- [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/v5.8.2...v5.8.3)

---
updated-dependencies:
- dependency-name: ra-language-french
  dependency-version: 5.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 08:40:33 +00:00
dependabot[bot]
23c39cede8 Bump @types/react from 19.1.5 to 19.1.6
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.5 to 19.1.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 08:40:13 +00:00
dependabot[bot]
50e62f0b6c Bump react-admin from 5.8.2 to 5.8.3
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.8.2 to 5.8.3.
- [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/v5.8.2...v5.8.3)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-version: 5.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 08:38:14 +00:00
dependabot[bot]
d7d1d3c5b5 Bump @tanstack/react-query from 5.77.2 to 5.79.0
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.77.2 to 5.79.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.79.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.79.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 08:38:07 +00:00
dependabot[bot]
16d339c6f3 Bump eslint-plugin-prettier from 5.4.0 to 5.4.1
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.4.0 to 5.4.1.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.4.0...v5.4.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 08:35:23 +00:00
dependabot[bot]
9b0786f699 Bump @types/node from 22.15.21 to 22.15.29
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.21 to 22.15.29.
- [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-version: 22.15.29
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 08:34:49 +00:00
dependabot[bot]
fc1b20e95b Bump docker/build-push-action from 6.17.0 to 6.18.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.17.0 to 6.18.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](1dc7386353...263435318d)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 07:59:53 +00:00
Dirk Klimpel
ef8ae9b38f fix: Start if BASE_URL is not defined (#677) 2025-05-30 09:01:22 +02:00
Dirk Klimpel
7a286ad506 Downgrade react-router to 6.27.0 to fix import users (#678) 2025-05-30 08:58:39 +02:00
Aine
c82d8653fd update deps 2025-05-26 16:19:10 +03:00
Aine
949c2e8809 Merge pull request #578 from etkecc/dependabot/npm_and_yarn/tanstack/react-query-5.77.1
Bump @tanstack/react-query from 5.76.1 to 5.77.1
2025-05-26 12:23:14 +00:00
Aine
20a1cec358 Merge pull request #579 from etkecc/dependabot/npm_and_yarn/react-router-dom-7.6.1
Bump react-router-dom from 7.6.0 to 7.6.1
2025-05-26 12:23:03 +00:00
Aine
21240b8ada Merge pull request #580 from etkecc/dependabot/npm_and_yarn/vitejs/plugin-react-4.5.0
Bump @vitejs/plugin-react from 4.4.1 to 4.5.0
2025-05-26 12:22:48 +00:00
Aine
f218363e97 Merge pull request #576 from etkecc/dependabot/npm_and_yarn/papaparse-5.5.3
Bump papaparse from 5.5.2 to 5.5.3
2025-05-26 12:22:28 +00:00
Aine
1a5fd713da Merge pull request #581 from etkecc/dependabot/npm_and_yarn/types/lodash-4.17.17
Bump @types/lodash from 4.17.16 to 4.17.17
2025-05-26 12:22:05 +00:00
dependabot[bot]
3d6202f335 Bump papaparse from 5.5.2 to 5.5.3
Bumps [papaparse](https://github.com/mholt/PapaParse) from 5.5.2 to 5.5.3.
- [Release notes](https://github.com/mholt/PapaParse/releases)
- [Commits](https://github.com/mholt/PapaParse/commits)

---
updated-dependencies:
- dependency-name: papaparse
  dependency-version: 5.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 12:20:07 +00:00
dependabot[bot]
9161e26d3a Bump @vitejs/plugin-react from 4.4.1 to 4.5.0
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.4.1 to 4.5.0.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.5.0/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 12:20:03 +00:00
dependabot[bot]
b00d810fae Bump @types/lodash from 4.17.16 to 4.17.17
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.16 to 4.17.17.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-version: 4.17.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 12:19:55 +00:00
Aine
9a4b995d09 Merge pull request #577 from etkecc/dependabot/npm_and_yarn/types/react-19.1.5
Bump @types/react from 19.1.4 to 19.1.5
2025-05-26 12:17:42 +00:00
Aine
c97405b622 Merge pull request #575 from etkecc/dependabot/npm_and_yarn/types/node-22.15.21
Bump @types/node from 22.15.19 to 22.15.21
2025-05-26 12:17:25 +00:00
dependabot[bot]
b5ebd9173e Bump react-router-dom from 7.6.0 to 7.6.1
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.6.0 to 7.6.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.6.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 09:53:56 +00:00
dependabot[bot]
a1ff4dc9ef Bump @tanstack/react-query from 5.76.1 to 5.77.1
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.76.1 to 5.77.1.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.77.1/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.77.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 09:52:55 +00:00
dependabot[bot]
2298fee054 Bump @types/react from 19.1.4 to 19.1.5
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.4 to 19.1.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 09:52:50 +00:00
dependabot[bot]
e236126a30 Bump @types/node from 22.15.19 to 22.15.21
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.19 to 22.15.21.
- [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-version: 22.15.21
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 09:52:22 +00:00
Aine
7da5265575 update version in package.json 2025-05-20 22:05:51 +03:00
Aine
62fc97a417 Sync with upstream 2025-05-20 22:02:51 +03:00
dependabot[bot]
fa3f2437a3 Bump softprops/action-gh-release from 2.0.8 to 2.2.2 (#660)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.8 to 2.2.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](c062e08bd5...da05d55257)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.2.2
  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>
2025-05-20 17:35:55 +02:00
dependabot[bot]
8dc5238fcb Bump JamesIves/github-pages-deploy-action from 4.6.8 to 4.7.3 (#659)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.8 to 4.7.3.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.8...v4.7.3)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-version: 4.7.3
  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>
2025-05-20 17:35:45 +02:00
dependabot[bot]
238350b940 Bump typescript-eslint from 8.32.0 to 8.32.1 (#661)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.32.0 to 8.32.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.32.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.32.1
  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>
2025-05-20 17:35:26 +02:00
dependabot[bot]
99bf7b1889 Bump vite from 5.4.19 to 6.3.5 (#664)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.19 to 6.3.5.
- [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/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.5
  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>
2025-05-20 17:30:15 +02:00
dependabot[bot]
d72c91644d Bump ra-language-english from 5.2.3 to 5.8.2 (#667)
Bumps [ra-language-english](https://github.com/marmelab/react-admin) from 5.2.3 to 5.8.2.
- [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/v5.2.3...v5.8.2)

---
updated-dependencies:
- dependency-name: ra-language-english
  dependency-version: 5.8.2
  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>
2025-05-20 17:09:09 +02:00
Aine
5bec4c71ea update yarn.lock 2025-05-19 12:34:38 +03:00
Aine
245dadc470 Merge pull request #562 from etkecc/dependabot/npm_and_yarn/react-hook-form-7.56.4
Bump react-hook-form from 7.56.3 to 7.56.4
2025-05-19 09:30:43 +00:00
Aine
08cde13fd4 Merge pull request #561 from etkecc/dependabot/npm_and_yarn/types/react-19.1.4
Bump @types/react from 19.1.3 to 19.1.4
2025-05-19 09:30:30 +00:00
Aine
765456d233 Merge pull request #566 from etkecc/dependabot/npm_and_yarn/types/node-22.15.19
Bump @types/node from 22.15.17 to 22.15.19
2025-05-19 09:30:17 +00:00
Aine
b6bc4a2d43 Merge pull request #558 from etkecc/dependabot/npm_and_yarn/ts-jest-29.3.4
Bump ts-jest from 29.3.2 to 29.3.4
2025-05-19 09:29:29 +00:00
Aine
851478bbcb Merge pull request #563 from etkecc/dependabot/npm_and_yarn/tanstack/react-query-5.76.1
Bump @tanstack/react-query from 5.75.7 to 5.76.1
2025-05-19 09:29:11 +00:00
Aine
ff8e43631a Merge pull request #565 from etkecc/dependabot/npm_and_yarn/eslint-9.27.0
Bump eslint from 9.26.0 to 9.27.0
2025-05-19 09:28:54 +00:00
Aine
7b8a43b835 Merge pull request #567 from etkecc/dependabot/npm_and_yarn/typescript-eslint-8.32.1
Bump typescript-eslint from 8.32.0 to 8.32.1
2025-05-19 09:28:36 +00:00
Aine
bc0c186242 Merge pull request #572 from etkecc/dependabot/npm_and_yarn/ra-language-french-5.8.2
Bump ra-language-french from 5.8.0 to 5.8.2
2025-05-19 09:28:20 +00:00
dependabot[bot]
d5b9981bf0 Bump eslint from 9.26.0 to 9.27.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.26.0 to 9.27.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/v9.26.0...v9.27.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 09:24:01 +00:00
dependabot[bot]
7a8977a00c Bump @tanstack/react-query from 5.75.7 to 5.76.1
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.75.7 to 5.76.1.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.76.1/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.76.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 09:23:45 +00:00
dependabot[bot]
5297e56eca Bump react-hook-form from 7.56.3 to 7.56.4
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.3 to 7.56.4.
- [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.56.3...v7.56.4)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.56.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 09:23:43 +00:00
Aine
82ff610b89 Merge pull request #564 from etkecc/dependabot/npm_and_yarn/react-admin-5.8.2
Bump react-admin from 5.8.0 to 5.8.2
2025-05-19 09:21:18 +00:00
Aine
263f25303e Merge pull request #573 from etkecc/dependabot/github_actions/docker/build-push-action-6.17.0
Bump docker/build-push-action from 6.16.0 to 6.17.0
2025-05-19 09:20:56 +00:00
dependabot[bot]
2aa7cdcb97 Bump docker/build-push-action from 6.16.0 to 6.17.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.16.0 to 6.17.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](14487ce63c...1dc7386353)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 07:49:35 +00:00
dependabot[bot]
e3e8f028f4 Bump ra-language-french from 5.8.0 to 5.8.2
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.8.0 to 5.8.2.
- [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/v5.8.0...v5.8.2)

---
updated-dependencies:
- dependency-name: ra-language-french
  dependency-version: 5.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 07:41:47 +00:00
dependabot[bot]
f18523e0f2 Bump typescript-eslint from 8.32.0 to 8.32.1
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.32.0 to 8.32.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.32.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.32.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 07:40:42 +00:00
dependabot[bot]
3533cbed93 Bump @types/node from 22.15.17 to 22.15.19
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.17 to 22.15.19.
- [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-version: 22.15.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 07:40:40 +00:00
dependabot[bot]
bf1cec2796 Bump react-admin from 5.8.0 to 5.8.2
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.8.0 to 5.8.2.
- [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/v5.8.0...v5.8.2)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-version: 5.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 07:39:55 +00:00
dependabot[bot]
7005d08c7e Bump @types/react from 19.1.3 to 19.1.4
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.3 to 19.1.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 07:39:36 +00:00
dependabot[bot]
e15b45bcf9 Bump ts-jest from 29.3.2 to 29.3.4
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.3.2 to 29.3.4.
- [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.3.2...v29.3.4)

---
updated-dependencies:
- dependency-name: ts-jest
  dependency-version: 29.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 07:39:05 +00:00
Aine
603737b2ad Merge pull request #545 from etkecc/dependabot/npm_and_yarn/mui/utils-7.1.0
Bump @mui/utils from 6.4.9 to 7.1.0
2025-05-12 09:21:15 +00:00
Aine
eebc7a4fab fix tests 2025-05-12 12:20:15 +03:00
Aine
3725b01fe5 Merge pull request #554 from etkecc/dependabot/npm_and_yarn/react-router-dom-7.6.0
Bump react-router-dom from 7.5.3 to 7.6.0
2025-05-12 09:02:22 +00:00
dependabot[bot]
6c41aab463 Bump react-router-dom from 7.5.3 to 7.6.0
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.5.3 to 7.6.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.6.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 09:01:34 +00:00
Aine
e8987607ff Merge pull request #548 from etkecc/dependabot/npm_and_yarn/react-router-7.6.0
Bump react-router from 7.5.3 to 7.6.0
2025-05-12 08:58:38 +00:00
Aine
9c440024bb Merge pull request #549 from etkecc/dependabot/npm_and_yarn/react-hook-form-7.56.3
Bump react-hook-form from 7.56.2 to 7.56.3
2025-05-12 08:58:20 +00:00
Aine
aa10df711e Merge pull request #550 from etkecc/dependabot/npm_and_yarn/typescript-eslint-8.32.0
Bump typescript-eslint from 8.31.1 to 8.32.0
2025-05-12 08:56:15 +00:00
Aine
51aca16e2f Merge pull request #551 from etkecc/dependabot/npm_and_yarn/eslint-plugin-prettier-5.4.0
Bump eslint-plugin-prettier from 5.3.1 to 5.4.0
2025-05-12 08:55:36 +00:00
Aine
4f1b9c2bfa Merge pull request #546 from etkecc/dependabot/npm_and_yarn/eslint-config-prettier-10.1.5
Bump eslint-config-prettier from 10.1.2 to 10.1.5
2025-05-12 08:55:23 +00:00
Aine
a801957194 Merge pull request #547 from etkecc/dependabot/npm_and_yarn/types/node-22.15.17
Bump @types/node from 22.15.3 to 22.15.17
2025-05-12 08:55:10 +00:00
dependabot[bot]
8f527eeb85 Bump @types/node from 22.15.3 to 22.15.17
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.3 to 22.15.17.
- [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-version: 22.15.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 08:44:38 +00:00
dependabot[bot]
92632f81f8 Bump typescript-eslint from 8.31.1 to 8.32.0
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.31.1 to 8.32.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.32.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.32.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 08:44:17 +00:00
Aine
e76809fb37 Merge pull request #553 from etkecc/dependabot/npm_and_yarn/types/papaparse-5.3.16
Bump @types/papaparse from 5.3.15 to 5.3.16
2025-05-12 08:41:52 +00:00
dependabot[bot]
03d142a47c Bump @types/papaparse from 5.3.15 to 5.3.16
Bumps [@types/papaparse](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/papaparse) from 5.3.15 to 5.3.16.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/papaparse)

---
updated-dependencies:
- dependency-name: "@types/papaparse"
  dependency-version: 5.3.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 08:39:12 +00:00
dependabot[bot]
f589427b3c Bump @mui/utils from 6.4.9 to 7.1.0
Bumps [@mui/utils](https://github.com/mui/material-ui/tree/HEAD/packages/mui-utils) from 6.4.9 to 7.1.0.
- [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/v7.1.0/packages/mui-utils)

---
updated-dependencies:
- dependency-name: "@mui/utils"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 08:39:02 +00:00
Aine
16d78b577e Merge pull request #555 from etkecc/dependabot/npm_and_yarn/tanstack/react-query-5.75.7
Bump @tanstack/react-query from 5.75.4 to 5.75.7
2025-05-12 08:37:00 +00:00
Aine
649450f7ee Merge pull request #556 from etkecc/dependabot/npm_and_yarn/types/react-19.1.3
Bump @types/react from 19.1.2 to 19.1.3
2025-05-12 08:36:45 +00:00
Aine
a64705c28c Merge pull request #552 from etkecc/dependabot/npm_and_yarn/vite-6.3.5
Bump vite from 6.3.4 to 6.3.5
2025-05-12 08:36:29 +00:00
dependabot[bot]
5d775ca08b Bump @types/react from 19.1.2 to 19.1.3
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.2 to 19.1.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:45:35 +00:00
dependabot[bot]
b2289e5776 Bump @tanstack/react-query from 5.75.4 to 5.75.7
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.75.4 to 5.75.7.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.75.7/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.75.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:45:26 +00:00
dependabot[bot]
607abc003b Bump vite from 6.3.4 to 6.3.5
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.4 to 6.3.5.
- [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/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:44:55 +00:00
dependabot[bot]
210623b39e Bump eslint-plugin-prettier from 5.3.1 to 5.4.0
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.3.1...v5.4.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:44:41 +00:00
dependabot[bot]
8c72dbbd9a Bump react-hook-form from 7.56.2 to 7.56.3
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.2 to 7.56.3.
- [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.56.2...v7.56.3)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.56.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:44:13 +00:00
dependabot[bot]
5779ce6301 Bump react-router from 7.5.3 to 7.6.0
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.5.3 to 7.6.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.6.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:44:04 +00:00
dependabot[bot]
664573d65e Bump eslint-config-prettier from 10.1.2 to 10.1.5
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 10.1.2 to 10.1.5.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v10.1.2...v10.1.5)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-version: 10.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 07:43:08 +00:00
Aine
cfcc3f5a73 set guests=false by default in users list 2025-05-12 10:33:52 +03:00
Aine
8c303b7940 update readme 2025-05-09 23:25:58 +03:00
Aine
785be9187d add docker hub image, fixes #541 2025-05-09 23:21:12 +03:00
Aine
69b04736d6 Merge pull request #543 from etkecc/update-from-upstream
Backport history from Awesome-Technologies/synapse-admin v0.10.4
2025-05-09 19:53:32 +00:00
Aine
4051631244 Merge branch 'master' into update-from-upstream 2025-05-09 22:51:45 +03:00
Dirk Klimpel
e8e28b5df1 Upgrade to react-admin v5 (#633)
* bump dependencies
* replace react-query with @tanstack/react-query
* remove darkTheme
* remove obsolete fullWidth
* fix pagination
* fix language for ra.navigation
* fix 'record' is possibly 'undefined'
* add missing @mui/utils
* bump yarn to 4.4.x
2025-05-09 20:53:41 +02:00
rkfg
d5c10b6e02 Federation page improvements (#583)
* Hide zero last retry timestamps in federation list
* Federation dark theme improvements
2025-05-09 17:54:46 +02:00
Huw Carpenter
3085b9ffa0 Add ability to toggle whether to show locked users (#573) 2025-05-09 17:52:55 +02:00
Patrick Kranz
b2a3fb0f87 Update fetch URL to use BASE_URL for config.json (#652)
Fixes #644.
2025-05-09 17:44:41 +02:00
milkomeda
1e8b4cc885 Fixed detection of missingFields in verifyCsv function of CSV import (#650)
Fixes #600, #552, #188.
2025-05-09 17:43:13 +02:00
Manuel Stahl
4d1a9cc147 Add content read permission to build-test workflow
Change-Id: I5dc911e79e7d0d8fbd88d6a9a686fc10f11a97cd
2025-05-09 17:36:42 +02:00
Manuel Stahl
1b8b702270 Add content write permission to ghpage workflow
Change-Id: I13a10316400dd80a8149e6e8a265c13a51aee652
2025-05-09 17:27:55 +02:00
Dirk Klimpel
61c32fb473 Add identifier when login with password (#601)
* Add identifier when login with password
* add identifier to unit tests
2025-05-09 17:24:23 +02:00
dependabot[bot]
ad876bb790 Bump the npm_and_yarn group with 3 updates (#656)
Bumps the npm_and_yarn group with 3 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) and [dompurify](https://github.com/cure53/DOMPurify).


Updates `vite` from 5.4.9 to 5.4.19
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)

Updates `@babel/runtime` from 7.24.4 to 7.27.1
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.1/packages/babel-runtime)

Updates `dompurify` from 2.5.0 to 2.5.8
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.5.0...2.5.8)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@babel/runtime"
  dependency-version: 7.27.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 2.5.8
  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>
2025-05-09 17:04:48 +02:00
Aine
b941b4c787 Merge pull request #540 from etkecc/update-from-upstream
Update from upstream
2025-05-09 11:04:44 +00:00
Aine
45b5213c8c Merge branch 'master' 2025-05-09 14:03:04 +03:00
dependabot[bot]
2524848dae Bump typescript-eslint from 7.16.1 to 8.11.0 (#638)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 7.16.1 to 8.11.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.11.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  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>
2025-05-09 12:03:03 +02:00
dependabot[bot]
669c1f3079 Bump JamesIves/github-pages-deploy-action from 4.6.3 to 4.6.8 (#626)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.3 to 4.6.8.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.3...v4.6.8)

---
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>
2025-05-09 11:17:06 +02:00
dependabot[bot]
590f673167 Bump eslint-plugin-unused-imports from 3.2.0 to 4.1.4 (#620)
Bumps [eslint-plugin-unused-imports](https://github.com/sweepline/eslint-plugin-unused-imports) from 3.2.0 to 4.1.4.
- [Commits](https://github.com/sweepline/eslint-plugin-unused-imports/commits/v4.1.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-unused-imports
  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>
2025-05-09 11:16:23 +02:00
dependabot[bot]
307793f000 Bump @testing-library/dom from 10.0.0 to 10.4.0 (#585)
Bumps [@testing-library/dom](https://github.com/testing-library/dom-testing-library) from 10.0.0 to 10.4.0.
- [Release notes](https://github.com/testing-library/dom-testing-library/releases)
- [Changelog](https://github.com/testing-library/dom-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/dom-testing-library/compare/v10.0.0...v10.4.0)

---
updated-dependencies:
- dependency-name: "@testing-library/dom"
  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>
2025-05-09 11:14:41 +02:00
dependabot[bot]
96f549fe42 Bump micromatch from 4.0.5 to 4.0.8 in the npm_and_yarn group (#610)
Bumps the npm_and_yarn group with 1 update: [micromatch](https://github.com/micromatch/micromatch).


Updates `micromatch` from 4.0.5 to 4.0.8
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  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>
2025-05-09 11:14:29 +02:00
dependabot[bot]
3de4332477 Bump vite from 5.3.4 to 5.4.9 (#634)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.3.4 to 5.4.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.9/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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>
2025-05-09 11:13:22 +02:00
Aine
3167b55430 lint fix 2025-05-06 11:31:43 +03:00
dependabot[bot]
cc44b5581f Bump react-router-dom from 7.5.2 to 7.5.3 (#528)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.5.2 to 7.5.3.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.5.3/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.5.3
  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>
2025-05-06 11:27:16 +03:00
dependabot[bot]
30615eaf02 Bump @typescript-eslint/parser from 8.31.0 to 8.31.1 (#534)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.31.0 to 8.31.1.
- [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/v8.31.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.31.1
  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>
2025-05-06 11:18:49 +03:00
dependabot[bot]
4e88f03aab Bump react-hook-form from 7.56.1 to 7.56.2 (#531)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.1 to 7.56.2.
- [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.56.1...v7.56.2)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.56.2
  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>
2025-05-06 11:06:05 +03:00
dependabot[bot]
9bc2788560 Bump @typescript-eslint/eslint-plugin from 8.31.0 to 8.31.1 (#536)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.31.0 to 8.31.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.31.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.31.1
  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>
2025-05-06 11:05:38 +03:00
dependabot[bot]
a65eb56098 Bump @tanstack/react-query from 5.74.7 to 5.75.2 (#530)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.74.7 to 5.75.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.75.2/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.75.2
  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>
2025-05-06 10:48:59 +03:00
dependabot[bot]
1c95995478 Bump eslint-plugin-prettier from 5.2.6 to 5.3.1 (#539)
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.2.6 to 5.3.1.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.2.6...v5.3.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.3.1
  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>
2025-05-06 10:48:30 +03:00
dependabot[bot]
d4c07980d5 Bump typescript-eslint from 8.31.0 to 8.31.1 (#526)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.31.0 to 8.31.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.31.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.31.1
  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>
2025-05-06 10:48:09 +03:00
dependabot[bot]
96946186c9 Bump eslint from 9.25.1 to 9.26.0 (#527)
Bumps [eslint](https://github.com/eslint/eslint) from 9.25.1 to 9.26.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/v9.25.1...v9.26.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.26.0
  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>
2025-05-06 10:47:53 +03:00
dependabot[bot]
742106071d Bump ra-language-french from 5.7.4 to 5.8.0 (#538)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.7.4 to 5.8.0.
- [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/v5.7.4...v5.8.0)

---
updated-dependencies:
- dependency-name: ra-language-french
  dependency-version: 5.8.0
  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>
2025-05-06 10:44:12 +03:00
dependabot[bot]
ce79eb330c Bump react-admin from 5.7.4 to 5.8.0 (#533)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.7.4 to 5.8.0.
- [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/v5.7.4...v5.8.0)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-version: 5.8.0
  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>
2025-05-06 10:43:54 +03:00
dependabot[bot]
04d07f5eb3 Bump vite from 6.3.3 to 6.3.4 in the npm_and_yarn group (#524)
Bumps the npm_and_yarn group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.3.3 to 6.3.4
- [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/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
  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>
2025-05-01 08:38:00 +03:00
Aine
de7930f472 nope, missed one 2025-04-29 18:02:36 +03:00
Aine
2a66988807 pin all 3rdparty actions 2025-04-29 18:00:17 +03:00
Aine
5bfa98d3a1 use github action commit hash 2025-04-29 17:54:03 +03:00
dependabot[bot]
ea41f929a6 Bump @tanstack/react-query from 5.74.4 to 5.74.7 (#523)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.74.4 to 5.74.7.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.74.7/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.74.7
  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>
2025-04-29 17:46:30 +03:00
dependabot[bot]
037dc09da6 Bump @types/node from 22.15.1 to 22.15.3 (#517)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.1 to 22.15.3.
- [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-version: 22.15.3
  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>
2025-04-29 17:46:16 +03:00
dependabot[bot]
b9ad596f38 Bump ra-language-french from 5.7.3 to 5.7.4 (#520)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.7.3 to 5.7.4.
- [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/v5.7.3...v5.7.4)

---
updated-dependencies:
- dependency-name: ra-language-french
  dependency-version: 5.7.4
  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>
2025-04-29 17:45:54 +03:00
dependabot[bot]
c83e61af2c Bump react-admin from 5.7.3 to 5.7.4 (#522)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.7.3 to 5.7.4.
- [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/v5.7.3...v5.7.4)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-version: 5.7.4
  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>
2025-04-29 17:45:32 +03:00
Borislav Pantaleev
2752716497 Remove getPermissions to fix redirect loop (#516) 2025-04-26 05:22:51 +00:00
Aine
64f9d8b48c update deps 2025-04-25 09:15:28 +03:00
dependabot[bot]
81fb008e59 Bump react-router-dom from 7.5.0 to 7.5.1 (#498)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.5.0 to 7.5.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.5.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.5.1
  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>
2025-04-23 14:29:45 +03:00
dependabot[bot]
cede7c611b Bump react-router from 7.5.0 to 7.5.1 (#512)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.5.0 to 7.5.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.5.1/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.5.1
  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>
Co-authored-by: Aine <97398200+aine-etke@users.noreply.github.com>
2025-04-23 14:23:45 +03:00
dependabot[bot]
8d66dca068 Bump react-hook-form from 7.55.0 to 7.56.0 (#500)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.55.0 to 7.56.0.
- [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.55.0...v7.56.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.56.0
  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>
2025-04-23 14:22:27 +03:00
dependabot[bot]
26620966ab Bump @types/react from 19.1.1 to 19.1.2 (#506)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.1 to 19.1.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.2
  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>
2025-04-23 14:22:01 +03:00
dependabot[bot]
29cb61bf47 Bump eslint from 9.24.0 to 9.25.0 (#505)
Bumps [eslint](https://github.com/eslint/eslint) from 9.24.0 to 9.25.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/v9.24.0...v9.25.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.25.0
  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>
2025-04-23 14:21:41 +03:00
dependabot[bot]
6b5863e257 Bump typescript-eslint from 8.29.1 to 8.30.1 (#510)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.29.1 to 8.30.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.30.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.30.1
  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>
2025-04-23 14:18:57 +03:00
dependabot[bot]
5a2c6ec326 Bump @eslint/js from 9.24.0 to 9.25.0 (#497)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.24.0 to 9.25.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.25.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.25.0
  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>
2025-04-23 14:16:43 +03:00
dependabot[bot]
f52bce55a8 Bump @tanstack/react-query from 5.74.0 to 5.74.4 (#501)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.74.0 to 5.74.4.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.74.4/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.74.4
  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>
2025-04-23 14:16:24 +03:00
dependabot[bot]
76d3f42017 Bump @vitejs/plugin-react from 4.3.4 to 4.4.1 (#508)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.4 to 4.4.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.4.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 4.4.1
  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>
2025-04-23 14:15:55 +03:00
dependabot[bot]
5bfd330f08 Bump ra-language-french from 5.7.2 to 5.7.3 (#507)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.7.2 to 5.7.3.
- [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/v5.7.2...v5.7.3)

---
updated-dependencies:
- dependency-name: ra-language-french
  dependency-version: 5.7.3
  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>
2025-04-23 14:15:35 +03:00
dependabot[bot]
6d8ea12c5a Bump vite from 6.2.6 to 6.3.2 (#502)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.2.
- [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/v6.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.2
  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>
2025-04-23 14:15:21 +03:00
dependabot[bot]
399b37f57e Bump react-admin from 5.7.2 to 5.7.3 (#499)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.7.2 to 5.7.3.
- [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/v5.7.2...v5.7.3)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-version: 5.7.3
  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>
2025-04-23 13:35:05 +03:00
Aine
e6f3d15b45 update docs 2025-04-15 16:10:10 +03:00
Aine
75e4cfa0c4 add contributing and security guidelines 2025-04-15 13:10:16 +03:00
Aine
ce3337c46f Update issue templates 2025-04-15 13:08:38 +03:00
Aine
54b67b3b48 ensure MXC URLs properly parsed when media IDs contain hypens, fixes #494 2025-04-15 11:17:12 +03:00
dependabot[bot]
61013e1875 Bump typescript-eslint from 8.29.0 to 8.29.1 (#492)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.29.0 to 8.29.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.29.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.29.1
  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>
2025-04-14 10:50:53 +03:00
dependabot[bot]
52d0d80816 Bump ts-jest from 29.3.1 to 29.3.2 (#484)
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.3.1 to 29.3.2.
- [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.3.1...v29.3.2)

---
updated-dependencies:
- dependency-name: ts-jest
  dependency-version: 29.3.2
  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>
2025-04-14 10:49:25 +03:00
dependabot[bot]
1fb58d3694 Bump @tanstack/react-query from 5.71.10 to 5.74.0 (#482)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.71.10 to 5.74.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.74.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.74.0
  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>
2025-04-14 10:48:11 +03:00
dependabot[bot]
8600154553 Bump eslint-config-prettier from 10.1.1 to 10.1.2 (#489)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 10.1.1 to 10.1.2.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v10.1.1...v10.1.2)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-version: 10.1.2
  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>
2025-04-14 10:47:59 +03:00
dependabot[bot]
dda348e270 Bump @typescript-eslint/eslint-plugin from 8.29.0 to 8.29.1 (#490)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.29.0 to 8.29.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.29.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.29.1
  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>
2025-04-14 10:47:41 +03:00
dependabot[bot]
d2fd4f2114 Bump @types/node from 22.14.0 to 22.14.1 (#491)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.14.0 to 22.14.1.
- [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-version: 22.14.1
  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>
2025-04-14 10:44:51 +03:00
dependabot[bot]
6f5dd89056 Bump @types/react from 19.1.0 to 19.1.1 (#487)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.0 to 19.1.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.1
  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>
2025-04-14 10:44:35 +03:00
dependabot[bot]
0a00e0a474 Bump ra-language-french from 5.7.1 to 5.7.2 (#485)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.7.1 to 5.7.2.
- [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/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: ra-language-french
  dependency-version: 5.7.2
  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>
2025-04-14 10:44:22 +03:00
dependabot[bot]
e63b2f54b7 Bump react-admin from 5.7.1 to 5.7.2 (#483)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.7.1 to 5.7.2.
- [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/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-version: 5.7.2
  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>
2025-04-14 10:41:27 +03:00
dependabot[bot]
85f1d6d3ed Bump vite from 6.2.5 to 6.2.6 in the npm_and_yarn group (#479)
Bumps the npm_and_yarn group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.5 to 6.2.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
  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>
2025-04-12 20:37:22 +03:00
Borislav Pantaleev
107d9f4e39 Fix long display names for users and rooms with word-break (#480) 2025-04-11 22:49:27 +03:00
dependabot[bot]
4f488e5a39 Bump react-router-dom from 7.4.1 to 7.5.0 (#467)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.4.1 to 7.5.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.5.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.5.0
  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>
2025-04-11 12:58:37 +03:00
dependabot[bot]
5a98844f1f Bump react-router from 7.4.1 to 7.5.0 (#473)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.4.1 to 7.5.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.5.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.5.0
  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>
2025-04-11 12:56:25 +03:00
dependabot[bot]
cabb9700a8 Bump react-test-renderer from 19.0.0 to 19.1.0 (#470)
Bumps [react-test-renderer](https://github.com/facebook/react/tree/HEAD/packages/react-test-renderer) from 19.0.0 to 19.1.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react-test-renderer)

---
updated-dependencies:
- dependency-name: react-test-renderer
  dependency-version: 19.1.0
  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>
2025-04-11 12:54:35 +03:00
dependabot[bot]
d02324d14c Bump @testing-library/react from 16.2.0 to 16.3.0 (#468)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 16.2.0 to 16.3.0.
- [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/v16.2.0...v16.3.0)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-version: 16.3.0
  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>
2025-04-11 12:52:44 +03:00
dependabot[bot]
929d638f45 Bump eslint-plugin-prettier from 5.2.5 to 5.2.6 (#460)
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.2.5 to 5.2.6.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.2.5...v5.2.6)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.2.6
  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>
2025-04-11 12:52:21 +03:00
dependabot[bot]
19896cb7cf Bump @mui/utils from 6.4.8 to 6.4.9 (#458)
Bumps [@mui/utils](https://github.com/mui/material-ui/tree/HEAD/packages/mui-utils) from 6.4.8 to 6.4.9.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v6.4.9/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v6.4.9/packages/mui-utils)

---
updated-dependencies:
- dependency-name: "@mui/utils"
  dependency-version: 6.4.9
  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>
2025-04-11 12:52:00 +03:00
dependabot[bot]
e3d093c960 Bump ts-jest-mock-import-meta from 1.2.1 to 1.3.0 (#463)
Bumps [ts-jest-mock-import-meta](https://github.com/ThomZz/ts-jest-mock-import-meta) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/ThomZz/ts-jest-mock-import-meta/releases)
- [Commits](https://github.com/ThomZz/ts-jest-mock-import-meta/compare/v1.2.1...v1.3.0)

---
updated-dependencies:
- dependency-name: ts-jest-mock-import-meta
  dependency-version: 1.3.0
  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>
2025-04-11 12:51:34 +03:00
dependabot[bot]
59b4e8c2af Bump ts-jest from 29.3.0 to 29.3.1 (#466)
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.3.0 to 29.3.1.
- [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.3.0...v29.3.1)

---
updated-dependencies:
- dependency-name: ts-jest
  dependency-version: 29.3.1
  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>
2025-04-11 12:51:02 +03:00
dependabot[bot]
e08d3b6828 Bump @types/node from 22.13.14 to 22.14.0 (#469)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.13.14 to 22.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-version: 22.14.0
  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>
2025-04-11 12:50:31 +03:00
dependabot[bot]
0f03011def Bump @types/react from 19.0.12 to 19.1.0 (#474)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.0.12 to 19.1.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.0
  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>
2025-04-11 12:50:07 +03:00
dependabot[bot]
6bdc9e35c4 Bump typescript from 5.8.2 to 5.8.3 (#475)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.8.2 to 5.8.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.8.3
  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>
2025-04-11 12:49:46 +03:00
dependabot[bot]
09bd471388 Bump @tanstack/react-query from 5.71.1 to 5.71.10 (#476)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.71.1 to 5.71.10.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.71.10/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.71.10
  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>
2025-04-11 12:49:19 +03:00
dependabot[bot]
4e90aac326 Bump ra-language-french from 5.7.0 to 5.7.1 (#464)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.7.0 to 5.7.1.
- [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/v5.7.0...v5.7.1)

---
updated-dependencies:
- dependency-name: ra-language-french
  dependency-version: 5.7.1
  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>
2025-04-11 12:49:03 +03:00
dependabot[bot]
ad40ee70e5 Bump eslint from 9.23.0 to 9.24.0 (#471)
Bumps [eslint](https://github.com/eslint/eslint) from 9.23.0 to 9.24.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/v9.23.0...v9.24.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.24.0
  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>
2025-04-11 12:48:22 +03:00
dependabot[bot]
b07050faf5 Bump react-admin from 5.7.0 to 5.7.1 (#472)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.7.0 to 5.7.1.
- [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/v5.7.0...v5.7.1)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-version: 5.7.1
  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>
2025-04-11 12:48:05 +03:00
Borislav Pantaleev
0832c43d76 (etke.cc-exclusive) Server Actions page (#457)
* WIP: add scheduler commands

* refactor scheduler commands page

* WIP on CRUD for ScheduledCommands

* more refactoring, finish CRUD On scheduled pages

* Add info text about Scheduler service

* Finish recurring commands add/edit

* add more texts

* fix server status behavior on not-loaded-yet state; adjust texts; lint fixes

* add link to the help pages in the commands palette

* Move Commands Panel to ServerSchedulesPage

* Rename Server Schedules to Server Actions

* more texts, a bit changed visual of the actions page, lint fix

* add docs

* fix tests

* Add UTC label to scheduled command create/edit
2025-04-11 12:41:47 +03:00
Aine
e2d3c0792b ensure users MXID is encoded in the datagrid links and create/edit form redirects, fixes #477 2025-04-08 18:29:06 +03:00
Aine
bac962c127 remove unused eslint plugin, run eslint --fix, rollback node memory workaround in ci 2025-04-05 21:37:31 +03:00
Aine
738685c599 increase max-old-space-size 2025-04-05 21:16:02 +03:00
Aine
d781268019 set --max-old-space-size in CI 2025-04-05 21:14:02 +03:00
Aine
218f0ba03c Configurable CORS credentials (#456)
* Configurable CORS credentials

* update readme
2025-04-05 21:08:29 +03:00
dependabot[bot]
f4084969b6 Bump vite from 6.2.4 to 6.2.5 in the npm_and_yarn group (#455)
Bumps the npm_and_yarn group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.4 to 6.2.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.5
  dependency-type: direct:development
  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>
2025-04-04 18:03:14 +00:00
Aine
a9fefa238c fix deps 2025-03-31 22:17:56 +03:00
dependabot[bot]
75a408a804 Bump react from 19.0.0 to 19.1.0 (#448)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.0.0 to 19.1.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react)

---
updated-dependencies:
- dependency-name: react
  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>
2025-03-31 22:14:30 +03:00
dependabot[bot]
c7bdc0ce4f Bump react-is from 19.0.0 to 19.1.0 (#444)
Bumps [react-is](https://github.com/facebook/react/tree/HEAD/packages/react-is) from 19.0.0 to 19.1.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react-is)

---
updated-dependencies:
- dependency-name: react-is
  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>
2025-03-31 22:13:55 +03:00
dependabot[bot]
b117864f04 Bump react-hook-form from 7.54.2 to 7.55.0 (#453)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.54.2 to 7.55.0.
- [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.54.2...v7.55.0)

---
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>
2025-03-31 22:09:44 +03:00
dependabot[bot]
39996db2e0 Bump react-router-dom from 7.4.0 to 7.4.1 (#451)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.4.0 to 7.4.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.4.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  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>
2025-03-31 22:06:51 +03:00
dependabot[bot]
05b0864759 Bump react-router from 7.4.0 to 7.4.1 (#446)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.4.0 to 7.4.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.4.1/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  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>
2025-03-31 22:04:29 +03:00
dependabot[bot]
a24b5b1914 Bump @mui/icons-material from 6.4.7 to 6.4.8 (#439)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 6.4.7 to 6.4.8.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v6.4.8/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v6.4.8/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>
2025-03-31 22:00:56 +03:00
dependabot[bot]
765f6d4da8 Bump @tanstack/react-query from 5.69.0 to 5.71.0 (#441)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.69.0 to 5.71.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.71.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  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>
2025-03-31 22:00:43 +03:00
dependabot[bot]
386e1e8fb2 Bump typescript-eslint from 8.27.0 to 8.28.0 (#440)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.27.0 to 8.28.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.28.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-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>
2025-03-31 22:00:16 +03:00
dependabot[bot]
d62985d1a7 Bump @typescript-eslint/parser from 8.27.0 to 8.28.0 (#447)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.27.0 to 8.28.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/v8.28.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>
2025-03-31 21:36:25 +03:00
dependabot[bot]
13afd4ef61 Bump eslint-plugin-prettier from 5.2.4 to 5.2.5 (#449)
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.2.4 to 5.2.5.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.2.4...v5.2.5)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  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>
2025-03-31 21:34:01 +03:00
dependabot[bot]
c5647b9d39 Bump eslint-plugin-yaml from 1.0.3 to 1.1.2 (#452)
Bumps [eslint-plugin-yaml](https://github.com/aminya/eslint-plugin-yaml) from 1.0.3 to 1.1.2.
- [Release notes](https://github.com/aminya/eslint-plugin-yaml/releases)
- [Commits](https://github.com/aminya/eslint-plugin-yaml/compare/v1.0.3...v1.1.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-yaml
  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>
2025-03-31 21:33:29 +03:00
dependabot[bot]
4220343b66 Bump @typescript-eslint/eslint-plugin from 8.27.0 to 8.28.0 (#437)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.27.0 to 8.28.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.28.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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>
2025-03-31 21:33:11 +03:00
dependabot[bot]
bd7ab44654 Bump @types/node from 22.13.12 to 22.13.14 (#438)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.13.12 to 22.13.14.
- [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>
2025-03-31 21:32:37 +03:00
dependabot[bot]
e7588b401a Bump vite from 6.2.3 to 6.2.4 in the npm_and_yarn group (#454)
Bumps the npm_and_yarn group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.3 to 6.2.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.4/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  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>
2025-03-31 21:32:02 +03:00
dependabot[bot]
24c60f7ff0 Bump ra-language-french from 5.6.4 to 5.7.0 (#442)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.6.4 to 5.7.0.
- [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/v5.6.4...v5.7.0)

---
updated-dependencies:
- dependency-name: ra-language-french
  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>
2025-03-31 21:31:42 +03:00
dependabot[bot]
f28e389336 Bump react-admin from 5.6.4 to 5.7.0 (#445)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.6.4 to 5.7.0.
- [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/v5.6.4...v5.7.0)

---
updated-dependencies:
- dependency-name: react-admin
  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>
2025-03-31 21:28:44 +03:00
dependabot[bot]
86a87e7727 Bump vite from 6.2.2 to 6.2.3 in the npm_and_yarn group (#434)
Bumps the npm_and_yarn group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.2 to 6.2.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  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>
2025-03-25 17:25:24 +02:00
dependabot[bot]
d9cf56a6ce Bump ts-jest from 29.2.6 to 29.3.0 (#431)
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.2.6 to 29.3.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.2.6...v29.3.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>
2025-03-24 11:27:43 +02:00
dependabot[bot]
e398d518c2 Bump react-router-dom from 7.3.0 to 7.4.0 (#418)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.3.0 to 7.4.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.4.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  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>
2025-03-24 11:25:56 +02:00
dependabot[bot]
6d1856c222 Bump react-router from 7.3.0 to 7.4.0 (#426)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.3.0 to 7.4.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.4.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  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>
2025-03-24 11:22:11 +02:00
dependabot[bot]
25be29910e Bump @tanstack/react-query from 5.68.0 to 5.69.0 (#422)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.68.0 to 5.69.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.69.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  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>
2025-03-24 11:17:01 +02:00
dependabot[bot]
17e7891d0a Bump ra-language-french from 5.6.3 to 5.6.4 (#423)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.6.3 to 5.6.4.
- [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/v5.6.3...v5.6.4)

---
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>
2025-03-24 11:16:46 +02:00
dependabot[bot]
f427255554 Bump eslint-plugin-prettier from 5.2.3 to 5.2.4 (#421)
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.2.3 to 5.2.4.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.2.3...v5.2.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  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>
2025-03-24 11:16:29 +02:00
dependabot[bot]
215a882281 Bump typescript-eslint from 8.26.1 to 8.27.0 (#415)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.26.1 to 8.27.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.27.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-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>
2025-03-24 11:12:50 +02:00
dependabot[bot]
4103c65415 Bump @mui/material from 6.4.7 to 6.4.8 (#424)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.4.7 to 6.4.8.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v6.4.8/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v6.4.8/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>
2025-03-24 11:10:35 +02:00
dependabot[bot]
d5cee75535 Bump @types/node from 22.13.10 to 22.13.12 (#425)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.13.10 to 22.13.12.
- [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>
2025-03-24 11:10:15 +02:00
dependabot[bot]
d8173f160d Bump @types/react from 19.0.10 to 19.0.12 (#427)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.0.10 to 19.0.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  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>
2025-03-24 11:10:01 +02:00
dependabot[bot]
fd0cf318dd Bump @typescript-eslint/parser from 8.26.1 to 8.27.0 (#432)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.26.1 to 8.27.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/v8.27.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>
2025-03-24 11:09:46 +02:00
dependabot[bot]
978eeafeaa Bump eslint from 9.22.0 to 9.23.0 (#430)
Bumps [eslint](https://github.com/eslint/eslint) from 9.22.0 to 9.23.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/v9.22.0...v9.23.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>
2025-03-24 11:09:23 +02:00
dependabot[bot]
9907c0e0e9 Bump react-admin from 5.6.3 to 5.6.4 (#416)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.6.3 to 5.6.4.
- [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/v5.6.3...v5.6.4)

---
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>
2025-03-24 11:08:34 +02:00
Borislav Pantaleev
7af2831be8 Make bulk registration CSV import more user-friendly (#411)
* Refactor CSV import

* Finish refactoring

* Add types file

* update readme
2025-03-21 20:29:07 +02:00
Aine
28043dc2ca do not show Server Status menu entry unless it is actually enabled, fixes #412 2025-03-18 13:35:16 +02:00
Aine
52d0f936d7 Merge pull request #410 from etkecc/dependabot/npm_and_yarn/npm_and_yarn-2c631a4876
Bump @babel/runtime from 7.26.7 to 7.26.10 in the npm_and_yarn group
2025-03-17 09:59:35 +00:00
dependabot[bot]
087f098b29 Bump @babel/runtime from 7.26.7 to 7.26.10 in the npm_and_yarn group
Bumps the npm_and_yarn group with 1 update: [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime).


Updates `@babel/runtime` from 7.26.7 to 7.26.10
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 09:58:29 +00:00
Aine
2eaf5a8bf0 Merge pull request #409 from etkecc/dependabot/npm_and_yarn/tanstack/react-query-5.68.0
Bump @tanstack/react-query from 5.67.2 to 5.68.0
2025-03-17 09:44:26 +00:00
Aine
7b1445ee4a Merge pull request #406 from etkecc/dependabot/npm_and_yarn/vite-6.2.2
Bump vite from 6.2.1 to 6.2.2
2025-03-17 09:44:09 +00:00
dependabot[bot]
934424e6f1 Bump vite from 6.2.1 to 6.2.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.1 to 6.2.2.
- [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/v6.2.2/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>
2025-03-17 09:43:09 +00:00
Aine
872f2fb7a2 Merge pull request #405 from etkecc/dependabot/npm_and_yarn/typescript-eslint-8.26.1
Bump typescript-eslint from 8.26.0 to 8.26.1
2025-03-17 09:41:02 +00:00
dependabot[bot]
756d1c018c Bump @tanstack/react-query from 5.67.2 to 5.68.0
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.67.2 to 5.68.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.68.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 07:43:39 +00:00
dependabot[bot]
a2c6482d68 Bump typescript-eslint from 8.26.0 to 8.26.1
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.26.0 to 8.26.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.26.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 07:42:23 +00:00
Aine
4d726ec97e do not set guests filter by default, fixes #404 2025-03-14 22:25:18 +02:00
Aine
df618441b7 Merge pull request #392 from etkecc/dependabot/npm_and_yarn/react-router-dom-7.3.0
Bump react-router-dom from 7.2.0 to 7.3.0
2025-03-10 09:56:44 +00:00
dependabot[bot]
0c9a6a6515 Bump react-router-dom from 7.2.0 to 7.3.0
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.2.0 to 7.3.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.3.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 09:55:46 +00:00
Aine
8640d1357f Merge pull request #391 from etkecc/dependabot/npm_and_yarn/react-router-7.3.0
Bump react-router from 7.2.0 to 7.3.0
2025-03-10 09:53:27 +00:00
Aine
35eab88465 add missing translations 2025-03-10 11:53:05 +02:00
Aine
f5ce8246a6 Merge pull request #397 from etkecc/dependabot/npm_and_yarn/typescript-5.8.2
Bump typescript from 5.7.3 to 5.8.2
2025-03-10 09:45:56 +00:00
Aine
b7b5392693 Merge pull request #395 from etkecc/dependabot/npm_and_yarn/eslint-9.22.0
Bump eslint from 9.21.0 to 9.22.0
2025-03-10 09:45:37 +00:00
dependabot[bot]
f6050616bf Bump eslint from 9.21.0 to 9.22.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.21.0 to 9.22.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/v9.21.0...v9.22.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 09:44:16 +00:00
Aine
0d2cbf3554 Merge pull request #396 from etkecc/dependabot/npm_and_yarn/eslint/js-9.22.0
Bump @eslint/js from 9.21.0 to 9.22.0
2025-03-10 09:42:01 +00:00
Aine
88923aaa2c Merge pull request #403 from etkecc/dependabot/npm_and_yarn/mui/icons-material-6.4.7
Bump @mui/icons-material from 6.4.5 to 6.4.7
2025-03-10 09:41:40 +00:00
dependabot[bot]
ae83cb0fdf Bump react-router from 7.2.0 to 7.3.0
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.2.0 to 7.3.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.3.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 09:40:37 +00:00
dependabot[bot]
c678253af5 Bump @mui/icons-material from 6.4.5 to 6.4.7
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 6.4.5 to 6.4.7.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v6.4.7/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v6.4.7/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>
2025-03-10 09:40:36 +00:00
dependabot[bot]
a7a486790d Bump typescript from 5.7.3 to 5.8.2
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.7.3 to 5.8.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.3...v5.8.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 09:40:31 +00:00
Aine
7894d58aef Merge pull request #388 from etkecc/dependabot/npm_and_yarn/ra-language-french-5.6.3
Bump ra-language-french from 5.6.2 to 5.6.3
2025-03-10 09:39:15 +00:00
Aine
23dbf6d144 Merge pull request #398 from etkecc/dependabot/npm_and_yarn/mui/material-6.4.7
Bump @mui/material from 6.4.6 to 6.4.7
2025-03-10 09:37:40 +00:00
Aine
3d22e607d6 Merge pull request #401 from etkecc/dependabot/npm_and_yarn/types/node-22.13.10
Bump @types/node from 22.13.8 to 22.13.10
2025-03-10 09:37:23 +00:00
Aine
5e384b72ff Merge pull request #389 from etkecc/dependabot/npm_and_yarn/eslint-config-prettier-10.1.1
Bump eslint-config-prettier from 10.0.2 to 10.1.1
2025-03-10 09:37:09 +00:00
Aine
313e1fc5f4 Merge pull request #393 from etkecc/dependabot/npm_and_yarn/tanstack/react-query-5.67.2
Bump @tanstack/react-query from 5.67.1 to 5.67.2
2025-03-10 09:36:52 +00:00
Aine
4b8f9da72b Merge pull request #399 from etkecc/dependabot/npm_and_yarn/react-admin-5.6.3
Bump react-admin from 5.6.2 to 5.6.3
2025-03-10 09:36:36 +00:00
Aine
70ad60f198 Merge pull request #402 from etkecc/dependabot/npm_and_yarn/vite-6.2.1
Bump vite from 6.2.0 to 6.2.1
2025-03-10 09:36:14 +00:00
dependabot[bot]
94c27bb399 Bump vite from 6.2.0 to 6.2.1
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.0 to 6.2.1.
- [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/create-vite@6.2.1/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>
2025-03-10 07:20:00 +00:00
dependabot[bot]
35e1f09d0f Bump @types/node from 22.13.8 to 22.13.10
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.13.8 to 22.13.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>
2025-03-10 07:19:52 +00:00
dependabot[bot]
91556561bb Bump react-admin from 5.6.2 to 5.6.3
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.6.2 to 5.6.3.
- [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/v5.6.2...v5.6.3)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 07:19:36 +00:00
dependabot[bot]
e9d0a3f595 Bump @mui/material from 6.4.6 to 6.4.7
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.4.6 to 6.4.7.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v6.4.7/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v6.4.7/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>
2025-03-10 07:19:27 +00:00
dependabot[bot]
1fe94d1b9e Bump @eslint/js from 9.21.0 to 9.22.0
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.21.0 to 9.22.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.22.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>
2025-03-10 07:19:09 +00:00
dependabot[bot]
188701f4f1 Bump @tanstack/react-query from 5.67.1 to 5.67.2
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.67.1 to 5.67.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.67.2/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 07:18:40 +00:00
dependabot[bot]
1743142a8a Bump eslint-config-prettier from 10.0.2 to 10.1.1
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 10.0.2 to 10.1.1.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v10.0.2...v10.1.1)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 07:17:41 +00:00
dependabot[bot]
87afed97b9 Bump ra-language-french from 5.6.2 to 5.6.3
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.6.2 to 5.6.3.
- [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/v5.6.2...v5.6.3)

---
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>
2025-03-10 07:17:34 +00:00
90 changed files with 5692 additions and 3689 deletions

63
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,63 @@
# Contribution Guidelines
Table of Contents:
<!-- vim-markdown-toc GFM -->
* [Did you find a bug?](#did-you-find-a-bug)
* [Is it a Security Vulnerability?](#is-it-a-security-vulnerability)
* [Is it already a known issue?](#is-it-already-a-known-issue)
* [Reporting a Bug](#reporting-a-bug)
* [Is there a patch for the bug?](#is-there-a-patch-for-the-bug)
* [Do you want to add a new feature?](#do-you-want-to-add-a-new-feature)
* [Is it just an idea?](#is-it-just-an-idea)
* [Is there a patch for the feature?](#is-there-a-patch-for-the-feature)
* [Do you have questions about the Synapse Admin project or need guidance?](#do-you-have-questions-about-the-synapse-admin-project-or-need-guidance)
<!-- vim-markdown-toc -->
## Did you find a bug?
### Is it a Security Vulnerability?
Please follow the [Security Policy](https://github.com/etkecc/synapse-admin/blob/main/.github/SECURITY.md) for reporting
security vulnerabilities.
### Is it already a known issue?
Please ensure the bug was not already reported by searching [the Issues section](https://github.com/etkecc/synapse-admin/issues).
### Reporting a Bug
If you think you have found a bug in Synapse Admin, it is not a security vulnerability, and it is not already reported,
please open [a new issue](https://github.com/etkecc/synapse-admin/issues/new) with:
* A proper title and clear description of the problem.
* As much relevant information as possible:
* The version of Synapse Admin you are using.
* The version of Synapse you are using.
* Any relevant browser console logs, failed requests details, and error messages.
### Is there a patch for the bug?
If you already have a patch for the bug, please open a pull request with the patch,
and mention the issue number in the pull request description.
## Do you want to add a new feature?
### Is it just an idea?
Please open [a new issue](https://github.com/etkecc/synapse-admin/issues/new) with:
* A proper title and clear description of the requested feature.
* Any relevant information about the feature:
* Why do you think this feature is needed?
* How do you think it should work? (provide Synapse Admin API endpoint)
* Any relevant screenshots or mockups.
### Is there a patch for the feature?
If you already have a patch for the feature, please open a pull request with the patch,
and mention the issue number in the pull request description.
## Do you have questions about the Synapse Admin project or need guidance?
Please use the official community Matrix room: [#synapse-admin:etke.cc](https://matrix.to/#/#synapse-admin:etke.cc)

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,34 @@
---
name: Bug report
about: Report a Synapse Admin bug
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Browser console logs**
If applicable, add the browser console's log
**Instance configuration:**
- Synapse Admin version: [e.g. v0.10.3-etke39]
- Synapse version [v1.127.1]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an idea for Synapse Admin
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Provide related Synapse Admin API endpoints**
If applicable, provide links to the Synapse Admin API's endpoints that could be used to implement that feature
**Additional context**
Add any other context or screenshots about the feature request here.

14
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
Only [the last published version](https://github.com/etkecc/synapse-admin/releases/latest) of the project is supported.
This means that only the latest version will receive security updates.
If you are using an older version, you are strongly encouraged to upgrade to the latest version.
## Reporting a Vulnerability
Please contact us using the [#synapse-admin:etke.cc](https://matrix.to/#/#synapse-admin:etke.cc) Matrix room.
The Synapse Admin project is a static JS UI for the Synapse server,
so it is unlikely that there are (or will be) any impactful security vulnerabilities in the project itself.
However, we do not rule out the possibility of such cases, so we will be happy to receive any reports!

View File

@@ -48,25 +48,31 @@ jobs:
name: dist name: dist
path: dist/ path: dist/
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to ghcr.io - name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to hub.docker.com
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: etkecc
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with: with:
images: | images: |
${{ github.repository }}
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
registry.etke.cc/${{ github.repository }} registry.etke.cc/${{ github.repository }}
tags: | tags: |
type=raw,value=latest,enable=${{ github.ref_name == 'main' }} type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
type=semver,pattern={{raw}} type=semver,pattern={{raw}}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
context: . context: .
@@ -106,7 +112,7 @@ jobs:
run: | run: |
mv dist synapse-admin mv dist synapse-admin
tar chvzf synapse-admin.tar.gz synapse-admin tar chvzf synapse-admin.tar.gz synapse-admin
- uses: softprops/action-gh-release@v2 - uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
with: with:
files: synapse-admin.tar.gz files: synapse-admin.tar.gz
generate_release_notes: true generate_release_notes: true

File diff suppressed because one or more lines are too long

925
.yarn/releases/yarn-4.4.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,7 @@
* [Configuration](#configuration) * [Configuration](#configuration)
* [Prefilling login form](#prefilling-login-form) * [Prefilling login form](#prefilling-login-form)
* [Restricting available homeserver](#restricting-available-homeserver) * [Restricting available homeserver](#restricting-available-homeserver)
* [Configuring CORS credentials](#configuring-cors-credentials)
* [Protecting appservice managed users](#protecting-appservice-managed-users) * [Protecting appservice managed users](#protecting-appservice-managed-users)
* [Adding custom menu items](#adding-custom-menu-items) * [Adding custom menu items](#adding-custom-menu-items)
* [Usage](#usage) * [Usage](#usage)
@@ -54,7 +55,7 @@ The full list is described below in the [Changes](#changes) section.
* As a core/default component on [etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=synapse-admin) * As a core/default component on [etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=synapse-admin)
* As a standalone app on [admin.etke.cc](https://admin.etke.cc) * As a standalone app on [admin.etke.cc](https://admin.etke.cc)
* As a prebuilt distribution on [GitHub Releases](https://github.com/etkecc/synapse-admin/releases) * As a prebuilt distribution on [GitHub Releases](https://github.com/etkecc/synapse-admin/releases)
* As a Docker container on [GitHub Container Registry](https://github.com/etkecc/synapse-admin/pkgs/container/synapse-admin) * As a Docker container on [Docker Hub](https://hub.docker.com/r/etkecc/synapse-admin) and [GitHub Container Registry](https://github.com/etkecc/synapse-admin/pkgs/container/synapse-admin)
* As a component in [Matrix-Docker-Ansible-Deploy Playbook](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-synapse-admin.md) * As a component in [Matrix-Docker-Ansible-Deploy Playbook](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-synapse-admin.md)
### Changes ### Changes
@@ -108,8 +109,10 @@ The following changes are already implemented:
* 📞 [Support E.164-based Matrix IDs (MSC4009)](https://github.com/etkecc/synapse-admin/pull/214) * 📞 [Support E.164-based Matrix IDs (MSC4009)](https://github.com/etkecc/synapse-admin/pull/214)
* 🛑 [Add support for Account Suspension (MSC3823)](https://github.com/etkecc/synapse-admin/pull/195) * 🛑 [Add support for Account Suspension (MSC3823)](https://github.com/etkecc/synapse-admin/pull/195)
* 🗑️ [Add "Purge Remote Media" button](https://github.com/etkecc/synapse-admin/pull/237) * 🗑️ [Add "Purge Remote Media" button](https://github.com/etkecc/synapse-admin/pull/237)
* [Respect base url (`BASE_PATH` / `vite build --base`) when loading `config.json`](https://github.com/etkecc/synapse-admin/pull/274) * 📁 [Respect base url (`BASE_PATH` / `vite build --base`) when loading `config.json`](https://github.com/etkecc/synapse-admin/pull/274)
* [Add Users' Account Data tab](https://github.com/etkecc/synapse-admin/pull/276) * 🗂️ [Add Users' Account Data tab](https://github.com/etkecc/synapse-admin/pull/276)
* 🧾 [Make bulk registration CSV import more user-friendly](https://github.com/etkecc/synapse-admin/pull/411)
* 🌐 [Configurable CORS Credentials](https://github.com/etkecc/synapse-admin/pull/456)
#### exclusive for [etke.cc](https://etke.cc) customers #### exclusive for [etke.cc](https://etke.cc) customers
@@ -119,6 +122,7 @@ The following list contains such features - they are only available for [etke.cc
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182) * 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240) * 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
* 🛠️ [Server Commands panel](https://github.com/etkecc/synapse-admin/pull/365) * 🛠️ [Server Commands panel](https://github.com/etkecc/synapse-admin/pull/365)
* 🚀 [Server Actions page](https://github.com/etkecc/synapse-admin/pull/457)
### Development ### Development
@@ -179,6 +183,12 @@ You can restrict the homeserver(s), so that the user can no longer define it him
[Documentation](./docs/restrict-hs.md) [Documentation](./docs/restrict-hs.md)
### Configuring CORS credentials
You can configure the CORS credentials mode for the Synapse Admin instance.
[Documentation](./docs/cors-credentials.md)
### Protecting appservice managed users ### Protecting appservice managed users
To avoid accidental adjustments of appservice-managed users (e.g., puppets created by a bridge) and breaking the bridge, To avoid accidental adjustments of appservice-managed users (e.g., puppets created by a bridge) and breaking the bridge,

View File

@@ -17,6 +17,7 @@ Table of contents:
Specific configuration options: Specific configuration options:
* [Customizing CORS credentials](./cors-credentials.md)
* [Restricting available homeserver](./restrict-hs.md) * [Restricting available homeserver](./restrict-hs.md)
* [System / Appservice-managed Users](./system-users.md) * [System / Appservice-managed Users](./system-users.md)
* [Custom Menu Items](./custom-menu.md) * [Custom Menu Items](./custom-menu.md)
@@ -33,6 +34,10 @@ they are not available for any other Synapse Admin deployments.
* [Server Status icon](../src/components/etke.cc/README.md#server-status-icon) * [Server Status icon](../src/components/etke.cc/README.md#server-status-icon)
* [Server Status page](../src/components/etke.cc/README.md#server-status-page) * [Server Status page](../src/components/etke.cc/README.md#server-status-page)
* [Server Actions page](../src/components/etke.cc/README.md#server-actions-page)
* [Server Commands Panel](../src/components/etke.cc/README.md#server-commands-panel)
* [Server Notifications icon](../src/components/etke.cc/README.md#server-notifications-icon)
* [Server Notifications page](../src/components/etke.cc/README.md#server-notifications-page)
## Deployment ## Deployment

View File

@@ -28,6 +28,12 @@ In this case, you could provide the configuration in the `/.well-known/matrix/cl
The homeserver URL should be the _actual_ homeserver URL, and not the delegated one. The homeserver URL should be the _actual_ homeserver URL, and not the delegated one.
Example: `https://matrix.example.com` or `https://synapse.example.net` Example: `https://matrix.example.com` or `https://synapse.example.net`
[More details](restrict-hs.md) [More details](restrict-hs.md)
* `corsCredentials` - configure the CORS credentials for the Synapse Admin instance.
It accepts the following values:
* `same-origin` (default): Cookies will be sent only if the request is made from the same origin as the server.
* `include`: Cookies will be sent regardless of the origin of the request.
* `omit`: Cookies will not be sent with the request.
[More details](cors-credentials.md)
* `asManagedUsers` - protect system user accounts managed by appservices (such as bridges) / system (such as bots) from accidental changes. * `asManagedUsers` - protect system user accounts managed by appservices (such as bridges) / system (such as bots) from accidental changes.
By defining a list of MXID regex patterns, you can protect these accounts from accidental changes. By defining a list of MXID regex patterns, you can protect these accounts from accidental changes.
Example: `^@baibot:example\\.com$`, `^@slackbot:example\\.com$`, `^@slack_[a-zA-Z0-9\\-]+:example\\.com$`, `^@telegrambot:example\\.com$`, `^@telegram_[a-zA-Z0-9]+:example\\.com$` Example: `^@baibot:example\\.com$`, `^@slackbot:example\\.com$`, `^@slack_[a-zA-Z0-9\\-]+:example\\.com$`, `^@telegrambot:example\\.com$`, `^@telegram_[a-zA-Z0-9]+:example\\.com$`

35
docs/cors-credentials.md Normal file
View File

@@ -0,0 +1,35 @@
# CORS Credentials
If you'd like to use cookie-based authentication
(for example, [ForwardAuth with Authelia](https://github.com/Awesome-Technologies/synapse-admin/issues/655)),
you can configure the `corsCredentials` option in the `config.json` file or in the `/.well-known/matrix/client` file.
## Configuration
> [Documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials)
The `corsCredentials` option accepts the following values:
* `same-origin` (default): Cookies will be sent only if the request is made from the same origin as the server.
* `include`: Cookies will be sent regardless of the origin of the request.
* `omit`: Cookies will not be sent with the request.
[Configuration options](config.md)
### config.json
```json
{
"corsCredentials": "include"
}
```
### `/.well-known/matrix/client`
```json
{
"cc.etke.synapse-admin": {
"corsCredentials": "include"
}
}
```

View File

@@ -10,22 +10,22 @@ const config: JestConfigWithTsJest = {
extensionsToTreatAsEsm: [".ts", ".tsx"], extensionsToTreatAsEsm: [".ts", ".tsx"],
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"], setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
transform: { transform: {
'^.+\\.tsx?$': [ "^.+\\.tsx?$": [
'ts-jest', "ts-jest",
{ {
diagnostics: { diagnostics: {
ignoreCodes: [1343] ignoreCodes: [1343],
}, },
astTransformers: { astTransformers: {
before: [ before: [
{ {
path: 'ts-jest-mock-import-meta', path: "ts-jest-mock-import-meta",
options: { metaObjectReplacement: { env: { BASE_URL: "/" } } } options: { metaObjectReplacement: { env: { BASE_URL: "/" } } },
} },
] ],
} },
} },
] ],
} },
}; };
export default config; export default config;

View File

@@ -6,6 +6,9 @@ default:
build: __install build: __install
@yarn run build --base=./ @yarn run build --base=./
update:
yarn upgrade-interactive --latest
# run the app in a development mode # run the app in a development mode
run: run:
@yarn start --host 0.0.0.0 @yarn start --host 0.0.0.0

View File

@@ -1,6 +1,6 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.10.3", "version": "0.11.1",
"description": "Admin GUI for the Matrix.org server Synapse", "description": "Admin GUI for the Matrix.org server Synapse",
"type": "module", "type": "module",
"author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)", "author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)",
@@ -11,36 +11,35 @@
"url": "https://github.com/etkecc/synapse-admin" "url": "https://github.com/etkecc/synapse-admin"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.25.0",
"@testing-library/dom": "^10.0.0", "@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.17",
"@types/node": "^22.13.8", "@types/node": "^22.15.29",
"@types/papaparse": "^5.3.15", "@types/papaparse": "^5.3.16",
"@types/react": "^19.0.10", "@types/react": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.24.0", "@typescript-eslint/parser": "^8.32.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.5.0",
"eslint": "^9.21.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^10.0.2", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.3", "eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-yaml": "^1.0.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react-test-renderer": "^19.0.0", "react-test-renderer": "^19.1.0",
"ts-jest": "^29.2.6", "ts-jest": "^29.3.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.7.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.26.0", "typescript-eslint": "^8.33.0",
"vite": "^6.2.0", "vite": "^6.3.5",
"vite-plugin-version-mark": "^0.1.4" "vite-plugin-version-mark": "^0.1.4"
}, },
"dependencies": { "dependencies": {
@@ -48,34 +47,34 @@
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@haleos/ra-language-german": "^1.0.0", "@haleos/ra-language-german": "^1.0.0",
"@haxqer/ra-language-chinese": "^4.16.2", "@haxqer/ra-language-chinese": "^4.16.2",
"@mui/icons-material": "^6.4.5", "@mui/icons-material": "^7.1.0",
"@mui/material": "^6.4.6", "@mui/material": "^7.1.1",
"@mui/utils": "^6.4.2", "@mui/utils": "^7.1.0",
"@tanstack/react-query": "^5.67.1", "@tanstack/react-query": "^5.79.0",
"history": "^5.3.0", "history": "^5.3.0",
"jest-fixed-jsdom": "^0.0.9", "jest-fixed-jsdom": "^0.0.9",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"papaparse": "^5.5.1", "papaparse": "^5.5.3",
"ra-core": "^5.4.4", "ra-core": "^5.4.4",
"ra-i18n-polyglot": "^5.4.4", "ra-i18n-polyglot": "^5.4.4",
"ra-language-english": "^5.4.4", "ra-language-english": "^5.4.4",
"ra-language-farsi": "^5.1.0", "ra-language-farsi": "^5.1.0",
"ra-language-french": "^5.6.2", "ra-language-french": "^5.8.3",
"ra-language-italian": "^3.13.1", "ra-language-italian": "^3.13.1",
"ra-language-russian": "^5.4.3", "ra-language-russian": "^5.4.4",
"react": "^19.0.0", "react": "^19.1.0",
"react-admin": "^5.6.2", "react-admin": "^5.8.3",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.57.0",
"react-is": "^19.0.0", "react-is": "^19.1.0",
"ts-jest-mock-import-meta": "^1.2.1", "react-router": "^7.6.0",
"react-router": "^7.2.0", "react-router-dom": "^7.6.1",
"react-router-dom": "^7.2.0" "ts-jest-mock-import-meta": "^1.3.0"
}, },
"scripts": { "scripts": {
"start": "vite serve", "start": "vite serve",
"build": "vite build", "build": "vite build",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ignore-path .gitignore --ignore-pattern testdata/ --ext .ts,.tsx,.yml,.yaml .", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ignore-path .gitignore --ignore-pattern testdata/ --ext .ts,.tsx .",
"fix": "yarn lint --fix", "fix": "yarn lint --fix",
"test": "yarn jest", "test": "yarn jest",
"test:watch": "yarn jest --watch" "test:watch": "yarn jest --watch"
@@ -88,15 +87,13 @@
"import", "import",
"prettier", "prettier",
"unused-imports", "unused-imports",
"@typescript-eslint", "@typescript-eslint"
"yaml"
], ],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic", "plugin:@typescript-eslint/stylistic",
"plugin:import/typescript", "plugin:import/typescript"
"plugin:yaml/legacy"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
@@ -135,8 +132,7 @@
] ]
] ]
} }
], ]
"unused-imports/no-unused-imports-ts": 2
} }
}, },
"prettier": { "prettier": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -1,6 +1,6 @@
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import fetchMock from "jest-fetch-mock"; import fetchMock from "jest-fetch-mock";
import { BrowserRouter } from "react-router-dom";
fetchMock.enableMocks(); fetchMock.enableMocks();
jest.mock("./synapse/authProvider", () => ({ jest.mock("./synapse/authProvider", () => ({

View File

@@ -1,12 +1,17 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { merge } from "lodash"; import { merge } from "lodash";
import polyglotI18nProvider from "ra-i18n-polyglot"; import polyglotI18nProvider from "ra-i18n-polyglot";
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin"; import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import AdminLayout from "./components/AdminLayout"; import AdminLayout from "./components/AdminLayout";
import UserImport from "./components/UserImport"; import ServerActionsPage from "./components/etke.cc/ServerActionsPage";
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
import RecurringCommandEdit from "./components/etke.cc/schedules/components/recurring/RecurringCommandEdit";
import ScheduledCommandEdit from "./components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit";
import ScheduledCommandShow from "./components/etke.cc/schedules/components/scheduled/ScheduledCommandShow";
import UserImport from "./components/user-import/UserImport";
import germanMessages from "./i18n/de"; import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en"; import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr"; import frenchMessages from "./i18n/fr";
@@ -23,9 +28,6 @@ import userMediaStats from "./resources/user_media_statistics";
import users from "./resources/users"; import users from "./resources/users";
import authProvider from "./synapse/authProvider"; import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider"; import dataProvider from "./synapse/dataProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
// TODO: Can we use lazy loading together with browser locale? // TODO: Can we use lazy loading together with browser locale?
const messages = { const messages = {
@@ -66,6 +68,12 @@ export const App = () => (
<CustomRoutes> <CustomRoutes>
<Route path="/import_users" element={<UserImport />} /> <Route path="/import_users" element={<UserImport />} />
<Route path="/server_status" element={<ServerStatusPage />} /> <Route path="/server_status" element={<ServerStatusPage />} />
<Route path="/server_actions" element={<ServerActionsPage />} />
<Route path="/server_actions/scheduled/:id/show" element={<ScheduledCommandShow />} />
<Route path="/server_actions/scheduled/:id" element={<ScheduledCommandEdit />} />
<Route path="/server_actions/scheduled/create" element={<ScheduledCommandEdit />} />
<Route path="/server_actions/recurring/:id" element={<RecurringCommandEdit />} />
<Route path="/server_actions/recurring/create" element={<RecurringCommandEdit />} />
<Route path="/server_notifications" element={<ServerNotificationsPage />} /> <Route path="/server_notifications" element={<ServerNotificationsPage />} />
</CustomRoutes> </CustomRoutes>
<Resource {...users} /> <Resource {...users} />

View File

@@ -4,4 +4,4 @@ import { Config } from "./utils/config";
export const AppContext = createContext<Config>({} as Config); export const AppContext = createContext<Config>({} as Config);
export const useAppContext = () => useContext(AppContext) as Config; export const useAppContext = () => useContext(AppContext) as Config;

View File

@@ -1,12 +1,26 @@
import { CheckForApplicationUpdate, AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu, useStore } from "react-admin"; import ManageHistoryIcon from "@mui/icons-material/ManageHistory";
import { LoginMethod } from "../pages/LoginPage";
import { useEffect, useState, Suspense } from "react"; import { useEffect, useState, Suspense } from "react";
import { Icons, DefaultIcon } from "../utils/icons"; import {
import { MenuItem, GetConfig, ClearConfig } from "../utils/config"; CheckForApplicationUpdate,
AppBar,
TitlePortal,
InspectorButton,
Confirm,
Layout,
Logout,
Menu,
useLogout,
UserMenu,
useStore,
} from "react-admin";
import Footer from "./Footer"; import Footer from "./Footer";
import ServerStatusBadge from "./etke.cc/ServerStatusBadge"; import { LoginMethod } from "../pages/LoginPage";
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
import { Icons, DefaultIcon } from "../utils/icons";
import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge"; import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge";
import { ServerProcessResponse, ServerStatusResponse } from "../synapse/dataProvider"; import { ServerProcessResponse, ServerStatusResponse } from "../synapse/dataProvider";
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
import { ServerStatusStyledBadge } from "./etke.cc/ServerStatusBadge"; import { ServerStatusStyledBadge } from "./etke.cc/ServerStatusBadge";
const AdminUserMenu = () => { const AdminUserMenu = () => {
@@ -50,68 +64,106 @@ const AdminUserMenu = () => {
}; };
const AdminAppBar = () => { const AdminAppBar = () => {
return (<AppBar userMenu={<AdminUserMenu />}> return (
<TitlePortal /> <AppBar userMenu={<AdminUserMenu />}>
<ServerStatusBadge /> <TitlePortal />
<ServerNotificationsBadge /> <ServerStatusBadge />
<InspectorButton /> <ServerNotificationsBadge />
</AppBar>); <InspectorButton />
</AppBar>
);
}; };
const AdminMenu = (props) => { const AdminMenu = props => {
const [menu, setMenu] = useState([] as MenuItem[]); const [menu, setMenu] = useState([] as MenuItem[]);
useEffect(() => setMenu(GetConfig().menu), []); const [etkeRoutesEnabled, setEtkeRoutesEnabled] = useState(false);
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); useEffect(() => {
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { success: false, ok: false, host: "", results: [] }); setMenu(GetConfig().menu);
if (GetConfig().etkeccAdmin) {
setEtkeRoutesEnabled(true);
}
}, []);
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
success: false,
ok: false,
host: "",
results: [],
});
return ( return (
<Menu {...props}> <Menu {...props}>
{menu && menu.length > 0 && <Menu.Item to="/server_status" leftIcon={ {etkeRoutesEnabled && (
<ServerStatusStyledBadge <Menu.Item
inSidebar={true} key="server_status"
command={serverProcess.command} to="/server_status"
locked_at={serverProcess.locked_at} leftIcon={
isOkay={serverStatus.ok} /> <ServerStatusStyledBadge
} inSidebar={true}
primaryText="Server Status" /> command={serverProcess.command}
} locked_at={serverProcess.locked_at}
<Menu.ResourceItems /> isOkay={serverStatus.ok}
{menu && menu.map((item, index) => { isLoaded={serverStatus.success}
const { url, icon, label } = item;
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
return (
<Suspense key={index}>
<Menu.Item
to={url}
target="_blank"
primaryText={label}
leftIcon={IconComponent ? <IconComponent /> : <DefaultIcon />}
onClick={props.onMenuClick}
/> />
</Suspense> }
); primaryText="Server Status"
})} />
)}
{etkeRoutesEnabled && (
<Menu.Item
key="server_actions"
to="/server_actions"
leftIcon={<ManageHistoryIcon />}
primaryText="Server Actions"
/>
)}
<Menu.ResourceItems />
{menu &&
menu.map((item, index) => {
const { url, icon, label } = item;
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
return (
<Suspense key={index}>
<Menu.Item
to={url}
target="_blank"
primaryText={label}
leftIcon={IconComponent ? <IconComponent /> : <DefaultIcon />}
onClick={props.onMenuClick}
/>
</Suspense>
);
})}
</Menu> </Menu>
); );
}; };
export const AdminLayout = ({ children }) => { export const AdminLayout = ({ children }) => {
return <> return (
<Layout appBar={AdminAppBar} menu={AdminMenu} sx={{ <>
['& .RaLayout-appFrame']: { <Layout
minHeight: '90vh', appBar={AdminAppBar}
height: '90vh', menu={AdminMenu}
}, sx={{
['& .RaLayout-content']: { ["& .RaLayout-appFrame"]: {
marginBottom: '3rem', minHeight: "90vh",
}, height: "90vh",
}}> },
{children} ["& .RaLayout-content"]: {
<CheckForApplicationUpdate /> marginBottom: "3rem",
</Layout> },
<Footer /> }}
</> >
{children}
<CheckForApplicationUpdate />
</Layout>
<Footer />
</>
);
}; };
export default AdminLayout; export default AdminLayout;

View File

@@ -1,6 +1,7 @@
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import { RecordContextProvider } from "react-admin";
import { act } from "react"; import { act } from "react";
import { RecordContextProvider } from "react-admin";
import AvatarField from "./AvatarField"; import AvatarField from "./AvatarField";
describe("AvatarField", () => { describe("AvatarField", () => {
@@ -8,7 +9,7 @@ describe("AvatarField", () => {
// Mock fetch // Mock fetch
global.fetch = jest.fn(() => global.fetch = jest.fn(() =>
Promise.resolve({ Promise.resolve({
blob: () => Promise.resolve(new Blob(["mock image data"], { type: 'image/jpeg' })), blob: () => Promise.resolve(new Blob(["mock image data"], { type: "image/jpeg" })),
}) })
) as jest.Mock; ) as jest.Mock;

View File

@@ -1,7 +1,8 @@
import { get } from "lodash";
import { Avatar, AvatarProps, Badge, Tooltip } from "@mui/material"; import { Avatar, AvatarProps, Badge, Tooltip } from "@mui/material";
import { FieldProps, useRecordContext, useTranslate } from "react-admin"; import { get } from "lodash";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { FieldProps, useRecordContext, useTranslate } from "react-admin";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia"; import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { isMXID, isASManaged } from "../utils/mxid"; import { isMXID, isASManaged } from "../utils/mxid";
@@ -96,12 +97,15 @@ const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
{letter} {letter}
</Avatar> </Avatar>
</Badge> </Badge>
</Tooltip>); </Tooltip>
);
} }
return (<Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant}> return (
{letter} <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant}>
</Avatar>); {letter}
</Avatar>
);
}; };
export default AvatarField; export default AvatarField;

View File

@@ -1,9 +1,21 @@
import ActionCheck from "@mui/icons-material/CheckCircle";
import ActionDelete from "@mui/icons-material/Delete";
import AlertError from "@mui/icons-material/ErrorOutline";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin"; import {
import ActionDelete from "@mui/icons-material/Delete"; SimpleForm,
import ActionCheck from "@mui/icons-material/CheckCircle"; BooleanInput,
import AlertError from "@mui/icons-material/ErrorOutline"; useTranslate,
RaRecord,
useNotify,
useRedirect,
useDelete,
NotificationType,
useDeleteMany,
Identifier,
useUnselectAll,
} from "react-admin";
interface DeleteRoomButtonProps { interface DeleteRoomButtonProps {
selectedIds: Identifier[]; selectedIds: Identifier[];
@@ -13,7 +25,7 @@ interface DeleteRoomButtonProps {
const resourceName = "rooms"; const resourceName = "rooms";
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => { const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = props => {
const translate = useTranslate(); const translate = useTranslate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [block, setBlock] = useState(false); const [block, setBlock] = useState(false);
@@ -28,7 +40,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
const handleDelete = (values: {block: boolean}) => { const handleDelete = (values: { block: boolean }) => {
deleteMany( deleteMany(
resourceName, resourceName,
{ ids: recordIds, meta: values }, { ids: recordIds, meta: values },
@@ -39,8 +51,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
unselectAll(); unselectAll();
redirect("/rooms"); redirect("/rooms");
}, },
onError: (error) => onError: error => notify("resources.rooms.action.erase.failure", { type: "error" as NotificationType }),
notify("resources.rooms.action.erase.failure", { type: 'error' as NotificationType }),
} }
); );
}; };

View File

@@ -1,9 +1,21 @@
import ActionCheck from "@mui/icons-material/CheckCircle";
import ActionDelete from "@mui/icons-material/Delete";
import AlertError from "@mui/icons-material/ErrorOutline";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin"; import {
import ActionDelete from "@mui/icons-material/Delete"; SimpleForm,
import ActionCheck from "@mui/icons-material/CheckCircle"; BooleanInput,
import AlertError from "@mui/icons-material/ErrorOutline"; useTranslate,
RaRecord,
useNotify,
useRedirect,
useDelete,
NotificationType,
useDeleteMany,
Identifier,
useUnselectAll,
} from "react-admin";
interface DeleteUserButtonProps { interface DeleteUserButtonProps {
selectedIds: Identifier[]; selectedIds: Identifier[];
@@ -13,7 +25,7 @@ interface DeleteUserButtonProps {
const resourceName = "users"; const resourceName = "users";
const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => { const DeleteUserButton: React.FC<DeleteUserButtonProps> = props => {
const translate = useTranslate(); const translate = useTranslate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [deleteMedia, setDeleteMedia] = useState(false); const [deleteMedia, setDeleteMedia] = useState(false);
@@ -29,7 +41,7 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
const handleDelete = (values: {deleteMedia: boolean, redactEvents: boolean}) => { const handleDelete = (values: { deleteMedia: boolean; redactEvents: boolean }) => {
deleteMany( deleteMany(
resourceName, resourceName,
{ ids: recordIds, meta: values }, { ids: recordIds, meta: values },
@@ -39,14 +51,13 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
messageArgs: { messageArgs: {
smart_count: recordIds.length, smart_count: recordIds.length,
}, },
type: 'info' as NotificationType, type: "info" as NotificationType,
}); });
handleDialogClose(); handleDialogClose();
unselectAll(); unselectAll();
redirect("/users"); redirect("/users");
}, },
onError: (error) => onError: error => notify("ra.notification.data_provider_error", { type: "error" as NotificationType }),
notify("ra.notification.data_provider_error", { type: 'error' as NotificationType }),
} }
); );
}; };

View File

@@ -1,4 +1,5 @@
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin"; import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
import { isASManaged } from "../utils/mxid"; import { isASManaged } from "../utils/mxid";
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => { export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {

View File

@@ -1,15 +1,20 @@
import { Stack, Switch, Typography } from "@mui/material";
import { useState, useEffect } from "react";
import { useRecordContext } from "react-admin"; import { useRecordContext } from "react-admin";
import { useNotify } from "react-admin"; import { useNotify } from "react-admin";
import { useDataProvider } from "react-admin"; import { useDataProvider } from "react-admin";
import { useState, useEffect } from "react";
import { Stack, Switch, Typography } from "@mui/material";
import { ExperimentalFeaturesModel, SynapseDataProvider } from "../synapse/dataProvider"; import { ExperimentalFeaturesModel, SynapseDataProvider } from "../synapse/dataProvider";
const experimentalFeaturesMap = { const experimentalFeaturesMap = {
msc3881: "enable remotely toggling push notifications for another client", msc3881: "enable remotely toggling push notifications for another client",
msc3575: "enable experimental sliding sync support", msc3575: "enable experimental sliding sync support",
}; };
const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boolean, updateFeature: (feature_name: string, feature_value: boolean) => void}) => { const ExperimentalFeatureRow = (props: {
featureKey: string;
featureValue: boolean;
updateFeature: (feature_name: string, feature_value: boolean) => void;
}) => {
const featureKey = props.featureKey; const featureKey = props.featureKey;
const featureValue = props.featureValue; const featureValue = props.featureValue;
const featureDescription = experimentalFeaturesMap[featureKey] ?? ""; const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
@@ -20,34 +25,33 @@ const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boole
props.updateFeature(featureKey, event.target.checked); props.updateFeature(featureKey, event.target.checked);
}; };
return <Stack return (
<Stack
direction="row" direction="row"
spacing={2} spacing={2}
alignItems="start" alignItems="start"
sx={{ sx={{
padding: 2, padding: 2,
}} }}
> >
<Switch checked={checked} onChange={handleChange} /> <Switch checked={checked} onChange={handleChange} />
<Stack> <Stack>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
fontWeight: "medium", fontWeight: "medium",
color: "text.primary" color: "text.primary",
}} }}
> >
{featureKey} {featureKey}
</Typography> </Typography>
<Typography <Typography variant="body2" color="text.secondary">
variant="body2"
color="text.secondary"
>
{featureDescription} {featureDescription}
</Typography> </Typography>
</Stack>
</Stack> </Stack>
</Stack> );
} };
export const ExperimentalFeaturesList = () => { export const ExperimentalFeaturesList = () => {
const record = useRecordContext(); const record = useRecordContext();
@@ -62,36 +66,35 @@ export const ExperimentalFeaturesList = () => {
const fetchFeatures = async () => { const fetchFeatures = async () => {
const features = await dataProvider.getFeatures(record.id); const features = await dataProvider.getFeatures(record.id);
setFeatures(features); setFeatures(features);
} };
fetchFeatures(); fetchFeatures();
}, []); }, []);
const updateFeature = async (feature_name: string, feature_value: boolean) => { const updateFeature = async (feature_name: string, feature_value: boolean) => {
const updatedFeatures = {...features, [feature_name]: feature_value} as ExperimentalFeaturesModel; const updatedFeatures = { ...features, [feature_name]: feature_value } as ExperimentalFeaturesModel;
setFeatures(updatedFeatures); setFeatures(updatedFeatures);
const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures); const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures);
notify("ra.notification.updated", { notify("ra.notification.updated", {
messageArgs: { smart_count: 1 }, messageArgs: { smart_count: 1 },
type: "success", type: "success",
}); });
}; };
return <> return (
<Stack <>
direction="column" <Stack direction="column" spacing={1}>
spacing={1} {Object.keys(features).map((featureKey: string) => (
> <ExperimentalFeatureRow
{Object.keys(features).map((featureKey: string) => key={featureKey}
<ExperimentalFeatureRow featureKey={featureKey}
key={featureKey} featureValue={features[featureKey]}
featureKey={featureKey} updateFeature={updateFeature}
featureValue={features[featureKey]} />
updateFeature={updateFeature} ))}
/> </Stack>
)} </>
</Stack> );
</> };
}
export default ExperimentalFeaturesList; export default ExperimentalFeaturesList;

View File

@@ -1,6 +1,6 @@
import { Avatar, Box, Link, Typography } from "@mui/material"; import { Avatar, Box, Link, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useEffect, useState } from "react";
const Footer = () => { const Footer = () => {
const [version, setVersion] = useState<string | null>(null); const [version, setVersion] = useState<string | null>(null);
@@ -13,36 +13,46 @@ const Footer = () => {
} }
}, []); }, []);
return (<Box return (
component="footer" <Box
sx={{ component="footer"
position: 'fixed', sx={{
zIndex: 100, position: "fixed",
bottom: 0, zIndex: 100,
width: '100%', bottom: 0,
bgcolor: theme.palette.background.default, width: "100%",
color: theme.palette.text.primary, bgcolor: theme.palette.background.default,
borderTop: '1px solid', color: theme.palette.text.primary,
borderColor: theme.palette.divider, borderTop: "1px solid",
fontSize: '0.89rem', borderColor: theme.palette.divider,
display: 'flex', fontSize: "0.89rem",
flexDirection: 'row', display: "flex",
alignItems: 'center', flexDirection: "row",
justifyContent: 'start', alignItems: "center",
p: 1, justifyContent: "start",
gap: '10px' p: 1,
}}> gap: "10px",
<Avatar src="./images/logo.webp" sx={{ width: "1rem", height: "1rem", display: "inline-block", verticalAlign: "sub" }} /> }}
>
<Avatar
src="./images/logo.webp"
sx={{ width: "1rem", height: "1rem", display: "inline-block", verticalAlign: "sub" }}
/>
<Link href="https://github.com/etkecc/synapse-admin" target="_blank"> <Link href="https://github.com/etkecc/synapse-admin" target="_blank">
Synapse Admin {version} Synapse Admin {version}
</Link> </Link>
by by
<Link href="https://etke.cc/?utm_source=synapse-admin&utm_medium=footer&utm_campaign=synapse-admin" target="_blank"> <Link
href="https://etke.cc/?utm_source=synapse-admin&utm_medium=footer&utm_campaign=synapse-admin"
target="_blank"
>
etke.cc etke.cc
</Link> </Link>
(originally developed by Awesome Technologies Innovationslabor GmbH). (originally developed by Awesome Technologies Innovationslabor GmbH).
<Link sx={{ fontWeight: 'bold' }} href="https://matrix.to/#/#synapse-admin:etke.cc" target="_blank">#synapse-admin:etke.cc</Link> <Link sx={{ fontWeight: "bold" }} href="https://matrix.to/#/#synapse-admin:etke.cc" target="_blank">
</Box> #synapse-admin:etke.cc
</Link>
</Box>
); );
}; };

View File

@@ -1,59 +1,58 @@
import { styled } from "@mui/material/styles";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { styled } from "@mui/material/styles";
const LoginFormBox = styled(Box)(({ theme }) => ({ const LoginFormBox = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
minHeight: "calc(100vh - 1rem)", 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)",
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.background.default : theme.palette.background.paper, backgroundColor: theme.palette.mode === "dark" ? theme.palette.background.default : theme.palette.background.paper,
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundSize: "cover", backgroundSize: "cover",
[`& .card`]: { [`& .card`]: {
width: "30rem", width: "30rem",
marginTop: "6rem", marginTop: "6rem",
marginBottom: "6rem", marginBottom: "6rem",
}, },
[`& .avatar`]: { [`& .avatar`]: {
margin: "1rem", margin: "1rem",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
}, },
[`& .icon`]: { [`& .icon`]: {
backgroundColor: theme.palette.grey[500], backgroundColor: theme.palette.grey[500],
}, },
[`& .hint`]: { [`& .hint`]: {
marginTop: "1em", marginTop: "1em",
marginBottom: "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 1rem 1rem 1rem", padding: "0 1rem 1rem 1rem",
}, },
[`& .select`]: { [`& .select`]: {
marginBottom: "2rem", marginBottom: "2rem",
}, },
[`& .actions`]: { [`& .actions`]: {
padding: "0 1rem 1rem 1rem", 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",
marginLeft: "0.5rem", marginLeft: "0.5rem",
}, },
[`& .matrixVersions`]: { [`& .matrixVersions`]: {
color: theme.palette.grey[500], color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif", fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem", fontSize: "0.8rem",
marginBottom: "1rem", marginBottom: "1rem",
marginLeft: "0.5rem", marginLeft: "0.5rem",
}, },
} }));
));
export default LoginFormBox; export default LoginFormBox;

View File

@@ -1,8 +1,8 @@
import { useState } from "react";
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@mui/icons-material/Cancel";
import MessageIcon from "@mui/icons-material/Message"; import MessageIcon from "@mui/icons-material/Message";
import { Dialog, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { Dialog, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { import {
Button, Button,
RaRecord, RaRecord,
@@ -20,7 +20,6 @@ import {
useTranslate, useTranslate,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "@tanstack/react-query";
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => { const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate(); const translate = useTranslate();
@@ -104,7 +103,7 @@ export const ServerNoticeBulkButton = () => {
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
const { mutate: sendNotices, isPending } = useMutation({ const { mutate: sendNotices, isPending } = useMutation({
mutationFn: (data) => mutationFn: data =>
dataProvider.createMany("servernotices", { dataProvider.createMany("servernotices", {
ids: selectedIds, ids: selectedIds,
data: data, data: data,

View File

@@ -1,7 +1,8 @@
import { useDataProvider, useRecordContext, useTranslate } from "react-admin"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { useEffect, useState } from "react";
import { Typography, Box, Stack, Accordion, AccordionSummary, AccordionDetails } from "@mui/material"; import { Typography, Box, Stack, Accordion, AccordionSummary, AccordionDetails } from "@mui/material";
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import { useEffect, useState } from "react";
import { useDataProvider, useRecordContext, useTranslate } from "react-admin";
import { SynapseDataProvider } from "../synapse/dataProvider"; import { SynapseDataProvider } from "../synapse/dataProvider";
const UserAccountData = () => { const UserAccountData = () => {
@@ -25,41 +26,43 @@ const UserAccountData = () => {
}, []); }, []);
if (Object.keys(globalAccountData).length === 0 && Object.keys(roomsAccountData).length === 0) { if (Object.keys(globalAccountData).length === 0 && Object.keys(roomsAccountData).length === 0) {
return <Typography variant="body2">{translate('ra.navigation.no_results', { return (
resource: 'Account Data', <Typography variant="body2">
_: 'No results found.', {translate("ra.navigation.no_results", {
})}</Typography>; resource: "Account Data",
_: "No results found.",
})}
</Typography>
);
} }
return <> return (
<Stack <>
direction="column" <Stack direction="column" spacing={2} width="100%">
spacing={2} <Typography variant="h6">{translate("resources.users.account_data.title")}</Typography>
width="100%" <Typography variant="body1">
> <Box>
<Typography variant="h6">{translate('resources.users.account_data.title')}</Typography> <Accordion>
<Typography variant="body1"> <AccordionSummary expandIcon={<ArrowDownwardIcon />}>
<Box> <Typography variant="h6">{translate("resources.users.account_data.global")}</Typography>
<Accordion> </AccordionSummary>
<AccordionSummary expandIcon={<ArrowDownwardIcon />}> <AccordionDetails>
<Typography variant="h6">{translate('resources.users.account_data.global')}</Typography> <Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(globalAccountData, null, 4)}</Box>
</AccordionSummary> </AccordionDetails>
<AccordionDetails> </Accordion>
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(globalAccountData, null, 4)}</Box> <Accordion>
</AccordionDetails> <AccordionSummary expandIcon={<ArrowDownwardIcon />}>
</Accordion> <Typography variant="h6">{translate("resources.users.account_data.rooms")}</Typography>
<Accordion> </AccordionSummary>
<AccordionSummary expandIcon={<ArrowDownwardIcon />}> <AccordionDetails>
<Typography variant="h6">{translate('resources.users.account_data.rooms')}</Typography> <Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box>
</AccordionSummary> </AccordionDetails>
<AccordionDetails> </Accordion>
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box> </Box>
</AccordionDetails> </Typography>
</Accordion> </Stack>
</Box> </>
</Typography> );
</Stack> };
</>
}
export default UserAccountData; export default UserAccountData;

View File

@@ -1,575 +0,0 @@
import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
import { ChangeEvent, useState } from "react";
import {
Button,
Card,
CardActions,
CardContent,
CardHeader,
Checkbox,
Container,
FormControlLabel,
NativeSelect,
} from "@mui/material";
import { DataProvider, useTranslate } from "ra-core";
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
import { generateRandomMXID, returnMXID } from "../utils/mxid";
import { generateRandomPassword } from "../utils/password";
const LOGGING = true;
const expectedFields = ["id", "displayname"].sort();
function TranslatableOption({ value, text }) {
const translate = useTranslate();
return <option value={value}>{translate(text)}</option>;
}
type Progress = {
done: number;
limit: number;
} | null;
interface ImportLine {
id: string;
displayname: string;
user_type?: string;
name?: string;
deactivated?: boolean;
guest?: boolean;
admin?: boolean;
is_admin?: boolean;
password?: string;
avatar_url?: string;
}
interface ChangeStats {
total: number;
id: number;
is_guest: number;
admin: number;
password: number;
}
interface ImportResult {
skippedRecords: RaRecord[];
erroredRecords: RaRecord[];
succeededRecords: RaRecord[];
totalRecordCount: number;
changeStats: ChangeStats;
wasDryRun: boolean;
}
const FilePicker = () => {
const [values, setValues] = useState<ImportLine[]>([]);
const [error, setError] = useState<string | string[] | null>(null);
const [stats, setStats] = useState<ChangeStats | null>(null);
const [dryRun, setDryRun] = useState(true);
const [progress, setProgress] = useState<Progress>(null);
const [importResults, setImportResults] = useState<ImportResult | null>(null);
const [skippedRecords, setSkippedRecords] = useState<string>("");
const [conflictMode, setConflictMode] = useState("stop");
const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState("update");
const translate = useTranslate();
const notify = useNotify();
const dataProvider = useDataProvider();
const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
if (progress !== null) return;
setValues([]);
setError(null);
setStats(null);
setImportResults(null);
const file = e.target.files ? e.target.files[0] : null;
if (!file) return;
/* Let's refuse some unreasonably big files instead of freezing
* up the browser */
if (file.size > 100000000) {
const message = translate("import_users.errors.unreasonably_big", {
size: (file.size / (1024 * 1024)).toFixed(2),
});
notify(message);
setError(message);
return;
}
try {
parseCsv<ImportLine>(file, {
header: true,
skipEmptyLines: true /* especially for a final EOL in the csv file */,
complete: result => {
if (result.errors) {
setError(result.errors.map(e => e.toString()));
}
/* Papaparse is very lenient, we may be able to salvage
* the data in the file. */
verifyCsv(result, { setValues, setStats, setError });
},
});
} catch {
setError("Unknown error");
return null;
}
};
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
/* First, verify the presence of required fields */
meta.fields = meta.fields?.map(f => f.trim().toLowerCase());
const missingFields = expectedFields.filter(eF => !meta.fields?.find(mF => eF === mF));
if (missingFields.length > 0) {
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
return false;
}
// XXX after deciding on how "name" and friends should be handled below,
// this place will want changes, too.
/* Collect some stats to prevent sneaky csv files from adding admin
users or something.
*/
const stats = {
user_types: { default: 0 },
is_guest: 0,
admin: 0,
deactivated: 0,
password: 0,
avatar_url: 0,
id: 0,
total: data.length,
};
const errorMessages = errors.map(e => e.message);
// sanitize the data first
data = data.map(line => {
const newLine = {} as ImportLine;
for (const [key, value] of Object.entries(line)) {
newLine[key.trim().toLowerCase()] = value;
}
return newLine;
});
// process the data
data.forEach((line, idx) => {
if (line.user_type === undefined || line.user_type === "") {
stats.user_types.default++;
} else {
stats.user_types[line.user_type] += 1;
}
/* XXX correct the csv export that react-admin offers for the users
* resource so it gives sensible field names and doesn't duplicate
* id as "name"?
*/
if (meta.fields?.includes("name")) {
delete line.name;
}
if (meta.fields?.includes("user_type")) {
delete line.user_type;
}
if (meta.fields?.includes("is_admin")) {
delete line.is_admin;
}
["is_guest", "admin", "deactivated"].forEach(f => {
if (line[f] === "true") {
stats[f]++;
line[f] = true; // we need true booleans instead of strings
} else {
if (line[f] !== "false" && line[f] !== "") {
console.log("invalid value", line[f], "for field " + f + " in row " + idx);
errorMessages.push(
translate("import_users.error.invalid_value", {
field: f,
row: idx,
})
);
}
line[f] = false; // default values to false
}
});
if (line.password !== undefined && line.password !== "") {
stats.password++;
}
if (line.avatar_url !== undefined && line.avatar_url !== "") {
stats.avatar_url++;
}
if (line.id !== undefined && line.id !== "") {
stats.id++;
}
});
if (errorMessages.length > 0) {
setError(errorMessages);
}
setStats(stats);
setValues(data);
return true;
};
const runImport = async () => {
if (progress !== null) {
notify("import_users.errors.already_in_progress");
return;
}
const results = await doImport(
dataProvider,
values,
conflictMode,
passwordMode,
useridMode,
dryRun,
setProgress,
setError
);
setImportResults(results);
// offer CSV download of skipped or errored records
// (so that the user doesn't have to filter out successful
// records manually when fixing stuff in the CSV)
setSkippedRecords(unparseCsv(results.skippedRecords));
if (LOGGING) console.log("Skipped records:");
if (LOGGING) console.log(skippedRecords);
};
// XXX every single one of the requests will restart the activity indicator
// which doesn't look very good.
const doImport = async (
dataProvider: DataProvider,
data: ImportLine[],
conflictMode: string,
passwordMode: boolean,
useridMode: string,
dryRun: boolean,
setProgress: (progress: Progress) => void,
setError: (message: string) => void
): Promise<ImportResult> => {
const skippedRecords: ImportLine[] = [];
const erroredRecords: ImportLine[] = [];
const succeededRecords: ImportLine[] = [];
const changeStats: ChangeStats = {
total: 0,
id: 0,
is_guest: 0,
admin: 0,
password: 0,
};
let entriesDone = 0;
const entriesCount = data.length;
try {
setProgress({ done: entriesDone, limit: entriesCount });
for (const entry of data) {
const userRecord = { ...entry };
// No need to do a bunch of cryptographic random number getting if
// we are using neither a generated password nor a generated user id.
if (useridMode === "ignore" || userRecord.id === undefined || userRecord.id === "") {
userRecord.id = generateRandomMXID();
}
if (passwordMode === false || entry.password === undefined || entry.password === "") {
userRecord.password = generateRandomPassword();
}
// we want to ensure that the ID is always full MXID, otherwise randomly-generated MXIDs will be in the full
// form, but the ones from the CSV will be localpart-only.
userRecord.id = returnMXID(userRecord.id);
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
/* For these modes we will consider the ID that's in the record.
* If the mode is "stop", we will not continue adding more records, and
* we will offer information on what was already added and what was
* skipped.
*
* If the mode is "skip", we record the record for later, but don't
* send it to the server.
*
* If the mode is "update", we change fields that are reasonable to
* update.
* - If the "password mode" is "true" (i.e. "use passwords from csv"):
* - if the record has a password
* - send the password along with the record
* - if the record has no password
* - generate a new password
* - If the "password mode" is "false"
* - never generate a new password to update existing users with
*/
/* We just act as if there are no IDs in the CSV, so every user will be
* created anew.
* We do a simple retry loop so that an accidental hit on an existing ID
* doesn't trip us up.
*/
if (LOGGING) console.log("will check for existence of record " + JSON.stringify(userRecord));
let retries = 0;
const submitRecord = (recordData: ImportLine) => {
return dataProvider.getOne("users", { id: recordData.id }).then(
async () => {
if (LOGGING) console.log("already existed");
if (useridMode === "update" || conflictMode === "skip") {
skippedRecords.push(recordData);
} else if (conflictMode === "stop") {
throw new Error(
translate("import_users.error.id_exits", {
id: recordData.id,
})
);
} else {
const newRecordData = Object.assign({}, recordData, {
id: generateRandomMXID(),
});
retries++;
if (retries > 512) {
console.warn("retry loop got stuck? pathological situation?");
skippedRecords.push(recordData);
} else {
await submitRecord(newRecordData);
}
}
},
async () => {
if (LOGGING) console.log("OK to create record " + recordData.id + " (" + recordData.displayname + ").");
if (!dryRun) {
await dataProvider.create("users", { data: recordData });
}
succeededRecords.push(recordData);
}
);
};
await submitRecord(userRecord);
entriesDone++;
setProgress({ done: entriesDone, limit: data.length });
}
setProgress(null);
} catch (e) {
setError(
translate("import_users.error.at_entry", {
entry: entriesDone + 1,
message: e instanceof Error ? e.message : String(e),
})
);
setProgress(null);
}
return {
skippedRecords,
erroredRecords,
succeededRecords,
totalRecordCount: entriesCount,
changeStats,
wasDryRun: dryRun,
};
};
const downloadSkippedRecords = () => {
const element = document.createElement("a");
console.log(skippedRecords);
const file = new Blob([skippedRecords], {
type: "text/comma-separated-values",
});
element.href = URL.createObjectURL(file);
element.download = "skippedRecords.csv";
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
};
const onConflictModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
if (progress !== null) {
return;
}
const value = e.target.value;
setConflictMode(value);
};
const onPasswordModeChange = (e: ChangeEvent<HTMLInputElement>) => {
if (progress !== null) {
return;
}
setPasswordMode(e.target.checked);
};
const onUseridModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
if (progress !== null) {
return;
}
const value = e.target.value;
setUseridMode(value);
};
const onDryRunModeChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (progress !== null) {
return;
}
setDryRun(e.target.checked);
};
// render individual small components
const statsCards = stats &&
!importResults && [
<Container>
<CardHeader title={translate("import_users.cards.importstats.header")} />
<CardContent>
<div>{translate("import_users.cards.importstats.users_total", stats.total)}</div>
<div>{translate("import_users.cards.importstats.guest_count", stats.is_guest)}</div>
<div>{translate("import_users.cards.importstats.admin_count", stats.admin)}</div>
</CardContent>
</Container>,
<Container>
<CardHeader title={translate("import_users.cards.ids.header")} />
<CardContent>
<div>
{stats.id === stats.total
? translate("import_users.cards.ids.all_ids_present")
: translate("import_users.cards.ids.count_ids_present", stats.id)}
</div>
{stats.id > 0 ? (
<div>
<NativeSelect onChange={onUseridModeChanged} value={useridMode} disabled={progress !== null}>
<TranslatableOption value="ignore" text="import_users.cards.ids.mode.ignore" />
<TranslatableOption value="update" text="import_users.cards.ids.mode.update" />
</NativeSelect>
</div>
) : (
""
)}
</CardContent>
</Container>,
<Container>
<CardHeader title={translate("import_users.cards.passwords.header")} />
<CardContent>
<div>
{stats.password === stats.total
? translate("import_users.cards.passwords.all_passwords_present")
: translate("import_users.cards.passwords.count_passwords_present", stats.password)}
</div>
{stats.password > 0 ? (
<div>
<FormControlLabel
control={
<Checkbox checked={passwordMode} disabled={progress !== null} onChange={onPasswordModeChange} />
}
label={translate("import_users.cards.passwords.use_passwords")}
/>
</div>
) : (
""
)}
</CardContent>
</Container>,
];
const conflictCards = stats && !importResults && (
<Container>
<CardHeader title={translate("import_users.cards.conflicts.header")} />
<CardContent>
<div>
<NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
<TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
<TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
</NativeSelect>
</div>
</CardContent>
</Container>
);
const errorCards = error && (
<Container>
<CardHeader title={translate("import_users.error.error")} />
<CardContent>
{(Array.isArray(error) ? error : [error]).map(e => (
<div>{e}</div>
))}
</CardContent>
</Container>
);
const uploadCard = !importResults && (
<Container>
<CardHeader title={translate("import_users.cards.upload.header")} />
<CardContent>
{translate("import_users.cards.upload.explanation")}
<a href="./data/example.csv">example.csv</a>
<br />
<br />
<input type="file" onChange={onFileChange} disabled={progress !== null} />
</CardContent>
</Container>
);
const resultsCard = importResults && (
<CardContent>
<CardHeader title={translate("import_users.cards.results.header")} />
<div>
{translate("import_users.cards.results.total", importResults.totalRecordCount)}
<br />
{translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
<br />
{importResults.skippedRecords.length
? [
translate("import_users.cards.results.skipped", importResults.skippedRecords.length),
<div>
<button onClick={downloadSkippedRecords}>
{translate("import_users.cards.results.download_skipped")}
</button>
</div>,
<br />,
]
: ""}
{importResults.erroredRecords.length
? [translate("import_users.cards.results.skipped", importResults.erroredRecords.length), <br />]
: ""}
<br />
{importResults.wasDryRun && [translate("import_users.cards.results.simulated_only"), <br />]}
</div>
</CardContent>
);
const startImportCard =
!values || values.length === 0 || importResults ? undefined : (
<CardActions>
<FormControlLabel
control={<Checkbox checked={dryRun} onChange={onDryRunModeChanged} disabled={progress !== null} />}
label={translate("import_users.cards.startImport.simulate_only")}
/>
<Button size="large" onClick={runImport} disabled={progress !== null}>
{translate("import_users.cards.startImport.run_import")}
</Button>
{progress !== null ? (
<div>
{progress.done} of {progress.limit} done
</div>
) : null}
</CardActions>
);
const allCards: React.JSX.Element[] = [];
if (uploadCard) allCards.push(uploadCard);
if (errorCards) allCards.push(errorCards);
if (conflictCards) allCards.push(conflictCards);
if (statsCards) allCards.push(...statsCards);
if (startImportCard) allCards.push(startImportCard);
if (resultsCard) allCards.push(resultsCard);
const cardContainer = <Card>{allCards}</Card>;
return [<Title defaultTitle={translate("import_users.title")} />, cardContainer];
};
export const UserImport = FilePicker;
export default UserImport;

View File

@@ -1,10 +1,18 @@
import { Stack, Typography } from "@mui/material"; import { Stack, Typography } from "@mui/material";
import { TextField } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin"; import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin";
import { TextField } from "@mui/material";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value: any, updateRateLimit: (limit: string, value: any) => void }) => { const RateLimitRow = ({
limit,
value,
updateRateLimit,
}: {
limit: string;
value: any;
updateRateLimit: (limit: string, value: any) => void;
}) => {
const translate = useTranslate(); const translate = useTranslate();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -16,35 +24,34 @@ const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value:
updateRateLimit(limit, value); updateRateLimit(limit, value);
}; };
return <Stack return (
spacing={1} <Stack
alignItems="start" spacing={1}
sx={{ alignItems="start"
sx={{
padding: 2, padding: 2,
}}
>
<TextField
id="outlined-number"
type="number"
value={value}
onChange={handleChange}
slotProps={{
inputLabel: {
shrink: true,
},
}} }}
label={translate(`resources.users.limits.${limit}`)} >
/> <TextField
<Stack> id="outlined-number"
<Typography type="number"
variant="body2" value={value}
color="text.secondary" onChange={handleChange}
> slotProps={{
{translate(`resources.users.limits.${limit}_text`)} inputLabel: {
</Typography> shrink: true,
},
}}
label={translate(`resources.users.limits.${limit}`)}
/>
<Stack>
<Typography variant="body2" color="text.secondary">
{translate(`resources.users.limits.${limit}_text`)}
</Typography>
</Stack>
</Stack> </Stack>
</Stack> );
} };
const UserRateLimits = () => { const UserRateLimits = () => {
const translate = useTranslate(); const translate = useTranslate();
@@ -62,36 +69,31 @@ const UserRateLimits = () => {
} }
useEffect(() => { useEffect(() => {
const fetchRateLimits = async () => { const fetchRateLimits = async () => {
const rateLimits = await dataProvider.getRateLimits(record.id); const rateLimits = await dataProvider.getRateLimits(record.id);
if (Object.keys(rateLimits).length > 0) { if (Object.keys(rateLimits).length > 0) {
setRateLimits(rateLimits); setRateLimits(rateLimits);
}
} }
};
fetchRateLimits(); fetchRateLimits();
}, []); }, []);
const updateRateLimit = async (limit: string, value: any) => { const updateRateLimit = async (limit: string, value: any) => {
let updatedRateLimits = { ...rateLimits, [limit]: value }; const updatedRateLimits = { ...rateLimits, [limit]: value };
setRateLimits(updatedRateLimits); setRateLimits(updatedRateLimits);
form.setValue(`rates.${limit}`, value, { shouldDirty: true }); form.setValue(`rates.${limit}`, value, { shouldDirty: true });
}; };
return <> return (
<Stack <>
direction="column" <Stack direction="column">
> {Object.keys(rateLimits).map((limit: string) => (
{Object.keys(rateLimits).map((limit: string) => <RateLimitRow key={limit} limit={limit} value={rateLimits[limit]} updateRateLimit={updateRateLimit} />
<RateLimitRow ))}
key={limit} </Stack>
limit={limit} </>
value={rateLimits[limit]} );
updateRateLimit={updateRateLimit}
/>
)}
</Stack>
</>
}; };
export default UserRateLimits; export default UserRateLimits;

View File

@@ -0,0 +1,37 @@
import { Stack, Tooltip, Typography, Box, Link } from "@mui/material";
import { useStore } from "react-admin";
import { ServerProcessResponse } from "../../synapse/dataProvider";
import { getTimeSince } from "../../utils/date";
const CurrentlyRunningCommand = () => {
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess;
if (!command || !locked_at) {
return null;
}
return (
<Stack spacing={1} direction="row" alignItems="center">
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Typography variant="h5">Currently running:</Typography>
<Typography variant="h5" color="text.secondary">
<Link href={"https://etke.cc/help/extras/scheduler/#" + command} target="_blank">
{command}
</Link>
<Tooltip title={locked_at.toString()}>
<Typography component="span" color="text.secondary" sx={{ display: "inline-block", ml: 1 }}>
(started {getTimeSince(locked_at)} ago)
</Typography>
</Tooltip>
</Typography>
</Box>
</Stack>
);
};
export default CurrentlyRunningCommand;

View File

@@ -47,10 +47,21 @@ In the application bar the new notifications icon is displayed that shows the nu
When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server. When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server.
### Server Actions Page
![Server Actions Page](../../../screenshots/etke.cc/server-actions/page.webp)
When you click on the `Server Actions` sidebar menu item, you will be redirected to the Server Actions page.
On this page you can do the following:
* [Run a command](#server-commands-panel) on your server immediately
* [Schedule a command](https://etke.cc/help/extras/scheduler/#schedule) to run at a specific date and time
* [Configure a recurring schedule](https://etke.cc/help/extras/scheduler/#recurring) for a command to run at a specific time every week
### Server Commands Panel ### Server Commands Panel
![Server Commands Panel](../../../screenshots/etke.cc/server-commands/panel.webp) ![Server Commands Panel](../../../screenshots/etke.cc/server-commands/panel.webp)
When you open [Server Status page](#server-status-page), you will see the Server Commands panel. This panel contains all When you open [Server Actions page](#server-status-page), you will see the Server Commands panel.
[the commands](https://etke.cc/help/extras/scheduler/#commands) you can run on your server in 1 click. Once command is finished, you will get a notification about the This panel contains all [the commands](https://etke.cc/help/extras/scheduler/#commands) you can run on your server in 1 click.
result. Once command is finished, you will get a notification about the result.

View File

@@ -0,0 +1,51 @@
import RestoreIcon from "@mui/icons-material/Restore";
import ScheduleIcon from "@mui/icons-material/Schedule";
import { Box, Typography, Link, Divider } from "@mui/material";
import { Stack } from "@mui/material";
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
import ServerCommandsPanel from "./ServerCommandsPanel";
import RecurringCommandsList from "./schedules/components/recurring/RecurringCommandsList";
import ScheduledCommandsList from "./schedules/components/scheduled/ScheduledCommandsList";
const ServerActionsPage = () => {
return (
<Stack spacing={3} mt={3}>
<Stack direction="column">
<CurrentlyRunningCommand />
<ServerCommandsPanel />
</Stack>
<Box sx={{ mt: 2 }}>
<Typography variant="h5">
<ScheduleIcon sx={{ verticalAlign: "middle", mr: 1 }} /> Scheduled commands
</Typography>
<Typography variant="body1">
The following commands are scheduled to run at specific times. You can view their details and modify them as
needed. More details about the mode can be found{" "}
<Link href="https://etke.cc/help/extras/scheduler/#schedule" target="_blank">
here
</Link>
.
</Typography>
<ScheduledCommandsList />
</Box>
<Box sx={{ mt: 2 }}>
<Typography variant="h5">
<RestoreIcon sx={{ verticalAlign: "middle", mr: 1 }} /> Recurring commands
</Typography>
<Typography variant="body1">
The following commands are set to run at specific weekday and time (weekly). You can view their details and
modify them as needed. More details about the mode can be found{" "}
<Link href="https://etke.cc/help/extras/scheduler/#recurring" target="_blank">
here
</Link>
.
</Typography>
<RecurringCommandsList />
</Box>
</Stack>
);
};
export default ServerActionsPage;

View File

@@ -1,167 +1,191 @@
import { PlayArrow, CheckCircle, HelpCenter, Construction } from "@mui/icons-material";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Alert,
TextField,
Box,
Link,
Typography,
} from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin"; import { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin";
import { useAppContext } from "../../Context"; import { Link as RouterLink } from "react-router-dom";
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Alert, TextField, Box } from "@mui/material";
import { PlayArrow, CheckCircle } from "@mui/icons-material";
import { Icons } from "../../utils/icons";
import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider";
import { Link } from 'react-router-dom';
import { useAppContext } from "../../Context";
import { useServerCommands } from "./hooks/useServerCommands";
import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider";
import { Icons } from "../../utils/icons";
const renderIcon = (icon: string) => { const renderIcon = (icon: string) => {
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined; const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null; return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null;
} };
const ServerCommandsPanel = () => { const ServerCommandsPanel = () => {
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
if (!etkeccAdmin) {
return null;
}
const createPath = useCreatePath(); const createPath = useCreatePath();
const { isLoading, serverCommands, setServerCommands } = useServerCommands();
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== "");
const [commandResult, setCommandResult] = useState<React.ReactNode[]>([]);
const dataProvider = useDataProvider();
const [ isLoading, setLoading ] = useState(true); useEffect(() => {
const [serverCommands, setServerCommands] = useState<{ [key: string]: ServerCommand }>({}); if (serverProcess.command === "") {
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); setCommandIsRunning(false);
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== ""); }
const [commandResult, setCommandResult] = useState<any[]>([]); }, [serverProcess]);
const dataProvider = useDataProvider(); const setCommandAdditionalArgs = (command: string, additionalArgs: string) => {
const updatedServerCommands = { ...serverCommands };
updatedServerCommands[command].additionalArgs = additionalArgs;
setServerCommands(updatedServerCommands);
};
useEffect(() => { const runCommand = async (command: string) => {
const fetchIsAdmin = async () => { setCommandResult([]);
const serverCommandsResponse = await dataProvider.getServerCommands(etkeccAdmin); setCommandIsRunning(true);
if (serverCommandsResponse) {
const serverCommands = serverCommandsResponse;
Object.keys(serverCommandsResponse).forEach((command: string) => {
serverCommands[command].additionalArgs = "";
});
setServerCommands(serverCommands);
}
setLoading(false);
}
fetchIsAdmin();
}, []);
useEffect(() => { try {
if (serverProcess.command === "") { const additionalArgs = serverCommands[command].additionalArgs || "";
const requestParams = additionalArgs ? { args: additionalArgs } : {};
const response = await dataProvider.runServerCommand(etkeccAdmin, command, requestParams);
if (!response.success) {
setCommandIsRunning(false); setCommandIsRunning(false);
return;
} }
}, [serverProcess]);
const setCommandAdditionalArgs = (command: string, additionalArgs: string) => { // Update UI with success message
const updatedServerCommands = {...serverCommands}; const commandResults = buildCommandResultMessages(command, additionalArgs);
updatedServerCommands[command].additionalArgs = additionalArgs; setCommandResult(commandResults);
setServerCommands(updatedServerCommands);
// Reset the additional args field
resetCommandArgs(command);
// Update server process status
await updateServerProcessStatus(serverCommands[command]);
} catch (error) {
setCommandIsRunning(false);
}
};
const buildCommandResultMessages = (command: string, additionalArgs: string): React.ReactNode[] => {
const results: React.ReactNode[] = [];
let commandScheduledText = `Command scheduled: ${command}`;
if (additionalArgs) {
commandScheduledText += `, with additional args: ${additionalArgs}`;
} }
const runCommand = async (command: string) => { results.push(<Box key="command-text">{commandScheduledText}</Box>);
setCommandResult([]); results.push(
setCommandIsRunning(true); <Box key="notification-link">
Expect your result in the{" "}
<RouterLink to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</RouterLink> page
soon.
</Box>
);
try { return results;
const additionalArgs = serverCommands[command].additionalArgs || ""; };
const requestParams = additionalArgs ? { args: additionalArgs } : {};
const response = await dataProvider.runServerCommand(etkeccAdmin, command, requestParams); const resetCommandArgs = (command: string) => {
const updatedServerCommands = { ...serverCommands };
updatedServerCommands[command].additionalArgs = "";
setServerCommands(updatedServerCommands);
};
if (!response.success) { const updateServerProcessStatus = async (command: ServerCommand) => {
setCommandIsRunning(false); const commandIsLocking = command.with_lock;
return; const serverProcess = await dataProvider.getServerRunningProcess(etkeccAdmin, true);
} if (!commandIsLocking && serverProcess.command === "") {
// if command is not locking, we simulate the "lock" mechanism so notifications will be refetched
// Update UI with success message serverProcess["command"] = command.name;
const commandResults = buildCommandResultMessages(command, additionalArgs); serverProcess["locked_at"] = new Date().toISOString();
setCommandResult(commandResults);
// Reset the additional args field
resetCommandArgs(command);
// Update server process status
await updateServerProcessStatus(serverCommands[command]);
} catch (error) {
setCommandIsRunning(false);
}
};
const buildCommandResultMessages = (command: string, additionalArgs: string): React.ReactNode[] => {
const results: React.ReactNode[] = [];
let commandScheduledText = `Command scheduled: ${command}`;
if (additionalArgs) {
commandScheduledText += `, with additional args: ${additionalArgs}`;
}
results.push(<Box>{commandScheduledText}</Box>);
results.push(
<Box>
Expect your result in the <Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon.
</Box>
);
return results;
};
const resetCommandArgs = (command: string) => {
const updatedServerCommands = {...serverCommands};
updatedServerCommands[command].additionalArgs = "";
setServerCommands(updatedServerCommands);
};
const updateServerProcessStatus = async (command: ServerCommand) => {
const commandIsLocking = command.with_lock;
const serverProcess = await dataProvider.getServerRunningProcess(etkeccAdmin, true);
if (!commandIsLocking && serverProcess.command === "") {
// if command is not locking, we simulate the "lock" mechanism so notifications will be refetched
serverProcess["command"] = command.name;
serverProcess["locked_at"] = new Date().toISOString();
}
setServerProcess({...serverProcess});
};
if (isLoading) {
return <Loading />
} }
return (<> setServerProcess({ ...serverProcess });
<h2>Server Commands</h2> };
<TableContainer component={Paper}>
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table"> if (isLoading) {
<TableHead> return <Loading />;
}
return (
<>
<Typography variant="h5">
<Construction sx={{ verticalAlign: "middle", mr: 1 }} /> Available Commands
</Typography>
<Typography variant="body1" sx={{ mt: 0 }}>
The following commands are available to run. More details about each of them can be found{" "}
<Link href="https://etke.cc/help/extras/scheduler/#commands" target="_blank">
here
</Link>
.
</Typography>
<TableContainer component={Paper} sx={{ mt: 2 }}>
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table">
<TableHead>
<TableRow> <TableRow>
<TableCell>Command</TableCell> <TableCell>Command</TableCell>
<TableCell></TableCell>
<TableCell>Description</TableCell> <TableCell>Description</TableCell>
<TableCell></TableCell> <TableCell></TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => ( {Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => (
<TableRow <TableRow key={command}>
key={command}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell scope="row"> <TableCell scope="row">
<Box> <Box>
{renderIcon(icon)} {renderIcon(icon)}
{command} {command}
</Box> </Box>
</TableCell> </TableCell>
<TableCell>
<Link href={"https://etke.cc/help/extras/scheduler/#" + command} target="_blank">
<Button size="small" startIcon={<HelpCenter />} title={command + " help"} />
</Link>
</TableCell>
<TableCell>{description}</TableCell> <TableCell>{description}</TableCell>
<TableCell> <TableCell>
{args && <TextField {args && (
size="small" <TextField
variant="standard" size="small"
onChange={(e) => { variant="standard"
setCommandAdditionalArgs(command, e.target.value); onChange={e => {
}} setCommandAdditionalArgs(command, e.target.value);
value={additionalArgs} }}
/>} value={additionalArgs}
/>
)}
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
color="primary" color="primary"
label="Run" label="Run"
startIcon={<PlayArrow />} startIcon={<PlayArrow />}
onClick={() => { runCommand(command); }} onClick={() => {
disabled={commandIsRunning || (args && typeof additionalArgs === 'string' && additionalArgs.length === 0)} runCommand(command);
}}
disabled={
commandIsRunning || (args && typeof additionalArgs === "string" && additionalArgs.length === 0)
}
></Button> ></Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -170,13 +194,15 @@ const ServerCommandsPanel = () => {
</Table> </Table>
</TableContainer> </TableContainer>
{commandResult.length > 0 && <Alert icon={<CheckCircle fontSize="inherit" />} severity="success"> {commandResult.length > 0 && (
{commandResult.map((result, index) => ( <Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
<div key={index}>{result}</div> {commandResult.map((result, index) => (
))} <div key={index}>{result}</div>
</Alert>} ))}
</Alert>
)}
</> </>
) );
}; };
export default ServerCommandsPanel; export default ServerCommandsPanel;

View File

@@ -1,9 +1,26 @@
import { Badge, useTheme, Button, Paper, Popper, ClickAwayListener, Box, List, ListItem, ListItemText, Typography, ListSubheader, IconButton, Divider, Tooltip } from "@mui/material";
import NotificationsIcon from '@mui/icons-material/Notifications';
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import NotificationsIcon from "@mui/icons-material/Notifications";
import {
Badge,
useTheme,
Button,
Paper,
Popper,
ClickAwayListener,
Box,
List,
ListItem,
ListItemText,
Typography,
ListSubheader,
IconButton,
Divider,
Tooltip,
} from "@mui/material";
import { Fragment, useEffect, useState } from "react";
import { useDataProvider, useStore } from "react-admin"; import { useDataProvider, useStore } from "react-admin";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { Fragment, useEffect, useState } from "react";
import { useAppContext } from "../../Context"; import { useAppContext } from "../../Context";
import { ServerNotificationsResponse, ServerProcessResponse } from "../../synapse/dataProvider"; import { ServerNotificationsResponse, ServerProcessResponse } from "../../synapse/dataProvider";
import { getTimeSince } from "../../utils/date"; import { getTimeSince } from "../../utils/date";
@@ -12,8 +29,14 @@ import { getTimeSince } from "../../utils/date";
const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000; const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;
const useServerNotifications = () => { const useServerNotifications = () => {
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { notifications: [], success: false }); const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", {
const [ serverProcess, setServerProcess ] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); notifications: [],
success: false,
});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
@@ -21,13 +44,16 @@ const useServerNotifications = () => {
const { notifications, success } = serverNotifications; const { notifications, success } = serverNotifications;
const fetchNotifications = async () => { const fetchNotifications = async () => {
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin, command !== ""); const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(
etkeccAdmin,
command !== ""
);
const serverNotifications = [...notificationsResponse.notifications]; const serverNotifications = [...notificationsResponse.notifications];
serverNotifications.reverse(); serverNotifications.reverse();
setServerNotifications({ setServerNotifications({
...notificationsResponse, ...notificationsResponse,
notifications: serverNotifications, notifications: serverNotifications,
success: notificationsResponse.success success: notificationsResponse.success,
}); });
}; };
@@ -60,7 +86,7 @@ const useServerNotifications = () => {
if (serverNotificationsInterval) { if (serverNotificationsInterval) {
clearInterval(serverNotificationsInterval); clearInterval(serverNotificationsInterval);
} }
} };
}, [etkeccAdmin, command, locked_at]); }, [etkeccAdmin, command, locked_at]);
return { success, notifications, deleteServerNotifications }; return { success, notifications, deleteServerNotifications };
@@ -89,7 +115,7 @@ export const ServerNotificationsBadge = () => {
}; };
const handleClearAllNotifications = async () => { const handleClearAllNotifications = async () => {
deleteServerNotifications() deleteServerNotifications();
handleClose(); handleClose();
}; };
@@ -100,20 +126,21 @@ export const ServerNotificationsBadge = () => {
return ( return (
<Box> <Box>
<IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}> <IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}>
<Tooltip title={notifications && notifications.length > 0 ? `${notifications.length} new notifications` : `No notifications yet`}> <Tooltip
{notifications && notifications.length > 0 && ( title={
<Badge badgeContent={notifications.length} color="error"> notifications && notifications.length > 0
<NotificationsIcon /> ? `${notifications.length} new notifications`
</Badge> : `No notifications yet`
) || <NotificationsIcon />} }
>
{(notifications && notifications.length > 0 && (
<Badge badgeContent={notifications.length} color="error">
<NotificationsIcon />
</Badge>
)) || <NotificationsIcon />}
</Tooltip> </Tooltip>
</IconButton> </IconButton>
<Popper <Popper open={open} anchorEl={anchorEl} placement="bottom-end" style={{ zIndex: 1300 }}>
open={open}
anchorEl={anchorEl}
placement="bottom-end"
style={{ zIndex: 1300 }}
>
<ClickAwayListener onClickAway={handleClose}> <ClickAwayListener onClickAway={handleClose}>
<Paper <Paper
elevation={3} elevation={3}
@@ -125,12 +152,14 @@ export const ServerNotificationsBadge = () => {
minWidth: "300px", minWidth: "300px",
maxWidth: { maxWidth: {
xs: "100vw", // Full width on mobile xs: "100vw", // Full width on mobile
sm: "400px" // Fixed width on desktop sm: "400px", // Fixed width on desktop
} },
}} }}
> >
{(!notifications || notifications.length === 0) ? ( {!notifications || notifications.length === 0 ? (
<Typography sx={{ p: 1 }} variant="body2">No new notifications</Typography> <Typography sx={{ p: 1 }} variant="body2">
No new notifications
</Typography>
) : ( ) : (
<List sx={{ p: 0 }} dense={true}> <List sx={{ p: 0 }} dense={true}>
<ListSubheader <ListSubheader
@@ -141,46 +170,55 @@ export const ServerNotificationsBadge = () => {
fontWeight: "bold", fontWeight: "bold",
}} }}
> >
<Typography variant="h6">Notifications</Typography> <Typography variant="h6">Notifications</Typography>
<Box sx={{ cursor: "pointer", color: theme.palette.primary.main }} onClick={() => handleSeeAllNotifications()}>See all notifications</Box> <Box
</ListSubheader> sx={{ cursor: "pointer", color: theme.palette.primary.main }}
onClick={() => handleSeeAllNotifications()}
>
See all notifications
</Box>
</ListSubheader>
<Divider /> <Divider />
{notifications.map((notification, index) => { {notifications.map((notification, index) => {
return (<Fragment key={notification.event_id ? notification.event_id + index : index }> return (
<ListItem <Fragment key={notification.event_id ? notification.event_id + index : index}>
onClick={() => handleSeeAllNotifications()} <ListItem
sx={{ onClick={() => handleSeeAllNotifications()}
display: "flex", sx={{
flexDirection: "column", display: "flex",
alignItems: "flex-start", flexDirection: "column",
overflow: "hidden", alignItems: "flex-start",
"&:hover": { overflow: "hidden",
backgroundColor: "action.hover", "&:hover": {
cursor: "pointer" backgroundColor: "action.hover",
} cursor: "pointer",
}} },
> }}
<ListItemText >
primary={ <ListItemText
<Typography primary={
variant="body2" <Typography
sx={{ variant="body2"
overflow: "hidden", sx={{
textOverflow: "ellipsis", overflow: "hidden",
}} textOverflow: "ellipsis",
dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }} }}
/> dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }}
} />
/> }
<ListItemText />
primary={ <ListItemText
<Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>{getTimeSince(notification.sent_at) + " ago"}</Typography> primary={
} <Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>
/> {getTimeSince(notification.sent_at) + " ago"}
</ListItem> </Typography>
<Divider /> }
</Fragment> />
)})} </ListItem>
<Divider />
</Fragment>
);
})}
<ListItem> <ListItem>
<Button <Button
key="clear-all-notifications" key="clear-all-notifications"
@@ -190,7 +228,7 @@ export const ServerNotificationsBadge = () => {
sx={{ sx={{
pl: 0, pl: 0,
pt: 1, pt: 1,
verticalAlign: "middle" verticalAlign: "middle",
}} }}
> >
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> <DeleteIcon fontSize="small" sx={{ mr: 1 }} />

View File

@@ -1,11 +1,12 @@
import { Box, Typography, Paper, Button } from "@mui/material"
import { Stack } from "@mui/material"
import { useStore } from "react-admin"
import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider"
import { useAppContext } from "../../Context";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import { getTimeSince } from "../../utils/date"; import { Box, Typography, Paper, Button } from "@mui/material";
import { Stack } from "@mui/material";
import { Tooltip } from "@mui/material"; import { Tooltip } from "@mui/material";
import { useStore } from "react-admin";
import { useAppContext } from "../../Context";
import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider";
import { getTimeSince } from "../../utils/date";
const DisplayTime = ({ date }: { date: string }) => { const DisplayTime = ({ date }: { date: string }) => {
const dateFromDateString = new Date(date.replace(" ", "T") + "Z"); const dateFromDateString = new Date(date.replace(" ", "T") + "Z");
@@ -15,8 +16,8 @@ const DisplayTime = ({ date }: { date: string }) => {
const ServerNotificationsPage = () => { const ServerNotificationsPage = () => {
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", {
notifications: [], notifications: [],
success: false, success: false,
}); });
const notifications = serverNotifications.notifications; const notifications = serverNotifications.notifications;
@@ -26,13 +27,17 @@ const ServerNotificationsPage = () => {
<Stack spacing={1} direction="row" alignItems="center"> <Stack spacing={1} direction="row" alignItems="center">
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}>
<Typography variant="h4">Server Notifications</Typography> <Typography variant="h4">Server Notifications</Typography>
<Button variant="text" color="error" onClick={async () => { <Button
await dataProvider.deleteServerNotifications(etkeccAdmin); variant="text"
setServerNotifications({ color="error"
notifications: [], onClick={async () => {
success: true, await dataProvider.deleteServerNotifications(etkeccAdmin);
}); setServerNotifications({
}}> notifications: [],
success: true,
});
}}
>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear
</Button> </Button>
</Box> </Box>

View File

@@ -1,48 +1,50 @@
import MonitorHeartIcon from "@mui/icons-material/MonitorHeart";
import { Avatar, Badge, Box, Theme, Tooltip } from "@mui/material"; import { Avatar, Badge, Box, Theme, Tooltip } from "@mui/material";
import { useEffect } from "react";
import { useAppContext } from "../../Context";
import { Button, useDataProvider, useStore } from "react-admin";
import { styled } from '@mui/material/styles';
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
import { BadgeProps } from "@mui/material/Badge"; import { BadgeProps } from "@mui/material/Badge";
import { useNavigate } from "react-router"; import { styled } from "@mui/material/styles";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useEffect } from "react";
import { Button, useDataProvider, useStore } from "react-admin";
import { useNavigate } from "react-router";
import { useAppContext } from "../../Context";
import { ServerProcessResponse, ServerStatusResponse } from "../../synapse/dataProvider"; import { ServerProcessResponse, ServerStatusResponse } from "../../synapse/dataProvider";
interface StyledBadgeProps extends BadgeProps { interface StyledBadgeProps extends BadgeProps {
backgroundColor: string; backgroundColor: string;
badgeColor: string badgeColor: string;
theme?: Theme; theme?: Theme;
} }
const StyledBadge = styled(Badge, { shouldForwardProp: (prop) => !['badgeColor', 'backgroundColor'].includes(prop as string) })<StyledBadgeProps> const StyledBadge = styled(Badge, {
(({ theme, backgroundColor, badgeColor }) => ({ shouldForwardProp: prop => !["badgeColor", "backgroundColor"].includes(prop as string),
'& .MuiBadge-badge': { })<StyledBadgeProps>(({ theme, backgroundColor, badgeColor }) => ({
backgroundColor: backgroundColor, "& .MuiBadge-badge": {
color: badgeColor, backgroundColor: backgroundColor,
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`, color: badgeColor,
'&::after': { boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
position: 'absolute', "&::after": {
top: 0, position: "absolute",
left: 0, top: 0,
width: '100%', left: 0,
height: '100%', width: "100%",
borderRadius: '50%', height: "100%",
animation: 'ripple 2.5s infinite ease-in-out', borderRadius: "50%",
border: '1px solid currentColor', animation: "ripple 2.5s infinite ease-in-out",
content: '""', border: "1px solid currentColor",
}, content: '""',
}, },
'@keyframes ripple': { },
'0%': { "@keyframes ripple": {
transform: 'scale(.8)', "0%": {
opacity: 1, transform: "scale(.8)",
}, opacity: 1,
'100%': {
transform: 'scale(2.4)',
opacity: 0,
},
}, },
"100%": {
transform: "scale(2.4)",
opacity: 0,
},
},
})); }));
// every 5 minutes // every 5 minutes
@@ -51,8 +53,16 @@ const SERVER_STATUS_INTERVAL_TIME = 5 * 60 * 1000;
const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000; const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000;
const useServerStatus = () => { const useServerStatus = () => {
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { ok: false, success: false, host: "", results: [] }); const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); ok: false,
success: false,
host: "",
results: [],
});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
@@ -79,7 +89,7 @@ const useServerStatus = () => {
// start the interval after 10 seconds to avoid too many requests // start the interval after 10 seconds to avoid too many requests
serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME); serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME);
}, 10000); }, 10000);
} else { } else {
setServerStatus({ ok: false, success: false, host: "", results: [] }); setServerStatus({ ok: false, success: false, host: "", results: [] });
} }
@@ -90,26 +100,32 @@ const useServerStatus = () => {
if (serverStatusInterval) { if (serverStatusInterval) {
clearInterval(serverStatusInterval); clearInterval(serverStatusInterval);
} }
} };
}, [etkeccAdmin, command]); }, [etkeccAdmin, command]);
return { isOkay, successCheck }; return { isOkay, successCheck };
}; };
const useCurrentServerProcess = () => { const useCurrentServerProcess = () => {
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const checkServerRunningProcess = async () => { const checkServerRunningProcess = async () => {
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin, command !== ""); const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(
etkeccAdmin,
command !== ""
);
setServerProcess({ setServerProcess({
...serverProcess, ...serverProcess,
command: serverProcess.command, command: serverProcess.command,
locked_at: serverProcess.locked_at locked_at: serverProcess.locked_at,
}); });
} };
useEffect(() => { useEffect(() => {
let serverCheckInterval: NodeJS.Timeout | null = null; let serverCheckInterval: NodeJS.Timeout | null = null;
@@ -131,16 +147,36 @@ const useCurrentServerProcess = () => {
if (serverCheckInterval) { if (serverCheckInterval) {
clearInterval(serverCheckInterval); clearInterval(serverCheckInterval);
} }
} };
}, [etkeccAdmin, command]); }, [etkeccAdmin, command]);
return { command, locked_at }; return { command, locked_at };
}; };
export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar = false }: { command: string, locked_at: string, isOkay: boolean, inSidebar: boolean }) => { export const ServerStatusStyledBadge = ({
command,
locked_at,
isOkay,
isLoaded,
inSidebar = false,
}: {
command: string;
locked_at: string;
isOkay: boolean;
isLoaded: boolean;
inSidebar: boolean;
}) => {
const theme = useTheme(); const theme = useTheme();
let badgeBackgroundColor = isOkay ? theme.palette.success.light : theme.palette.error.main; let badgeBackgroundColor = isLoaded
let badgeColor = isOkay ? theme.palette.success.light : theme.palette.error.main; ? isOkay
? theme.palette.success.light
: theme.palette.error.main
: theme.palette.grey[600];
let badgeColor = isLoaded
? isOkay
? theme.palette.success.light
: theme.palette.error.main
: theme.palette.grey[600];
if (command && locked_at) { if (command && locked_at) {
badgeBackgroundColor = theme.palette.warning.main; badgeBackgroundColor = theme.palette.warning.main;
@@ -151,9 +187,10 @@ export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar
avatarBackgroundColor = theme.palette.grey[600]; avatarBackgroundColor = theme.palette.grey[600];
} }
return <StyledBadge return (
<StyledBadge
overlap="circular" overlap="circular"
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
variant="dot" variant="dot"
backgroundColor={badgeBackgroundColor} backgroundColor={badgeBackgroundColor}
badgeColor={badgeColor} badgeColor={badgeColor}
@@ -161,35 +198,44 @@ export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar
<Avatar sx={{ height: 24, width: 24, background: avatarBackgroundColor }}> <Avatar sx={{ height: 24, width: 24, background: avatarBackgroundColor }}>
<MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} /> <MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} />
</Avatar> </Avatar>
</StyledBadge> </StyledBadge>
);
}; };
const ServerStatusBadge = () => { const ServerStatusBadge = () => {
const { isOkay, successCheck } = useServerStatus(); const { isOkay, successCheck } = useServerStatus();
const { command, locked_at } = useCurrentServerProcess(); const { command, locked_at } = useCurrentServerProcess();
const navigate = useNavigate(); const navigate = useNavigate();
if (!successCheck) { if (!successCheck) {
return null; return null;
} }
const handleServerStatusClick = () => { const handleServerStatusClick = () => {
navigate("/server_status"); navigate("/server_status");
}; };
let tooltipText = "Click to view Server Status"; let tooltipText = "Click to view Server Status";
if (command && locked_at) { if (command && locked_at) {
tooltipText = `Running: ${command}; ${tooltipText}`; tooltipText = `Running: ${command}; ${tooltipText}`;
} }
return <Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 }}}> return (
<Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 } }}>
<Tooltip title={tooltipText} sx={{ cursor: "pointer" }}> <Tooltip title={tooltipText} sx={{ cursor: "pointer" }}>
<Box> <Box>
<ServerStatusStyledBadge inSidebar={false} command={command || ""} locked_at={locked_at || ""} isOkay={isOkay} /> <ServerStatusStyledBadge
</Box> inSidebar={false}
command={command || ""}
locked_at={locked_at || ""}
isOkay={isOkay}
isLoaded={successCheck}
/>
</Box>
</Tooltip> </Tooltip>
</Button> </Button>
);
}; };
export default ServerStatusBadge; export default ServerStatusBadge;

View File

@@ -1,13 +1,22 @@
import { useStore } from "ra-core"; import CheckIcon from "@mui/icons-material/Check";
import { Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import EngineeringIcon from '@mui/icons-material/Engineering'; import EngineeringIcon from "@mui/icons-material/Engineering";
import { Alert, Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
import { useStore } from "ra-core";
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider"; import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
import ServerCommandsPanel from "./ServerCommandsPanel";
import { getTimeSince } from "../../utils/date"; import { getTimeSince } from "../../utils/date";
const StatusChip = ({ isOkay, size = "medium", command }: { isOkay: boolean, size?: "small" | "medium", command?: string }) => { const StatusChip = ({
isOkay,
size = "medium",
command,
}: {
isOkay: boolean;
size?: "small" | "medium";
command?: string;
}) => {
let label = "OK"; let label = "OK";
let icon = <CheckIcon />; let icon = <CheckIcon />;
let color: ChipProps["color"] = "success"; let color: ChipProps["color"] = "success";
@@ -23,9 +32,7 @@ const StatusChip = ({ isOkay, size = "medium", command }: { isOkay: boolean, siz
icon = <EngineeringIcon />; icon = <EngineeringIcon />;
} }
return ( return <Chip icon={icon} label={label} color={color} variant="outlined" size={size} />;
<Chip icon={icon} label={label} color={color} variant="outlined" size={size} />
);
}; };
const ServerComponentText = ({ text }: { text: string }) => { const ServerComponentText = ({ text }: { text: string }) => {
@@ -39,14 +46,17 @@ const ServerStatusPage = () => {
host: "", host: "",
results: [], results: [],
}); });
const [ serverProcess, setServerProcess ] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const successCheck = serverStatus.success; const successCheck = serverStatus.success;
const isOkay = serverStatus.ok; const isOkay = serverStatus.ok;
const host = serverStatus.host; const host = serverStatus.host;
const results = serverStatus.results; const results = serverStatus.results;
let groupedResults: Record<string, ServerStatusComponent[]> = {}; const groupedResults: Record<string, ServerStatusComponent[]> = {};
for (const result of results) { for (const result of results) {
if (!groupedResults[result.category]) { if (!groupedResults[result.category]) {
groupedResults[result.category] = []; groupedResults[result.category] = [];
@@ -58,10 +68,7 @@ const ServerStatusPage = () => {
return ( return (
<Paper elevation={3} sx={{ p: 3, mt: 3 }}> <Paper elevation={3} sx={{ p: 3, mt: 3 }}>
<Stack direction="row" spacing={2} alignItems="center"> <Stack direction="row" spacing={2} alignItems="center">
<CloseIcon color="error" /> <Typography color="info">Fetching real-time server health... Just a moment!</Typography>
<Typography color="error">
Unable to fetch server status. Please try again later.
</Typography>
</Stack> </Stack>
</Paper> </Paper>
); );
@@ -78,23 +85,23 @@ const ServerStatusPage = () => {
{host} {host}
</Typography> </Typography>
</Stack> </Stack>
{command && locked_at && (
<Stack spacing={1} direction="row" alignItems="center">
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Typography variant="h5">Currently running:</Typography>
<Typography variant="h5" color="text.secondary">
<Link href={"https://etke.cc/help/extras/scheduler/#"+command} target="_blank">
{command}
</Link>
<Tooltip title={locked_at.toString()}>
<Typography component="span" color="text.secondary" sx={{ display: "inline-block", ml: 1 }}>(started {getTimeSince(locked_at)} ago)</Typography>
</Tooltip>
</Typography>
</Box>
</Stack>
)}
<ServerCommandsPanel /> <CurrentlyRunningCommand />
<Typography variant="body1">
This is a{" "}
<Link href="https://etke.cc/services/monitoring/" target="_blank">
monitoring report
</Link>{" "}
of the server. If any of the checks below concern you, please check the{" "}
<Link
href="https://etke.cc/services/monitoring/#what-to-do-if-the-monitoring-report-shows-issues"
target="_blank"
>
suggested actions
</Link>
.
</Typography>
<Stack spacing={2} direction="row"> <Stack spacing={2} direction="row">
{Object.keys(groupedResults).map((category, idx) => ( {Object.keys(groupedResults).map((category, idx) => (
@@ -117,8 +124,10 @@ const ServerStatusPage = () => {
<ServerComponentText text={result.label.text} /> <ServerComponentText text={result.label.text} />
)} )}
</Box> </Box>
{result.reason && <Typography color="text.secondary" dangerouslySetInnerHTML={{ __html: result.reason }}/>} {result.reason && (
{(!result.ok && result.help) && ( <Typography color="text.secondary" dangerouslySetInnerHTML={{ __html: result.reason }} />
)}
{!result.ok && result.help && (
<Link href={result.help} target="_blank" rel="noopener noreferrer" sx={{ mt: 1 }}> <Link href={result.help} target="_blank" rel="noopener noreferrer" sx={{ mt: 1 }}>
Learn more Learn more
</Link> </Link>

View File

@@ -0,0 +1,29 @@
import { useState, useEffect } from "react";
import { useDataProvider } from "react-admin";
import { useAppContext } from "../../../Context";
import { ServerCommand } from "../../../synapse/dataProvider";
export const useServerCommands = () => {
const { etkeccAdmin } = useAppContext();
const [isLoading, setLoading] = useState(true);
const [serverCommands, setServerCommands] = useState<Record<string, ServerCommand>>({});
const dataProvider = useDataProvider();
useEffect(() => {
const fetchServerCommands = async () => {
const serverCommandsResponse = await dataProvider.getServerCommands(etkeccAdmin);
if (serverCommandsResponse) {
const serverCommands = serverCommandsResponse;
Object.keys(serverCommandsResponse).forEach((command: string) => {
serverCommands[command].additionalArgs = "";
});
setServerCommands(serverCommands);
}
setLoading(false);
};
fetchServerCommands();
}, [dataProvider, etkeccAdmin]);
return { isLoading, serverCommands, setServerCommands };
};

View File

@@ -0,0 +1,21 @@
const transformCommandsToChoices = (commands: Record<string, any>) => {
return Object.entries(commands).map(([key, value]) => ({
id: key,
name: value.name,
description: value.description,
}));
};
const ScheduledCommandCreate = () => {
const commandChoices = transformCommandsToChoices(serverCommands);
return (
<SimpleForm>
<SelectInput
source="command"
choices={commandChoices}
optionText={choice => `${choice.name} - ${choice.description}`}
/>
</SimpleForm>
);
};

View File

@@ -0,0 +1,182 @@
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Card, CardContent, CardHeader, Box, Alert, Typography, Link } from "@mui/material";
import { useQueryClient } from "@tanstack/react-query";
import { useState, useEffect } from "react";
import {
Form,
TextInput,
SaveButton,
useNotify,
useDataProvider,
Loading,
Button,
SelectInput,
TimeInput,
} from "react-admin";
import { useWatch } from "react-hook-form";
import { useParams, useNavigate } from "react-router-dom";
import RecurringDeleteButton from "./RecurringDeleteButton";
import { useAppContext } from "../../../../../Context";
import { RecurringCommand } from "../../../../../synapse/dataProvider";
import { useServerCommands } from "../../../hooks/useServerCommands";
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
const transformCommandsToChoices = (commands: Record<string, any>) => {
return Object.entries(commands).map(([key, value]) => ({
id: key,
name: value.name,
description: value.description,
}));
};
const ArgumentsField = ({ serverCommands }) => {
const selectedCommand = useWatch({ name: "command" });
const showArgs = selectedCommand && serverCommands[selectedCommand]?.args === true;
if (!showArgs) return null;
return <TextInput required source="args" label="Arguments" fullWidth multiline />;
};
const RecurringCommandEdit = () => {
const { id } = useParams();
const navigate = useNavigate();
const notify = useNotify();
const dataProvider = useDataProvider();
const queryClient = useQueryClient();
const { etkeccAdmin } = useAppContext();
const [command, setCommand] = useState<RecurringCommand | undefined>(undefined);
const isCreating = typeof id === "undefined";
const [loading, setLoading] = useState(!isCreating);
const { data: recurringCommands, isLoading: isLoadingList } = useRecurringCommands();
const { serverCommands, isLoading: isLoadingServerCommands } = useServerCommands();
const pageTitle = isCreating ? "Create Recurring Command" : "Edit Recurring Command";
const commandChoices = transformCommandsToChoices(serverCommands);
const dayOfWeekChoices = [
{ id: "Monday", name: "Monday" },
{ id: "Tuesday", name: "Tuesday" },
{ id: "Wednesday", name: "Wednesday" },
{ id: "Thursday", name: "Thursday" },
{ id: "Friday", name: "Friday" },
{ id: "Saturday", name: "Saturday" },
{ id: "Sunday", name: "Sunday" },
];
useEffect(() => {
if (!isCreating && recurringCommands) {
const commandToEdit = recurringCommands.find(cmd => cmd.id === id);
if (commandToEdit) {
const timeValue = commandToEdit.time || "";
const timeParts = timeValue.split(" ");
const parsedCommand = {
...commandToEdit,
day_of_week: timeParts.length > 1 ? timeParts[0] : "Monday",
execution_time: timeParts.length > 1 ? timeParts[1] : timeValue,
};
setCommand(parsedCommand);
}
setLoading(false);
}
}, [id, recurringCommands, isCreating]);
const handleSubmit = async data => {
try {
// Format the time from the Date object to a string in HH:MM format
let formattedTime = "00:00";
if (data.execution_time instanceof Date) {
const hours = String(data.execution_time.getHours()).padStart(2, "0");
const minutes = String(data.execution_time.getMinutes()).padStart(2, "0");
formattedTime = `${hours}:${minutes}`;
} else if (typeof data.execution_time === "string") {
formattedTime = data.execution_time;
}
const submissionData = {
...data,
time: `${data.day_of_week} ${formattedTime}`,
};
delete submissionData.day_of_week;
delete submissionData.execution_time;
delete submissionData.scheduled_at;
// Only include args when it's required for the selected command
const selectedCommand = data.command;
if (!selectedCommand || !serverCommands[selectedCommand]?.args) {
delete submissionData.args;
}
let result;
if (isCreating) {
result = await dataProvider.createRecurringCommand(etkeccAdmin, submissionData);
notify("recurring_commands.action.create_success", { type: "success" });
} else {
result = await dataProvider.updateRecurringCommand(etkeccAdmin, {
...submissionData,
id: id,
});
notify("recurring_commands.action.update_success", { type: "success" });
}
// Invalidate scheduled commands queries
queryClient.invalidateQueries({ queryKey: ["scheduledCommands"] });
navigate("/server_actions");
} catch (error) {
notify("recurring_commands.action.update_failure", { type: "error" });
}
};
if (loading || isLoadingList || isLoadingServerCommands) {
return <Loading />;
}
return (
<Box sx={{ mt: 2 }}>
<Button label="Back" onClick={() => navigate("/server_actions")} startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} />
<Card>
<CardHeader title={pageTitle} />
<CardContent>
{command && (
<Alert severity="info">
<Typography variant="body1" sx={{ px: 2 }}>
You can find more details about the command{" "}
<Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank">
here
</Link>
.
</Typography>
</Alert>
)}
<Form
defaultValues={command || undefined}
onSubmit={handleSubmit}
record={command || undefined}
warnWhenUnsavedChanges
>
<Box display="flex" flexDirection="column" gap={2}>
{!isCreating && <TextInput readOnly source="id" label="ID" fullWidth required />}
<SelectInput source="command" choices={commandChoices} label="Command" fullWidth required />
<ArgumentsField serverCommands={serverCommands} />
<SelectInput source="day_of_week" choices={dayOfWeekChoices} label="Day of Week" fullWidth required />
<TimeInput source="execution_time" label="Time (UTC)" fullWidth required />
<Box mt={2} display="flex" justifyContent="space-between">
<SaveButton label={isCreating ? "Create" : "Update"} />
{!isCreating && <RecurringDeleteButton />}
</Box>
</Box>
</Form>
</CardContent>
</Card>
</Box>
);
};
export default RecurringCommandEdit;

View File

@@ -0,0 +1,62 @@
import AddIcon from "@mui/icons-material/Add";
import { Paper } from "@mui/material";
import { Loading, Button } from "react-admin";
import { DateField } from "react-admin";
import { Datagrid } from "react-admin";
import { ListContextProvider, TextField, TopToolbar, Identifier } from "react-admin";
import { ResourceContextProvider, useList } from "react-admin";
import { useNavigate } from "react-router-dom";
import { DATE_FORMAT } from "../../../../../utils/date";
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
const ListActions = () => {
const navigate = useNavigate();
return (
<TopToolbar>
<Button label="Create" onClick={() => navigate("/server_actions/recurring/create")} startIcon={<AddIcon />} />
</TopToolbar>
);
};
const RecurringCommandsList = () => {
const { data, isLoading, error } = useRecurringCommands();
const listContext = useList({
resource: "recurring",
sort: { field: "scheduled_at", order: "DESC" },
perPage: 50,
data: data || [],
isLoading: isLoading,
});
if (isLoading) return <Loading />;
return (
<ResourceContextProvider value="recurring">
<ListContextProvider value={listContext}>
<ListActions />
<Paper>
<Datagrid
bulkActionButtons={false}
rowClick={(id: Identifier, resource: string, record: any) => {
if (!record) {
return "";
}
return `/server_actions/${resource}/${id}`;
}}
>
<TextField source="command" />
<TextField source="args" label="Arguments" />
<TextField source="time" label="Time (UTC)" />
<DateField options={DATE_FORMAT} showTime source="scheduled_at" label="Next run at (local time)" />
</Datagrid>
</Paper>
</ListContextProvider>
</ResourceContextProvider>
);
};
export default RecurringCommandsList;

View File

@@ -0,0 +1,65 @@
import DeleteIcon from "@mui/icons-material/Delete";
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import { useNotify, useDataProvider, useRecordContext } from "react-admin";
import { Button, Confirm } from "react-admin";
import { useNavigate } from "react-router-dom";
import { useAppContext } from "../../../../../Context";
import { RecurringCommand } from "../../../../../synapse/dataProvider";
const RecurringDeleteButton = () => {
const record = useRecordContext() as RecurringCommand;
const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider();
const notify = useNotify();
const theme = useTheme();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const handleClick = e => {
e.stopPropagation();
setOpen(true);
};
const handleConfirm = async () => {
setIsDeleting(true);
try {
await dataProvider.deleteRecurringCommand(etkeccAdmin, record.id);
notify("recurring_commands.action.delete_success", { type: "success" });
navigate("/server_actions");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
notify(`Error: ${errorMessage}`, { type: "error" });
} finally {
setIsDeleting(false);
setOpen(false);
}
};
const handleCancel = () => {
setOpen(false);
};
return (
<>
<Button
sx={{ color: theme.palette.error.main }}
label="Delete"
onClick={handleClick}
disabled={isDeleting}
startIcon={<DeleteIcon />}
/>
<Confirm
isOpen={open}
title="Delete Recurring Command"
content={`Are you sure you want to delete the command: ${record?.command || ""}?`}
onConfirm={handleConfirm}
onClose={handleCancel}
/>
</>
);
};
export default RecurringDeleteButton;

View File

@@ -0,0 +1,141 @@
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Card, CardContent, CardHeader, Box } from "@mui/material";
import { Typography, Link } from "@mui/material";
import { useState, useEffect } from "react";
import {
Form,
TextInput,
DateTimeInput,
SaveButton,
useNotify,
useDataProvider,
Loading,
Button,
BooleanInput,
SelectInput,
} from "react-admin";
import { useWatch } from "react-hook-form";
import { useParams, useNavigate } from "react-router-dom";
import ScheduleDeleteButton from "./ScheduledDeleteButton";
import { useAppContext } from "../../../../../Context";
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
import { useServerCommands } from "../../../hooks/useServerCommands";
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
const transformCommandsToChoices = (commands: Record<string, any>) => {
return Object.entries(commands).map(([key, value]) => ({
id: key,
name: value.name,
description: value.description,
}));
};
const ArgumentsField = ({ serverCommands }) => {
const selectedCommand = useWatch({ name: "command" });
const showArgs = selectedCommand && serverCommands[selectedCommand]?.args === true;
if (!showArgs) return null;
return <TextInput required source="args" label="Arguments" fullWidth multiline />;
};
const ScheduledCommandEdit = () => {
const { id } = useParams();
const navigate = useNavigate();
const notify = useNotify();
const dataProvider = useDataProvider();
const { etkeccAdmin } = useAppContext();
const [command, setCommand] = useState<ScheduledCommand | null>(null);
const isCreating = typeof id === "undefined";
const [loading, setLoading] = useState(!isCreating);
const { data: scheduledCommands, isLoading: isLoadingList } = useScheduledCommands();
const { serverCommands, isLoading: isLoadingServerCommands } = useServerCommands();
const pageTitle = isCreating ? "Create Scheduled Command" : "Edit Scheduled Command";
const commandChoices = transformCommandsToChoices(serverCommands);
useEffect(() => {
if (!isCreating && scheduledCommands) {
const commandToEdit = scheduledCommands.find(cmd => cmd.id === id);
if (commandToEdit) {
setCommand(commandToEdit);
}
setLoading(false);
}
}, [id, scheduledCommands, isCreating]);
const handleSubmit = async data => {
try {
let result;
data.scheduled_at = new Date(data.scheduled_at).toISOString();
if (isCreating) {
result = await dataProvider.createScheduledCommand(etkeccAdmin, data);
notify("scheduled_commands.action.create_success", { type: "success" });
} else {
result = await dataProvider.updateScheduledCommand(etkeccAdmin, {
...data,
id: id,
});
notify("scheduled_commands.action.update_success", { type: "success" });
}
navigate("/server_actions");
} catch (error) {
notify("scheduled_commands.action.update_failure", { type: "error" });
}
};
if (loading || isLoadingList) {
return <Loading />;
}
return (
<Box sx={{ mt: 2 }}>
<Button label="Back" onClick={() => navigate("/server_actions")} startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} />
<Card>
<CardHeader title={pageTitle} />
{command && (
<Typography variant="body1" sx={{ px: 2 }}>
You can find more details about the command{" "}
<Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank">
here
</Link>
.
</Typography>
)}
<CardContent>
<Form
defaultValues={command || undefined}
onSubmit={handleSubmit}
record={command || undefined}
warnWhenUnsavedChanges
>
<Box display="flex" flexDirection="column" gap={2}>
{command && <TextInput readOnly source="id" label="ID" fullWidth required />}
<SelectInput
readOnly={!isCreating}
source="command"
choices={commandChoices}
label="Command"
fullWidth
required
/>
<ArgumentsField serverCommands={serverCommands} />
<DateTimeInput source="scheduled_at" label="Scheduled at" fullWidth required />
<Box mt={2} display="flex" justifyContent="space-between">
<SaveButton label={isCreating ? "Create" : "Update"} />
{!isCreating && <ScheduleDeleteButton />}
</Box>
</Box>
</Form>
</CardContent>
</Card>
</Box>
);
};
export default ScheduledCommandEdit;

View File

@@ -0,0 +1,89 @@
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Alert, Box, Card, CardContent, CardHeader, Typography, Link } from "@mui/material";
import { useState, useEffect } from "react";
import {
Loading,
Button,
useDataProvider,
useNotify,
SimpleShowLayout,
TextField,
BooleanField,
DateField,
RecordContextProvider,
} from "react-admin";
import { useParams, useNavigate } from "react-router-dom";
import ScheduledDeleteButton from "./ScheduledDeleteButton";
import { useAppContext } from "../../../../../Context";
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
const ScheduledCommandShow = () => {
const { id } = useParams();
const navigate = useNavigate();
const [command, setCommand] = useState<ScheduledCommand | null>(null);
const [loading, setLoading] = useState(true);
const { data: scheduledCommands, isLoading: isLoadingList } = useScheduledCommands();
useEffect(() => {
if (scheduledCommands) {
const commandToShow = scheduledCommands.find(cmd => cmd.id === id);
if (commandToShow) {
setCommand(commandToShow);
}
setLoading(false);
}
}, [id, scheduledCommands]);
if (loading || isLoadingList) {
return <Loading />;
}
if (!command) {
return null;
}
return (
<Box sx={{ mt: 2 }}>
<Button label="Back" onClick={() => navigate("/server_actions")} startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} />
<RecordContextProvider value={command}>
<Card>
<CardHeader title="Scheduled Command Details" />
<CardContent>
{command && (
<Alert severity="info">
<Typography variant="body1" sx={{ px: 2 }}>
You can find more details about the command{" "}
<Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank">
here
</Link>
.
</Typography>
</Alert>
)}
<SimpleShowLayout>
<TextField source="id" label="ID" />
<TextField source="command" label="Command" />
{command.args && <TextField source="args" label="Arguments" />}
<BooleanField source="is_recurring" label="Is recurring" />
<DateField source="scheduled_at" label="Scheduled at" showTime />
</SimpleShowLayout>
{command.is_recurring && (
<Alert severity="warning">
Scheduled commands created from a recurring one are not editable as they will be regenerated
automatically. Please edit the recurring command instead.
</Alert>
)}
</CardContent>
</Card>
<Box display="flex" justifyContent="flex-end" mt={2}>
<ScheduledDeleteButton />
</Box>
</RecordContextProvider>
</Box>
);
};
export default ScheduledCommandShow;

View File

@@ -0,0 +1,72 @@
import AddIcon from "@mui/icons-material/Add";
import { Paper } from "@mui/material";
import { Loading, Button, useNotify, useRefresh, useCreatePath, useRecordContext } from "react-admin";
import { ResourceContextProvider, useList } from "react-admin";
import { ListContextProvider, TextField } from "react-admin";
import { Datagrid } from "react-admin";
import { BooleanField, DateField, TopToolbar } from "react-admin";
import { useDataProvider } from "react-admin";
import { Identifier } from "react-admin";
import { useNavigate } from "react-router-dom";
import { useAppContext } from "../../../../../Context";
import { DATE_FORMAT } from "../../../../../utils/date";
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
const ListActions = () => {
const navigate = useNavigate();
const handleCreate = () => {
navigate("/server_actions/scheduled/create");
};
return (
<TopToolbar>
<Button label="Create" onClick={handleCreate} startIcon={<AddIcon />} />
</TopToolbar>
);
};
const ScheduledCommandsList = () => {
const { data, isLoading, error } = useScheduledCommands();
const listContext = useList({
resource: "scheduled",
sort: { field: "scheduled_at", order: "DESC" },
perPage: 50,
data: data || [],
isLoading: isLoading,
});
if (isLoading) return <Loading />;
return (
<ResourceContextProvider value="scheduled">
<ListContextProvider value={listContext}>
<ListActions />
<Paper>
<Datagrid
bulkActionButtons={false}
rowClick={(id: Identifier, resource: string, record: any) => {
if (!record) {
return "";
}
if (record.is_recurring) {
return `/server_actions/${resource}/${id}/show`;
}
return `/server_actions/${resource}/${id}`;
}}
>
<TextField source="command" />
<TextField source="args" label="Arguments" />
<BooleanField source="is_recurring" label="Is recurring?" />
<DateField options={DATE_FORMAT} showTime source="scheduled_at" label="Run at (local time)" />
</Datagrid>
</Paper>
</ListContextProvider>
</ResourceContextProvider>
);
};
export default ScheduledCommandsList;

View File

@@ -0,0 +1,65 @@
import DeleteIcon from "@mui/icons-material/Delete";
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import { useNotify, useDataProvider, useRecordContext } from "react-admin";
import { Button, Confirm } from "react-admin";
import { useNavigate } from "react-router-dom";
import { useAppContext } from "../../../../../Context";
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
const ScheduledDeleteButton = () => {
const record = useRecordContext() as ScheduledCommand;
const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider();
const notify = useNotify();
const theme = useTheme();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const handleClick = e => {
e.stopPropagation();
setOpen(true);
};
const handleConfirm = async () => {
setIsDeleting(true);
try {
await dataProvider.deleteScheduledCommand(etkeccAdmin, record.id);
notify("scheduled_commands.action.delete_success", { type: "success" });
navigate("/server_actions");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
notify(`Error: ${errorMessage}`, { type: "error" });
} finally {
setIsDeleting(false);
setOpen(false);
}
};
const handleCancel = () => {
setOpen(false);
};
return (
<>
<Button
sx={{ color: theme.palette.error.main }}
label="Delete"
onClick={handleClick}
disabled={isDeleting}
startIcon={<DeleteIcon />}
/>
<Confirm
isOpen={open}
title="Delete Scheduled Command"
content={`Are you sure you want to delete the command: ${record?.command || ""}?`}
onConfirm={handleConfirm}
onClose={handleCancel}
/>
</>
);
};
export default ScheduledDeleteButton;

View File

@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { useDataProvider } from "react-admin";
import { useAppContext } from "../../../../Context";
export const useRecurringCommands = () => {
const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider();
const { data, isLoading, error } = useQuery({
queryKey: ["recurringCommands"],
queryFn: () => dataProvider.getRecurringCommands(etkeccAdmin),
});
return { data, isLoading, error };
};

View File

@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { useDataProvider } from "react-admin";
import { useAppContext } from "../../../../Context";
export const useScheduledCommands = () => {
const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider();
const { data, isLoading, error } = useQuery({
queryKey: ["scheduledCommands"],
queryFn: () => dataProvider.getScheduledCommands(etkeccAdmin),
});
return { data, isLoading, error };
};

View File

@@ -1,17 +1,26 @@
import { get } from "lodash";
import { useState } from "react";
import BlockIcon from "@mui/icons-material/Block"; import BlockIcon from "@mui/icons-material/Block";
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@mui/icons-material/Cancel";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep"; import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
import DownloadIcon from "@mui/icons-material/Download";
import DownloadingIcon from "@mui/icons-material/Downloading";
import FileOpenIcon from "@mui/icons-material/FileOpen"; import FileOpenIcon from "@mui/icons-material/FileOpen";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen"; import LockOpenIcon from "@mui/icons-material/LockOpen";
import DownloadIcon from '@mui/icons-material/Download'; import {
import DownloadingIcon from '@mui/icons-material/Downloading'; Grid2 as Grid,
import { Grid2 as Grid, Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip, Link } from "@mui/material"; Box,
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
Tooltip,
Link,
} from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles"; import { alpha, useTheme } from "@mui/material/styles";
import { useMutation } from "@tanstack/react-query";
import { get } from "lodash";
import { useState } from "react";
import { import {
BooleanInput, BooleanInput,
Button, Button,
@@ -30,12 +39,11 @@ import {
useRefresh, useRefresh,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import { useMutation } from "@tanstack/react-query";
import { dateParser } from "../utils/date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider"; import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia"; import { dateParser } from "../utils/date";
import decodeURLComponent from "../utils/decodeURLComponent"; import decodeURLComponent from "../utils/decodeURLComponent";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => { const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate(); const translate = useTranslate();
@@ -55,24 +63,9 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
<DialogContent> <DialogContent>
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText> <DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}> <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
<DateTimeInput <DateTimeInput source="before_ts" label="delete_media.fields.before_ts" defaultValue={0} parse={dateParser} />
source="before_ts" <NumberInput source="size_gt" label="delete_media.fields.size_gt" defaultValue={0} min={0} step={1024} />
label="delete_media.fields.before_ts" <BooleanInput source="keep_profiles" label="delete_media.fields.keep_profiles" defaultValue={true} />
defaultValue={0}
parse={dateParser}
/>
<NumberInput
source="size_gt"
label="delete_media.fields.size_gt"
defaultValue={0}
min={0}
step={1024}
/>
<BooleanInput
source="keep_profiles"
label="delete_media.fields.keep_profiles"
defaultValue={true}
/>
</SimpleForm> </SimpleForm>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -410,7 +403,7 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
document.body.appendChild(anchorElement); document.body.appendChild(anchorElement);
anchorElement.click(); anchorElement.click();
document.body.removeChild(anchorElement); document.body.removeChild(anchorElement);
setTimeout(() => URL.revokeObjectURL(blobURL), 10);; setTimeout(() => URL.revokeObjectURL(blobURL), 10);
}; };
const handleFile = async (preview: boolean) => { const handleFile = async (preview: boolean) => {
@@ -460,7 +453,7 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
onClick={() => handleFile(false)} onClick={() => handleFile(false)}
style={{ minWidth: 0, padding: 0, marginRight: 8 }} style={{ minWidth: 0, padding: 0, marginRight: 8 }}
> >
{loading ? <DownloadingIcon /> : <DownloadIcon />} {loading ? <DownloadingIcon /> : <DownloadIcon />}
</Button> </Button>
<span>{label}</span> <span>{label}</span>
</Box> </Box>
@@ -491,7 +484,7 @@ export const MediaIDField = ({ source }) => {
mxcURL = `mxc://${homeserver}/${mediaID}`; mxcURL = `mxc://${homeserver}/${mediaID}`;
} }
return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type}/>; return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type} />;
}; };
export const ReportMediaContent = ({ source }) => { export const ReportMediaContent = ({ source }) => {
@@ -510,5 +503,5 @@ export const ReportMediaContent = ({ source }) => {
uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString()); uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString());
} }
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type}/>; return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type} />;
}; };

View File

@@ -0,0 +1,50 @@
import { NativeSelect, Paper } from "@mui/material";
import { CardContent, CardHeader, Container } from "@mui/material";
import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react";
import { ParsedStats, Progress } from "./types";
const TranslatableOption = ({ value, text }: { value: string; text: string }) => {
const translate = useTranslate();
return <option value={value}>{translate(text)}</option>;
};
const ConflictModeCard = ({
stats,
importResults,
onConflictModeChanged,
conflictMode,
progress,
}: {
stats: ParsedStats | null;
importResults: any;
onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>;
conflictMode: string;
progress: Progress;
}) => {
const translate = useTranslate();
if (!stats || importResults) {
return null;
}
return (
<Container sx={{ mb: 3 }}>
<Paper elevation={1}>
<CardHeader
title={translate("import_users.cards.conflicts.header")}
sx={{ borderBottom: 1, borderColor: "divider" }}
/>
<CardContent>
<NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
<TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
<TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
</NativeSelect>
</CardContent>
</Paper>
</Container>
);
};
export default ConflictModeCard;

View File

@@ -0,0 +1,36 @@
import { Container, Paper, CardHeader, CardContent, Stack, Typography } from "@mui/material";
import { useTranslate } from "ra-core";
const ErrorsCard = ({ errors }: { errors: string[] }) => {
const translate = useTranslate();
if (errors.length === 0) {
return null;
}
return (
<Container sx={{ mb: 3 }}>
<Paper elevation={1}>
<CardHeader
title={translate("import_users.error.error")}
sx={{
borderBottom: 1,
borderColor: "error.main",
color: "error.main",
}}
/>
<CardContent>
<Stack spacing={1}>
{errors.map((e, idx) => (
<Typography key={idx} color="error">
{e}
</Typography>
))}
</Stack>
</CardContent>
</Paper>
</Container>
);
};
export default ErrorsCard;

View File

@@ -0,0 +1,95 @@
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import DownloadIcon from "@mui/icons-material/Download";
import {
Alert,
Box,
CardContent,
CardHeader,
Container,
List,
ListItem,
ListItemText,
Paper,
Stack,
Typography,
} from "@mui/material";
import { Button, Link, useTranslate } from "react-admin";
import { ImportResult } from "./types";
const ResultsCard = ({
importResults,
downloadSkippedRecords,
}: {
importResults: ImportResult | null;
downloadSkippedRecords: () => void;
}) => {
const translate = useTranslate();
if (!importResults) {
return null;
}
return (
<Container>
<Paper>
<CardHeader
title={translate("import_users.cards.results.header")}
pb={0}
sx={{
borderBottom: 1,
}}
/>
<CardContent>
<Stack spacing={2}>
<Typography key="total" color="text.primary">
{translate("import_users.cards.results.total", importResults.totalRecordCount)}
</Typography>
<Typography key="successful" color="success.main">
{translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
</Typography>
<List dense>
{importResults.succeededRecords.map(record => (
<ListItem key={record.id}>
<ListItemText primary={record.displayname} />
</ListItem>
))}
</List>
{importResults.skippedRecords.length > 0 && (
<Box>
<Typography key="skipped" color="warning.main">
{translate("import_users.cards.results.skipped", importResults.skippedRecords.length)}
</Typography>
<Button
variant="outlined"
startIcon={<DownloadIcon />}
onClick={downloadSkippedRecords}
sx={{ mt: 2 }}
label={translate("import_users.cards.results.download_skipped")}
></Button>
</Box>
)}
{importResults.erroredRecords.length > 0 && (
<Typography key="errored" color="error.main">
{translate("import_users.cards.results.skipped", importResults.erroredRecords.length)}
</Typography>
)}
{importResults.wasDryRun && (
<Alert severity="warning" key="simulated">
{translate("import_users.cards.results.simulated_only")}
</Alert>
)}
</Stack>
</CardContent>
</Paper>
<Box sx={{ mt: 2 }}>
<Link to="/users">
<Button variant="outlined" startIcon={<ArrowBackIcon />} label={translate("ra.action.back")} />
</Link>
</Box>
</Container>
);
};
export default ResultsCard;

View File

@@ -0,0 +1,50 @@
import { Button, Checkbox, Paper, Container } from "@mui/material";
import { CardActions, FormControlLabel } from "@mui/material";
import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react";
import { Progress, ImportLine, ImportResult } from "./types";
const StartImportCard = ({
csvData,
importResults,
progress,
dryRun,
onDryRunModeChanged,
runImport,
}: {
csvData: ImportLine[];
importResults: ImportResult | null;
progress: Progress;
dryRun: boolean;
onDryRunModeChanged: ChangeEventHandler<HTMLInputElement>;
runImport: () => void;
}) => {
const translate = useTranslate();
if (!csvData || csvData.length === 0 || importResults) {
return null;
}
return (
<Container>
<Paper>
<CardActions>
<FormControlLabel
control={<Checkbox checked={dryRun} onChange={onDryRunModeChanged} disabled={progress !== null} />}
label={translate("import_users.cards.startImport.simulate_only")}
/>
<Button variant="contained" size="large" onClick={runImport} disabled={progress !== null}>
{translate("import_users.cards.startImport.run_import")}
</Button>
{progress !== null ? (
<div>
{progress.done} of {progress.limit} done
</div>
) : null}
</CardActions>
</Paper>
</Container>
);
};
export default StartImportCard;

View File

@@ -0,0 +1,100 @@
import { Card, Paper, Stack, CardContent, CardHeader, Container, Typography } from "@mui/material";
import { NativeSelect } from "@mui/material";
import { FormControlLabel } from "@mui/material";
import { Checkbox } from "@mui/material";
import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react";
import { ParsedStats, Progress } from "./types";
const StatsCard = ({
stats,
progress,
importResults,
useridMode,
passwordMode,
onUseridModeChanged,
onPasswordModeChange,
}: {
stats: ParsedStats | null;
progress: Progress;
importResults: any;
useridMode: string;
passwordMode: boolean;
onUseridModeChanged: ChangeEventHandler<HTMLSelectElement>;
onPasswordModeChange: ChangeEventHandler<HTMLInputElement>;
}) => {
const translate = useTranslate();
if (!stats) {
return null;
}
if (importResults) {
return null;
}
return (
<>
<Container sx={{ mb: 3 }}>
<Paper>
<Card>
<CardHeader
title={translate("import_users.cards.importstats.header")}
sx={{ borderBottom: 1, borderColor: "divider" }}
/>
<CardContent>
<Stack spacing={1}>
<Typography>{translate("import_users.cards.importstats.users_total", stats.total)}</Typography>
<Typography>{translate("import_users.cards.importstats.guest_count", stats.is_guest)}</Typography>
<Typography>{translate("import_users.cards.importstats.admin_count", stats.admin)}</Typography>
</Stack>
</CardContent>
<CardHeader
title={translate("import_users.cards.ids.header")}
sx={{ borderBottom: 1, borderColor: "divider" }}
/>
<CardContent>
<Stack spacing={2}>
<Typography>
{stats.id === stats.total
? translate("import_users.cards.ids.all_ids_present")
: translate("import_users.cards.ids.count_ids_present", stats.id)}
</Typography>
{stats.id > 0 && (
<NativeSelect onChange={onUseridModeChanged} value={useridMode} disabled={progress !== null}>
<option value={"ignore"}>{translate("import_users.cards.ids.mode.ignore")}</option>
<option value={"update"}>{translate("import_users.cards.ids.mode.update")}</option>
</NativeSelect>
)}
</Stack>
</CardContent>
<CardHeader
title={translate("import_users.cards.passwords.header")}
sx={{ borderBottom: 1, borderColor: "divider" }}
/>
<CardContent>
<Stack spacing={1}>
<Typography>
{stats.password === stats.total
? translate("import_users.cards.passwords.all_passwords_present")
: translate("import_users.cards.passwords.count_passwords_present", stats.password)}
</Typography>
{stats.password > 0 && (
<FormControlLabel
control={
<Checkbox checked={passwordMode} disabled={progress !== null} onChange={onPasswordModeChange} />
}
label={translate("import_users.cards.passwords.use_passwords")}
/>
)}
</Stack>
</CardContent>
</Card>
</Paper>
</Container>
</>
);
};
export default StatsCard;

View File

@@ -0,0 +1,44 @@
import { CardHeader, CardContent, Container, Link, Stack, Typography, Paper } from "@mui/material";
import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react";
import { Progress } from "./types";
const UploadCard = ({
importResults,
onFileChange,
progress,
}: {
importResults: any;
onFileChange: ChangeEventHandler<HTMLInputElement>;
progress: Progress;
}) => {
const translate = useTranslate();
if (importResults) {
return null;
}
return (
<Container sx={{ mb: 3 }}>
<Paper elevation={1}>
<CardHeader
title={translate("import_users.cards.upload.header")}
sx={{ borderBottom: 1, borderColor: "divider" }}
/>
<CardContent>
<Stack spacing={2}>
<Typography>
{translate("import_users.cards.upload.explanation")}
<Link href="./data/example.csv" sx={{ ml: 1 }}>
example.csv
</Link>
</Typography>
<input type="file" onChange={onFileChange} disabled={progress !== null} />
</Stack>
</CardContent>
</Paper>
</Container>
);
};
export default UploadCard;

View File

@@ -0,0 +1,69 @@
import { Stack } from "@mui/material";
import { useTranslate } from "ra-core";
import { Title } from "react-admin";
import ConflictModeCard from "./ConflictModeCard";
import ErrorsCard from "./ErrorsCard";
import ResultsCard from "./ResultsCard";
import StartImportCard from "./StartImportCard";
import StatsCard from "./StatsCard";
import UploadCard from "./UploadCard";
import useImportFile from "./useImportFile";
const UserImport = () => {
const {
csvData,
dryRun,
importResults,
progress,
errors,
stats,
conflictMode,
passwordMode,
useridMode,
onFileChange,
onDryRunModeChanged,
runImport,
onConflictModeChanged,
onPasswordModeChange,
onUseridModeChanged,
downloadSkippedRecords,
} = useImportFile();
const translate = useTranslate();
return (
<Stack spacing={3} mt={3} direction="column">
<Title defaultTitle={translate("import_users.title")} />
<UploadCard importResults={importResults} onFileChange={onFileChange} progress={progress} />
<ErrorsCard errors={errors} />
<ConflictModeCard
stats={stats}
importResults={importResults}
conflictMode={conflictMode}
onConflictModeChanged={onConflictModeChanged}
progress={progress}
/>
<StatsCard
stats={stats}
progress={progress}
importResults={importResults}
passwordMode={passwordMode}
useridMode={useridMode}
onPasswordModeChange={onPasswordModeChange}
onUseridModeChanged={onUseridModeChanged}
/>
<StartImportCard
csvData={csvData}
importResults={importResults}
progress={progress}
dryRun={dryRun}
onDryRunModeChanged={onDryRunModeChanged}
runImport={runImport}
/>
<ResultsCard importResults={importResults} downloadSkippedRecords={downloadSkippedRecords} />
</Stack>
);
};
export default UserImport;

View File

@@ -0,0 +1,47 @@
import { RaRecord } from "react-admin";
export interface ImportLine {
id: string;
displayname: string;
user_type?: string;
name?: string;
deactivated?: boolean;
is_guest?: boolean;
admin?: boolean;
is_admin?: boolean;
password?: string;
avatar_url?: string;
}
export interface ParsedStats {
user_types: Record<string, number>;
is_guest: number;
admin: number;
deactivated: number;
password: number;
avatar_url: number;
id: number;
total: number;
}
export interface ChangeStats {
total: number;
id: number;
is_guest: number;
admin: number;
password: number;
}
export type Progress = {
done: number;
limit: number;
} | null;
export interface ImportResult {
skippedRecords: RaRecord[];
erroredRecords: RaRecord[];
succeededRecords: RaRecord[];
totalRecordCount: number;
changeStats: ChangeStats;
wasDryRun: boolean;
}

View File

@@ -0,0 +1,377 @@
import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
import { ChangeEvent, useState } from "react";
import { useTranslate, useNotify, HttpError } from "react-admin";
import { ImportLine, ParsedStats, Progress, ImportResult, ChangeStats } from "./types";
import dataProvider from "../../synapse/dataProvider";
import { returnMXID } from "../../utils/mxid";
import { generateRandomMXID } from "../../utils/mxid";
import { generateRandomPassword } from "../../utils/password";
const LOGGING = true;
const EXPECTED_FIELDS = ["id", "displayname"].sort();
const useImportFile = () => {
const [csvData, setCsvData] = useState<ImportLine[]>([]);
const [errors, setErrors] = useState<string[]>([]);
const [stats, setStats] = useState<ParsedStats | null>(null);
const [dryRun, setDryRun] = useState(true);
const [progress, setProgress] = useState<Progress>(null);
const [importResults, setImportResults] = useState<ImportResult | null>(null);
const [skippedRecords, setSkippedRecords] = useState<string>("");
const [conflictMode, setConflictMode] = useState<"stop" | "skip">("stop");
const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState<"update" | "ignore">("update");
const translate = useTranslate();
const notify = useNotify();
const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
if (progress !== null) return;
setCsvData([]);
setErrors([]);
setStats(null);
setImportResults(null);
const file = e.target.files ? e.target.files[0] : null;
if (!file) return;
/* Let's refuse some unreasonably big files instead of freezing
* up the browser */
if (file.size > 100000000) {
const message = translate("import_users.errors.unreasonably_big", {
size: (file.size / (1024 * 1024)).toFixed(2),
});
notify(message);
setErrors([message]);
return;
}
try {
parseCsv<ImportLine>(file, {
header: true,
skipEmptyLines: true /* especially for a final EOL in the csv file */,
complete: result => {
if (result.errors) {
setErrors(result.errors.map(e => e.toString()));
}
/* Papaparse is very lenient, we may be able to salvage
* the data in the file. */
verifyCsv(result);
},
});
} catch {
setErrors(["Unknown error"]);
return null;
}
};
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>) => {
/* First, verify the presence of required fields */
meta.fields = meta.fields?.map(f => f.trim().toLowerCase());
const missingFields = EXPECTED_FIELDS.filter(eF => !meta.fields?.find(mF => eF === mF));
if (missingFields.length > 0) {
setErrors([translate("import_users.error.required_field", { field: missingFields[0] })]);
return false;
}
/* Collect some stats to prevent sneaky csv files from adding admin
users or something.
*/
const stats: ParsedStats = {
user_types: { default: 0 },
is_guest: 0,
admin: 0,
deactivated: 0,
password: 0,
avatar_url: 0,
id: 0,
total: data.length,
};
const errorMessages = errors.map(e => e.message);
// sanitize the data first
data = data.map(line => {
const newLine = {} as ImportLine;
for (const [key, value] of Object.entries(line)) {
newLine[key.trim().toLowerCase()] = value;
}
return newLine;
});
// process the data
data.forEach((line, idx) => {
if (line.user_type === undefined || line.user_type === "") {
stats.user_types.default++;
} else {
stats.user_types[line.user_type] += 1;
}
/* XXX correct the csv export that react-admin offers for the users
* resource so it gives sensible field names and doesn't duplicate
* id as "name"?
*/
if (meta.fields?.includes("name")) {
delete line.name;
}
if (meta.fields?.includes("user_type")) {
delete line.user_type;
}
if (meta.fields?.includes("is_admin")) {
delete line.is_admin;
}
["is_guest", "admin", "deactivated"].forEach(f => {
if (line[f] === "true") {
stats[f]++;
line[f] = true; // we need true booleans instead of strings
} else {
if (line[f] !== "false" && line[f] !== "") {
console.log("invalid value", line[f], "for field " + f + " in row " + idx);
errorMessages.push(
translate("import_users.error.invalid_value", {
field: f,
row: idx,
})
);
}
line[f] = false; // default values to false
}
});
if (line.password !== undefined && line.password !== "") {
stats.password++;
}
if (line.avatar_url !== undefined && line.avatar_url !== "") {
stats.avatar_url++;
}
if (line.id !== undefined && line.id !== "") {
stats.id++;
}
});
if (errorMessages.length > 0) {
setErrors(errorMessages);
return false;
}
setStats(stats);
setCsvData(data);
return true;
};
const onConflictModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
if (progress !== null) {
return;
}
const value = e.target.value as "stop" | "skip";
setConflictMode(value);
};
const onPasswordModeChange = (e: ChangeEvent<HTMLInputElement>) => {
if (progress !== null) {
return;
}
setPasswordMode(e.target.checked);
};
const onUseridModeChanged = (e: ChangeEvent<HTMLSelectElement>) => {
if (progress !== null) {
return;
}
const value = e.target.value as "update" | "ignore";
setUseridMode(value);
};
const onDryRunModeChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (progress !== null) {
return;
}
setDryRun(e.target.checked);
};
const runImport = async () => {
if (progress !== null) {
notify("import_users.errors.already_in_progress");
return;
}
const results = await doImport();
setImportResults(results);
// offer CSV download of skipped or errored records
// (so that the user doesn't have to filter out successful
// records manually when fixing stuff in the CSV)
setSkippedRecords(unparseCsv(results.skippedRecords));
if (LOGGING) console.log("Skipped records:");
if (LOGGING) console.log(skippedRecords);
};
const doImport = async (): Promise<ImportResult> => {
const skippedRecords: ImportLine[] = [];
const erroredRecords: ImportLine[] = [];
const succeededRecords: ImportLine[] = [];
const changeStats: ChangeStats = {
total: 0,
id: 0,
is_guest: 0,
admin: 0,
password: 0,
};
let entriesDone = 0;
const entriesCount = csvData.length;
try {
setProgress({ done: entriesDone, limit: entriesCount });
for (const entry of csvData) {
const userRecord = { ...entry };
// No need to do a bunch of cryptographic random number getting if
// we are using neither a generated password nor a generated user id.
if (useridMode === "ignore" || userRecord.id === undefined || userRecord.id === "") {
userRecord.id = generateRandomMXID();
}
if (passwordMode === false || entry.password === undefined || entry.password === "") {
userRecord.password = generateRandomPassword();
}
// we want to ensure that the ID is always full MXID, otherwise randomly-generated MXIDs will be in the full
// form, but the ones from the CSV will be localpart-only.
userRecord.id = returnMXID(userRecord.id);
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
/* For these modes we will consider the ID that's in the record.
* If the mode is "stop", we will not continue adding more records, and
* we will offer information on what was already added and what was
* skipped.
*
* If the mode is "skip", we record the record for later, but don't
* send it to the server.
*
* If the mode is "update", we change fields that are reasonable to
* update.
* - If the "password mode" is "true" (i.e. "use passwords from csv"):
* - if the record has a password
* - send the password along with the record
* - if the record has no password
* - generate a new password
* - If the "password mode" is "false"
* - never generate a new password to update existing users with
*/
/* We just act as if there are no IDs in the CSV, so every user will be
* created anew.
* We do a simple retry loop so that an accidental hit on an existing ID
* doesn't trip us up.
*/
if (LOGGING) console.log("will check for existence of record " + JSON.stringify(userRecord));
let retries = 0;
const submitRecord = async (recordData: ImportLine) => {
try {
const response = await dataProvider.getOne("users", { id: recordData.id });
if (LOGGING) console.log("already existed");
if (conflictMode === "stop") {
throw new Error(
translate("import_users.error.id_exits", {
id: recordData.id,
})
);
}
if (conflictMode === "skip" || useridMode === "update") {
skippedRecords.push(recordData);
return;
}
const newRecordData = Object.assign({}, recordData, {
id: generateRandomMXID(),
});
retries++;
if (retries > 512) {
console.warn("retry loop got stuck? pathological situation?");
skippedRecords.push(recordData);
return;
}
await submitRecord(newRecordData);
} catch (e) {
if (!(e instanceof HttpError) || (e.status && e.status !== 404)) {
throw e;
}
if (LOGGING) console.log("OK to create record " + recordData.id + " (" + recordData.displayname + ").");
if (!dryRun) {
await dataProvider.create("users", { data: recordData });
}
succeededRecords.push(recordData);
}
};
await submitRecord(userRecord);
entriesDone++;
setProgress({ done: entriesDone, limit: csvData.length });
}
setProgress(null);
} catch (e) {
setErrors([
translate("import_users.error.at_entry", {
entry: entriesDone + 1,
message: e instanceof Error ? e.message : String(e),
}),
]);
setProgress(null);
}
return {
skippedRecords,
erroredRecords,
succeededRecords,
totalRecordCount: entriesCount,
changeStats,
wasDryRun: dryRun,
};
};
const downloadSkippedRecords = () => {
const element = document.createElement("a");
console.log(skippedRecords);
const file = new Blob([skippedRecords], {
type: "text/comma-separated-values",
});
element.href = URL.createObjectURL(file);
element.download = "skippedRecords.csv";
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
};
return {
csvData,
dryRun,
onDryRunModeChanged,
runImport,
progress,
importResults,
errors,
stats,
conflictMode,
passwordMode,
useridMode,
onConflictModeChanged,
onPasswordModeChange,
onUseridModeChanged,
onFileChange,
downloadSkippedRecords,
};
};
export default useImportFile;

View File

@@ -29,14 +29,15 @@ const fixedGermanMessages = {
}, },
message: { message: {
...formalGermanMessages.ra.message, ...formalGermanMessages.ra.message,
access_denied: access_denied: "Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
"Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
authentication_error: authentication_error:
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.", "Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
select_all_limit_reached: "Es gibt zu viele Elemente, um sie alle auszuwählen. Es wurden nur die ersten %{max} Elemente ausgewählt.", select_all_limit_reached:
"Es gibt zu viele Elemente, um sie alle auszuwählen. Es wurden nur die ersten %{max} Elemente ausgewählt.",
placeholder_data_warning: "Netzwerkproblem: Datenaktualisierung fehlgeschlagen.",
}, },
}, },
} };
const de: SynapseTranslationMessages = { const de: SynapseTranslationMessages = {
...fixedGermanMessages, ...fixedGermanMessages,
@@ -54,7 +55,8 @@ const de: SynapseTranslationMessages = {
access_token: "Zugriffstoken", access_token: "Zugriffstoken",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.", title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.",
content: "Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?", content:
"Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?",
confirm: "Sitzung beenden", confirm: "Sitzung beenden",
cancel: "Nur vom Admin-Panel abmelden", cancel: "Nur vom Admin-Panel abmelden",
}, },
@@ -90,7 +92,7 @@ const de: SynapseTranslationMessages = {
goToPdf: "Gehe zum PDF", goToPdf: "Gehe zum PDF",
cards: { cards: {
importstats: { importstats: {
header: "Benutzer importieren", header: "Geparste Benutzer für den Import",
users_total: "%{smart_count} Benutzer in der CSV Datei |||| %{smart_count} Benutzer in der CSV Datei", users_total: "%{smart_count} Benutzer in der CSV Datei |||| %{smart_count} Benutzer in der CSV Datei",
guest_count: "%{smart_count} Gast |||| %{smart_count} Gäste", guest_count: "%{smart_count} Gast |||| %{smart_count} Gäste",
admin_count: "%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren", admin_count: "%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
@@ -204,9 +206,11 @@ const de: SynapseTranslationMessages = {
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.", password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.", create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.",
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.", deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
suspend: "Ein gesperrter Benutzer kann sich nicht mehr anmelden und wird in den schreibgeschützten Modus versetzt.", suspend:
"Ein gesperrter Benutzer kann sich nicht mehr anmelden und wird in den schreibgeschützten Modus versetzt.",
erase: "DSGVO konformes Löschen der Benutzerdaten.", erase: "DSGVO konformes Löschen der Benutzerdaten.",
erase_text: "Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.", erase_text:
"Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.", erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.", modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
username_available: "Benutzername verfügbar", username_available: "Benutzername verfügbar",
@@ -226,7 +230,8 @@ const de: SynapseTranslationMessages = {
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)", redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
generate_password: "Passwort generieren", generate_password: "Passwort generieren",
overwrite_title: "Warnung!", overwrite_title: "Warnung!",
overwrite_content: "Dieser Benutzername ist bereits vergeben. Sind Sie sicher, dass Sie den vorhandenen Benutzer überschreiben möchten?", overwrite_content:
"Dieser Benutzername ist bereits vergeben. Sind Sie sicher, dass Sie den vorhandenen Benutzer überschreiben möchten?",
overwrite_cancel: "Abbrechen", overwrite_cancel: "Abbrechen",
overwrite_confirm: "Überschreiben", overwrite_confirm: "Überschreiben",
}, },
@@ -240,7 +245,7 @@ const de: SynapseTranslationMessages = {
title: "Kontodaten", title: "Kontodaten",
global: "Globale", global: "Globale",
rooms: "Räume", rooms: "Räume",
} },
}, },
rooms: { rooms: {
name: "Raum |||| Räume", name: "Raum |||| Räume",
@@ -303,10 +308,11 @@ const de: SynapseTranslationMessages = {
assign_admin: "Raumadministrator zuweisen", assign_admin: "Raumadministrator zuweisen",
title: "Raumadministrator zu %{roomName} zuweisen", title: "Raumadministrator zu %{roomName} zuweisen",
confirm: "Raumadministrator zuweisen", confirm: "Raumadministrator zuweisen",
content: "Geben Sie die vollständige MXID des Benutzers an, der als Administrator gesetzt werden soll.\nWarnung: Damit dies funktioniert, muss der Raum mindestens ein lokales Mitglied als Administrator haben.", content:
"Geben Sie die vollständige MXID des Benutzers an, der als Administrator gesetzt werden soll.\nWarnung: Damit dies funktioniert, muss der Raum mindestens ein lokales Mitglied als Administrator haben.",
success: "Der/die Benutzer wurde/n als Raumadministrator gesetzt.", success: "Der/die Benutzer wurde/n als Raumadministrator gesetzt.",
failure: "Der/die Benutzer konnte/n nicht als Raumadministrator gesetzt werden. %{errMsg}", failure: "Der/die Benutzer konnte/n nicht als Raumadministrator gesetzt werden. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@@ -467,8 +473,8 @@ const de: SynapseTranslationMessages = {
info: "Dies ist eine Liste der Medien, die in den Raum hochgeladen wurden. Es ist nicht möglich, Medien zu löschen, die in externen Medien-Repositories hochgeladen wurden.", info: "Dies ist eine Liste der Medien, die in den Raum hochgeladen wurden. Es ist nicht möglich, Medien zu löschen, die in externen Medien-Repositories hochgeladen wurden.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "Raumverzeichnis", name: "Raumverzeichnis",

View File

@@ -18,7 +18,8 @@ const en: SynapseTranslationMessages = {
access_token: "Access token", access_token: "Access token",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "You are using an existing Matrix access token.", title: "You are using an existing Matrix access token.",
content: "Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?", content:
"Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?",
confirm: "Destroy session", confirm: "Destroy session",
cancel: "Just logout from admin panel", cancel: "Just logout from admin panel",
}, },
@@ -40,7 +41,7 @@ const en: SynapseTranslationMessages = {
detail: "Details", detail: "Details",
permission: "Permissions", permission: "Permissions",
media: "Media", media: "Media",
} },
}, },
reports: { tabs: { basic: "Basic", detail: "Details" } }, reports: { tabs: { basic: "Basic", detail: "Details" } },
}, },
@@ -58,7 +59,7 @@ const en: SynapseTranslationMessages = {
goToPdf: "Go to PDF", goToPdf: "Go to PDF",
cards: { cards: {
importstats: { importstats: {
header: "Import users", header: "Parsed users for import",
users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file", users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
guest_count: "%{smart_count} guest |||| %{smart_count} guests", guest_count: "%{smart_count} guest |||| %{smart_count} guests",
admin_count: "%{smart_count} admin |||| %{smart_count} admins", admin_count: "%{smart_count} admin |||| %{smart_count} admins",
@@ -174,7 +175,8 @@ const en: SynapseTranslationMessages = {
deactivate: "You must provide a password to re-activate an account.", deactivate: "You must provide a password to re-activate an account.",
suspend: "Suspending user means they are put into a read-only mode.", suspend: "Suspending user means they are put into a read-only mode.",
erase: "Mark the user as GDPR-erased", erase: "Mark the user as GDPR-erased",
erase_text: "This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.", erase_text:
"This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
erase_admin_error: "Deleting own user is not allowed.", erase_admin_error: "Deleting own user is not allowed.",
modify_managed_user_error: "Modifying a system-managed user is not allowed.", modify_managed_user_error: "Modifying a system-managed user is not allowed.",
username_available: "Username is available", username_available: "Username is available",
@@ -208,7 +210,7 @@ const en: SynapseTranslationMessages = {
title: "Account Data", title: "Account Data",
global: "Global", global: "Global",
rooms: "Rooms", rooms: "Rooms",
} },
}, },
rooms: { rooms: {
name: "Room |||| Rooms", name: "Room |||| Rooms",
@@ -271,10 +273,11 @@ const en: SynapseTranslationMessages = {
assign_admin: "Assign admin", assign_admin: "Assign admin",
title: "Assign a room admin to %{roomName}", title: "Assign a room admin to %{roomName}",
confirm: "Make admin", confirm: "Make admin",
content: "Put the full MXID of the user which will be set as admin.\nWarning: for this to work, the room needs to have at least one local member as admin.", content:
"Put the full MXID of the user which will be set as admin.\nWarning: for this to work, the room needs to have at least one local member as admin.",
success: "The user has been set as room admin.", success: "The user has been set as room admin.",
failure: "The user could not be set as room admin. %{errMsg}", failure: "The user could not be set as room admin. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@@ -479,5 +482,23 @@ const en: SynapseTranslationMessages = {
helper: { length: "Length of the token if no token is given." }, helper: { length: "Length of the token if no token is given." },
}, },
}, },
scheduled_commands: {
action: {
create_success: "Scheduled command created successfully",
update_success: "Scheduled command updated successfully",
update_failure: "An error has occurred",
delete_success: "Scheduled command deleted successfully",
delete_failure: "An error has occurred",
},
},
recurring_commands: {
action: {
create_success: "Recurring command created successfully",
update_success: "Recurring command updated successfully",
update_failure: "An error has occurred",
delete_success: "Recurring command deleted successfully",
delete_failure: "An error has occurred",
},
},
}; };
export default en; export default en;

View File

@@ -17,7 +17,8 @@ const fa: SynapseTranslationMessages = {
access_token: "توکن دسترسی", access_token: "توکن دسترسی",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.", title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.",
content: "آیا می‌خواهید این جلسه (که می‌تواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟", content:
"آیا می‌خواهید این جلسه (که می‌تواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟",
confirm: "نابودی جلسه", confirm: "نابودی جلسه",
cancel: "فقط خروج از پنل مدیریت", cancel: "فقط خروج از پنل مدیریت",
}, },
@@ -51,7 +52,7 @@ const fa: SynapseTranslationMessages = {
goToPdf: "رفتن به PDF", goToPdf: "رفتن به PDF",
cards: { cards: {
importstats: { importstats: {
header: "وارد کردن کاربران", header: "کاربران پردازش شده برای وارد کردن",
users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file", users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
guest_count: "%{smart_count} guest |||| %{smart_count} guests", guest_count: "%{smart_count} guest |||| %{smart_count} guests",
admin_count: "%{smart_count} admin |||| %{smart_count} admins", admin_count: "%{smart_count} admin |||| %{smart_count} admins",
@@ -166,7 +167,8 @@ const fa: SynapseTranslationMessages = {
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.", deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
suspend: "کاربران معلق نمی توانند وارد شوند و پیام های آنها به دیگران نمایش داده نمی شود.", suspend: "کاربران معلق نمی توانند وارد شوند و پیام های آنها به دیگران نمایش داده نمی شود.",
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید", erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.", erase_text:
"وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.", erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.", modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
username_available: "نام کاربری موجود", username_available: "نام کاربری موجود",
@@ -186,7 +188,8 @@ const fa: SynapseTranslationMessages = {
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)", redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
generate_password: "توليد رمز عبور", generate_password: "توليد رمز عبور",
overwrite_title: "هشدار!", overwrite_title: "هشدار!",
overwrite_content: "این نام کاربری قبلا استفاده شده است. آیا مطمئن هستید که می خواهید کاربر موجود را بازنویسی کنید؟", overwrite_content:
"این نام کاربری قبلا استفاده شده است. آیا مطمئن هستید که می خواهید کاربر موجود را بازنویسی کنید؟",
overwrite_cancel: "انصراف", overwrite_cancel: "انصراف",
overwrite_confirm: "بازنویسی", overwrite_confirm: "بازنویسی",
}, },
@@ -200,7 +203,7 @@ const fa: SynapseTranslationMessages = {
title: "داده های کاربر", title: "داده های کاربر",
global: "عمومی", global: "عمومی",
rooms: "اتاق ها", rooms: "اتاق ها",
} },
}, },
rooms: { rooms: {
name: "اتاق |||| اتاق ها", name: "اتاق |||| اتاق ها",
@@ -263,10 +266,11 @@ const fa: SynapseTranslationMessages = {
assign_admin: "مدیر انتخاب کنید", assign_admin: "مدیر انتخاب کنید",
title: "مدیر اتاق %{roomName} را انتخاب کنید", title: "مدیر اتاق %{roomName} را انتخاب کنید",
confirm: "مدیر انتخاب کنید", confirm: "مدیر انتخاب کنید",
content: "کامل MXID کاربری را وارد کنید که به عنوان مدیر تنظیم شود.\nهشدار: برای این کار، اتاق باید حداقل یک اعضای محلی به عنوان مدیر داشته باشد.", content:
"کامل MXID کاربری را وارد کنید که به عنوان مدیر تنظیم شود.\nهشدار: برای این کار، اتاق باید حداقل یک اعضای محلی به عنوان مدیر داشته باشد.",
success: "کاربر به عنوان مدیر اتاق تنظیم شد.", success: "کاربر به عنوان مدیر اتاق تنظیم شد.",
failure: "کاربر به عنوان مدیر اتاق تنظیم نشد. %{errMsg}", failure: "کاربر به عنوان مدیر اتاق تنظیم نشد. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@@ -413,8 +417,8 @@ const fa: SynapseTranslationMessages = {
info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.", info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "راهنمای اتاق", name: "راهنمای اتاق",

View File

@@ -17,14 +17,20 @@ const fr: SynapseTranslationMessages = {
access_token: "Jeton d'accès", access_token: "Jeton d'accès",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Vous utilisez un jeton d'accès Matrix existant.", title: "Vous utilisez un jeton d'accès Matrix existant.",
content: "Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?", content:
"Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?",
confirm: "Détruire la session", confirm: "Détruire la session",
cancel: "Se déconnecter simplement du panneau d'administration", cancel: "Se déconnecter simplement du panneau d'administration",
}, },
}, },
users: { users: {
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.", invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.",
tabs: { sso: "Authentification unique", experimental: "Expérimental", limits: "Limites", account_data: "Données du compte" }, tabs: {
sso: "Authentification unique",
experimental: "Expérimental",
limits: "Limites",
account_data: "Données du compte",
},
}, },
rooms: { rooms: {
tabs: { tabs: {
@@ -52,7 +58,7 @@ const fr: SynapseTranslationMessages = {
goToPdf: "Voir le PDF", goToPdf: "Voir le PDF",
cards: { cards: {
importstats: { importstats: {
header: "Importer des utilisateurs", header: "Utilisateurs analysés pour l'import",
users_total: users_total:
"%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV", "%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs", guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs",
@@ -168,7 +174,8 @@ const fr: SynapseTranslationMessages = {
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.", deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.", suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.",
erase: "Marquer l'utilisateur comme effacé conformément au RGPD", erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
erase_text: "Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.", erase_text:
"Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.", erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.", modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
username_available: "Nom d'utilisateur disponible", username_available: "Nom d'utilisateur disponible",
@@ -188,7 +195,8 @@ const fr: SynapseTranslationMessages = {
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)", redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
generate_password: "Générer un mot de passe", generate_password: "Générer un mot de passe",
overwrite_title: "Attention !", overwrite_title: "Attention !",
overwrite_content: "Ce nom d'utilisateur est déjà pris. Êtes-vous sûr de vouloir écraser l'utilisateur existant ?", overwrite_content:
"Ce nom d'utilisateur est déjà pris. Êtes-vous sûr de vouloir écraser l'utilisateur existant ?",
overwrite_cancel: "Annuler", overwrite_cancel: "Annuler",
overwrite_confirm: "Écraser", overwrite_confirm: "Écraser",
}, },
@@ -202,7 +210,7 @@ const fr: SynapseTranslationMessages = {
title: "Données du compte", title: "Données du compte",
global: "Globales", global: "Globales",
rooms: "Salons", rooms: "Salons",
} },
}, },
rooms: { rooms: {
name: "Salon |||| Salons", name: "Salon |||| Salons",
@@ -265,10 +273,11 @@ const fr: SynapseTranslationMessages = {
assign_admin: "Assigner un administrateur", assign_admin: "Assigner un administrateur",
title: "Assigner un administrateur au salon %{roomName}", title: "Assigner un administrateur au salon %{roomName}",
confirm: "Assigner un administrateur", confirm: "Assigner un administrateur",
content: "Entrez la MXID complète de l'utilisateur qui sera désigné comme administrateur.\nAttention : pour que cela fonctionne, la salle doit avoir au moins un membre local en tant qu'administrateur.", content:
"Entrez la MXID complète de l'utilisateur qui sera désigné comme administrateur.\nAttention : pour que cela fonctionne, la salle doit avoir au moins un membre local en tant qu'administrateur.",
success: "L'utilisateur a été désigné comme administrateur de la salle.", success: "L'utilisateur a été désigné comme administrateur de la salle.",
failure: "L'utilisateur n'a pas pu être désigné comme administrateur de la salle. %{errMsg}", failure: "L'utilisateur n'a pas pu être désigné comme administrateur de la salle. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@@ -415,8 +424,8 @@ const fr: SynapseTranslationMessages = {
info: "Cette liste contient les médias qui ont été téléchargés dans le salon. Il n'est pas possible de supprimer les médias qui ont été téléversés dans des dépôts de médias externes.", info: "Cette liste contient les médias qui ont été téléchargés dans le salon. Il n'est pas possible de supprimer les médias qui ont été téléversés dans des dépôts de médias externes.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "Répertoire des salons", name: "Répertoire des salons",

26
src/i18n/index.d.ts vendored
View File

@@ -22,7 +22,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
}; };
users: { users: {
invalid_user_id: string; invalid_user_id: string;
tabs: { sso: string; experimental: string; limits: string; account_data: string; }; tabs: { sso: string; experimental: string; limits: string; account_data: string };
}; };
rooms: { rooms: {
details?: string; // TODO: fa, fr, it, zh details?: string; // TODO: fa, fr, it, zh
@@ -188,7 +188,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
support: string; support: string;
regular: string; regular: string;
system_managed: string; system_managed: string;
} };
limits: { limits: {
messages_per_second: string; messages_per_second: string;
messages_per_second_text: string; messages_per_second_text: string;
@@ -199,7 +199,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
title: string; title: string;
global: string; global: string;
rooms: string; rooms: string;
} };
}; };
rooms: { rooms: {
name: string; name: string;
@@ -252,7 +252,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
content: string; content: string;
fields: { fields: {
block: string; block: string;
}, };
success: string; success: string;
failure: string; failure: string;
}; };
@@ -473,4 +473,22 @@ interface SynapseTranslationMessages extends TranslationMessages {
}; };
}; };
}; };
scheduled_commands?: {
action: {
create_success: string;
update_success: string;
update_failure: string;
delete_success: string;
delete_failure: string;
};
};
recurring_commands?: {
action: {
create_success: string;
update_success: string;
update_failure: string;
delete_success: string;
delete_failure: string;
};
};
} }

View File

@@ -17,7 +17,8 @@ const it: SynapseTranslationMessages = {
access_token: "Token di accesso", access_token: "Token di accesso",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Stai utilizzando un token di accesso Matrix esistente.", title: "Stai utilizzando un token di accesso Matrix esistente.",
content: "Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?", content:
"Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?",
confirm: "Distruggi sessione", confirm: "Distruggi sessione",
cancel: "Disconnetti solo dal pannello di amministrazione", cancel: "Disconnetti solo dal pannello di amministrazione",
}, },
@@ -51,7 +52,7 @@ const it: SynapseTranslationMessages = {
goToPdf: "Vai al PDF", goToPdf: "Vai al PDF",
cards: { cards: {
importstats: { importstats: {
header: "Importa utenti", header: "Utenti analizzati per l'importazione",
users_total: "%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV", users_total: "%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV",
guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti", guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti",
admin_count: "%{smart_count} amministratore |||| %{smart_count} amministratori", admin_count: "%{smart_count} amministratore |||| %{smart_count} amministratori",
@@ -166,7 +167,8 @@ const it: SynapseTranslationMessages = {
deactivate: "Devi fornire una password per riattivare l'account.", deactivate: "Devi fornire una password per riattivare l'account.",
suspend: "Sospendi l'utente", suspend: "Sospendi l'utente",
erase: "Constrassegna l'utente come cancellato dal GDPR", erase: "Constrassegna l'utente come cancellato dal GDPR",
erase_text: "Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.", erase_text:
"Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.",
erase_admin_error: "Non è consentito eliminare il proprio utente.", erase_admin_error: "Non è consentito eliminare il proprio utente.",
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.", modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
username_available: "Nome utente disponibile", username_available: "Nome utente disponibile",
@@ -186,7 +188,8 @@ const it: SynapseTranslationMessages = {
redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)", redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)",
generate_password: "Genera password", generate_password: "Genera password",
overwrite_title: "Attenzione!", overwrite_title: "Attenzione!",
overwrite_content: "Questo nome utente è già stato utilizzato. Sei sicuro di voler sovrascrivere l'utente esistente?", overwrite_content:
"Questo nome utente è già stato utilizzato. Sei sicuro di voler sovrascrivere l'utente esistente?",
overwrite_cancel: "Annulla", overwrite_cancel: "Annulla",
overwrite_confirm: "Sovrascrivi", overwrite_confirm: "Sovrascrivi",
}, },
@@ -200,7 +203,7 @@ const it: SynapseTranslationMessages = {
title: "Dati del profilo", title: "Dati del profilo",
global: "Globale", global: "Globale",
rooms: "Stanza", rooms: "Stanza",
} },
}, },
rooms: { rooms: {
name: "Stanza |||| Stanze", name: "Stanza |||| Stanze",
@@ -228,230 +231,231 @@ const it: SynapseTranslationMessages = {
helper: { helper: {
/* forward_extremities: /* forward_extremities:
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */ "Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
}, },
enums: { enums: {
join_rules: { join_rules: {
public: "Pubblica", public: "Pubblica",
knock: "Bussa", knock: "Bussa",
invite: "Invita", invite: "Invita",
private: "Privata", private: "Privata",
},
guest_access: {
can_join: "Gli utenti ospiti possono entrare",
forbidden: "Gli utenti ospiti non possono entrare",
},
history_visibility: {
invited: "Dall'invito",
joined: "Dall'entrata",
shared: "Dalla condivisione",
world_readable: "Chiunque",
},
unencrypted: "Non criptata",
}, },
guest_access: { action: {
can_join: "Gli utenti ospiti possono entrare", erase: {
forbidden: "Gli utenti ospiti non possono entrare", title: "Cancella stanza",
}, content:
history_visibility: { "Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
invited: "Dall'invito", },
joined: "Dall'entrata", make_admin: {
shared: "Dalla condivisione", assign_admin: "Assegna un amministratore",
world_readable: "Chiunque", title: "Assegna un amministratore alla stanza %{roomName}",
}, confirm: "Assegna un amministratore",
unencrypted: "Non criptata", content:
}, "Inserisci la MXID completa dell'utente che sarà designato come amministratore.\nAttenzione: per questo funzionare, la stanza deve avere almeno un membro locale come amministratore.",
action: { success: "L'utente è stato designato come amministratore della stanza.",
erase: { failure: "L'utente non può essere designato come amministratore della stanza. %{errMsg}",
title: "Cancella stanza",
content:
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
},
make_admin: {
assign_admin: "Assegna un amministratore",
title: "Assegna un amministratore alla stanza %{roomName}",
confirm: "Assegna un amministratore",
content: "Inserisci la MXID completa dell'utente che sarà designato come amministratore.\nAttenzione: per questo funzionare, la stanza deve avere almeno un membro locale come amministratore.",
success: "L'utente è stato designato come amministratore della stanza.",
failure: "L'utente non può essere designato come amministratore della stanza. %{errMsg}",
}
},
},
reports: {
name: "Evento segnalato |||| Eventi segnalati",
fields: {
id: "ID",
received_ts: "Orario del report",
user_id: "richiedente",
name: "nome della stanza",
score: "punteggio",
reason: "ragione",
event_id: "ID dell'evento",
event_json: {
origin: "server di origine",
origin_server_ts: "ora dell'invio",
type: "tipo di evento",
content: {
msgtype: "tipo di contenuto",
body: "contenuto",
format: "formato",
formatted_body: "contenuto formattato",
algorithm: "algoritmo",
}, },
}, },
}, },
}, reports: {
connections: { name: "Evento segnalato |||| Eventi segnalati",
name: "Connessioni", fields: {
fields: { id: "ID",
last_seen: "Data", received_ts: "Orario del report",
ip: "Indirizzo IP", user_id: "richiedente",
user_agent: "agente utente", name: "nome della stanza",
}, score: "punteggio",
}, reason: "ragione",
devices: { event_id: "ID dell'evento",
name: "Dispositivo |||| Dispositivi", event_json: {
fields: { origin: "server di origine",
device_id: "ID del dispositivo", origin_server_ts: "ora dell'invio",
display_name: "Nome del dispositivo", type: "tipo di evento",
last_seen_ts: "Timestamp", content: {
last_seen_ip: "Indirizzo IP", msgtype: "tipo di contenuto",
}, body: "contenuto",
action: { format: "formato",
erase: { formatted_body: "contenuto formattato",
title: "Rimozione del dispositivo %{id}", algorithm: "algoritmo",
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?', },
success: "Dispositivo rimosso con successo.", },
failure: "C'è stato un errore.",
}, },
}, },
}, connections: {
users_media: { name: "Connessioni",
name: "Media", fields: {
fields: { last_seen: "Data",
media_id: "ID del media", ip: "Indirizzo IP",
media_length: "Peso del file (in Byte)", user_agent: "agente utente",
media_type: "Tipo", },
upload_name: "Nome del file",
quarantined_by: "In quarantena da",
safe_from_quarantine: "Protetto dalla quarantena",
created_ts: "Creato",
last_access_ts: "Ultimo accesso",
}, },
}, devices: {
protect_media: { name: "Dispositivo |||| Dispositivi",
action: { fields: {
create: "Non protetto, proteggi", device_id: "ID del dispositivo",
delete: "Protetto, rimuovi protezione", display_name: "Nome del dispositivo",
none: "In quarantena", last_seen_ts: "Timestamp",
send_success: "Stato della protezione cambiato con successo.", last_seen_ip: "Indirizzo IP",
send_failure: "C'è stato un errore.", },
action: {
erase: {
title: "Rimozione del dispositivo %{id}",
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
success: "Dispositivo rimosso con successo.",
failure: "C'è stato un errore.",
},
},
}, },
}, users_media: {
quarantine_media: { name: "Media",
action: { fields: {
name: "Quarantina", media_id: "ID del media",
create: "Aggiungi alla quarantena", media_length: "Peso del file (in Byte)",
delete: "In quarantena, rimuovi dalla quarantena", media_type: "Tipo",
none: "Protetto dalla quarantena", upload_name: "Nome del file",
send_success: "Stato della quarantena cambiato con successo.", quarantined_by: "In quarantena da",
send_failure: "C'è stato un errore.", safe_from_quarantine: "Protetto dalla quarantena",
created_ts: "Creato",
last_access_ts: "Ultimo accesso",
},
}, },
}, protect_media: {
pushers: { action: {
name: "Pusher |||| Pusher", create: "Non protetto, proteggi",
fields: { delete: "Protetto, rimuovi protezione",
app: "App", none: "In quarantena",
app_display_name: "Nome dell'app", send_success: "Stato della protezione cambiato con successo.",
app_id: "ID dell'app", send_failure: "C'è stato un errore.",
device_display_name: "Nome del dispositivo", },
kind: "Tipo",
lang: "Lingua",
profile_tag: "Tag del profilo",
pushkey: "Pushkey",
data: { url: "URL" },
}, },
}, quarantine_media: {
servernotices: { action: {
name: "Avvisi del server", name: "Quarantina",
send: "Invia avvisi", create: "Aggiungi alla quarantena",
fields: { delete: "In quarantena, rimuovi dalla quarantena",
body: "Messaggio", none: "Protetto dalla quarantena",
send_success: "Stato della quarantena cambiato con successo.",
send_failure: "C'è stato un errore.",
},
}, },
action: { pushers: {
send: "Invia nota", name: "Pusher |||| Pusher",
send_success: "Avviso inviato con successo.", fields: {
send_failure: "C'è stato un errore.", app: "App",
app_display_name: "Nome dell'app",
app_id: "ID dell'app",
device_display_name: "Nome del dispositivo",
kind: "Tipo",
lang: "Lingua",
profile_tag: "Tag del profilo",
pushkey: "Pushkey",
data: { url: "URL" },
},
}, },
helper: { servernotices: {
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.', name: "Avvisi del server",
send: "Invia avvisi",
fields: {
body: "Messaggio",
},
action: {
send: "Invia nota",
send_success: "Avviso inviato con successo.",
send_failure: "C'è stato un errore.",
},
helper: {
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
},
}, },
}, user_media_statistics: {
user_media_statistics: { name: "Media degli utenti",
name: "Media degli utenti", fields: {
fields: { media_count: "Numero media",
media_count: "Numero media", media_length: "Lunghezza media",
media_length: "Lunghezza media", },
}, },
}, forward_extremities: {
forward_extremities: { name: "Invia estremità",
name: "Invia estremità", fields: {
fields: { id: "Event ID",
id: "Event ID", received_ts: "Timestamp",
received_ts: "Timestamp", depth: "Profondità",
depth: "Profondità", state_group: "State group",
state_group: "State group", },
}, },
}, room_state: {
room_state: { name: "Eventi di stato",
name: "Eventi di stato", fields: {
fields: { type: "Tipo",
type: "Tipo", content: "Contenuto",
content: "Contenuto", origin_server_ts: "Ora dell'invio",
origin_server_ts: "Ora dell'invio", sender: "Mittente",
sender: "Mittente", },
}, },
}, room_media: {
room_media: { name: "Media",
name: "Media", fields: {
fields: { media_id: "ID Media",
media_id: "ID Media", },
helper: {
info: "Questo è un elenco dei media caricati nella stanza. Non è possibile eliminare i media caricati su repository esterni.",
},
action: {
error: "%{errcode} (%{errstatus}) %{error}",
},
}, },
helper: { room_directory: {
info: "Questo è un elenco dei media caricati nella stanza. Non è possibile eliminare i media caricati su repository esterni.", name: "Elenco delle stanze",
fields: {
world_readable: "gli utenti ospite possono vedere senza entrare",
guest_can_join: "gli utenti ospite possono entrare",
},
action: {
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
content:
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
erase: "Rimuovi dall'elenco",
create: "Crea",
send_success: "Stanza creata con successo.",
send_failure: "C'è stato un errore.",
},
}, },
action: { destinations: {
error: "%{errcode} (%{errstatus}) %{error}" name: "Federazione",
} fields: {
}, destination: "Destinazione",
room_directory: { failure_ts: "Timestamp dell'errore",
name: "Elenco delle stanze", retry_last_ts: "Tentativo ultimo timestamp",
fields: { retry_interval: "Intervallo dei tentativi",
world_readable: "gli utenti ospite possono vedere senza entrare", last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
guest_can_join: "gli utenti ospite possono entrare", stream_ordering: "Flusso",
},
action: { reconnect: "Riconnetti" },
}, },
action: { registration_tokens: {
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco", name: "Token di registrazione",
content: fields: {
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?", token: "Token",
erase: "Rimuovi dall'elenco", valid: "Token valido",
create: "Crea", uses_allowed: "Usi permessi",
send_success: "Stanza creata con successo.", pending: "In attesa",
send_failure: "C'è stato un errore.", completed: "Completato",
expiry_time: "Data della scadenza",
length: "Lunghezza",
},
helper: { length: "Lunghezza del token se non viene dato alcun token." },
}, },
}, },
destinations: {
name: "Federazione",
fields: {
destination: "Destinazione",
failure_ts: "Timestamp dell'errore",
retry_last_ts: "Tentativo ultimo timestamp",
retry_interval: "Intervallo dei tentativi",
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
stream_ordering: "Flusso",
},
action: { reconnect: "Riconnetti" },
},
registration_tokens: {
name: "Token di registrazione",
fields: {
token: "Token",
valid: "Token valido",
uses_allowed: "Usi permessi",
pending: "In attesa",
completed: "Completato",
expiry_time: "Data della scadenza",
length: "Lunghezza",
},
helper: { length: "Lunghezza del token se non viene dato alcun token." },
},
},
}; };
export default it; export default it;

View File

@@ -23,14 +23,12 @@ const fixedRussianMessages = {
}, },
message: { message: {
...russianMessages.ra.message, ...russianMessages.ra.message,
access_denied: access_denied: "У вас нет прав доступа к этой странице.",
"У вас нет прав доступа к этой странице.", authentication_error: "Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
authentication_error:
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
select_all_limit_reached: "Слишком много элементов для выбора. Были выбраны только первые %{max} элементов.", select_all_limit_reached: "Слишком много элементов для выбора. Были выбраны только первые %{max} элементов.",
}, },
}, },
} };
const ru: SynapseTranslationMessages = { const ru: SynapseTranslationMessages = {
...fixedRussianMessages, ...fixedRussianMessages,
@@ -48,14 +46,20 @@ const ru: SynapseTranslationMessages = {
access_token: "Токен доступа", access_token: "Токен доступа",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Вы используете существующий токен доступа Matrix.", title: "Вы используете существующий токен доступа Matrix.",
content: "Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?", content:
"Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?",
confirm: "Завершить сессию", confirm: "Завершить сессию",
cancel: "Просто выйти из панели администрирования", cancel: "Просто выйти из панели администрирования",
}, },
}, },
users: { users: {
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.", invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения", account_data: "Данные пользователя" }, tabs: {
sso: "SSO",
experimental: "Экспериментальные",
limits: "Ограничения",
account_data: "Данные пользователя",
},
}, },
rooms: { rooms: {
details: "Данные комнаты", details: "Данные комнаты",
@@ -83,7 +87,7 @@ const ru: SynapseTranslationMessages = {
goToPdf: "Перейти к PDF", goToPdf: "Перейти к PDF",
cards: { cards: {
importstats: { importstats: {
header: "Импорт пользователей", header: "Анализированные пользователи для импорта",
users_total: users_total:
"%{smart_count} пользователь в CSV файле |||| %{smart_count} пользователя в CSV файле |||| %{smart_count} пользователей в CSV файле", "%{smart_count} пользователь в CSV файле |||| %{smart_count} пользователя в CSV файле |||| %{smart_count} пользователей в CSV файле",
guest_count: "%{smart_count} гость |||| %{smart_count} гостя |||| %{smart_count} гостей", guest_count: "%{smart_count} гость |||| %{smart_count} гостя |||| %{smart_count} гостей",
@@ -206,9 +210,11 @@ const ru: SynapseTranslationMessages = {
password: "Смена пароля завершит все сессии пользователя.", password: "Смена пароля завершит все сессии пользователя.",
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.", create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.", deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
suspend: "Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.", suspend:
"Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR", erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.", erase_text:
"Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
erase_admin_error: "Удаление собственного пользователя запрещено.", erase_admin_error: "Удаление собственного пользователя запрещено.",
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.", modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
username_available: "Имя пользователя доступно", username_available: "Имя пользователя доступно",
@@ -228,7 +234,8 @@ const ru: SynapseTranslationMessages = {
redact_events: "Удаление всех событий, отправленных пользователем (-ами)", redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
generate_password: "Сгенерировать пароль", generate_password: "Сгенерировать пароль",
overwrite_title: "Предупреждение!", overwrite_title: "Предупреждение!",
overwrite_content: "Это имя пользователя уже занято. Вы уверены, что хотите перезаписать существующего пользователя?", overwrite_content:
"Это имя пользователя уже занято. Вы уверены, что хотите перезаписать существующего пользователя?",
overwrite_cancel: "Отмена", overwrite_cancel: "Отмена",
overwrite_confirm: "Перезаписать", overwrite_confirm: "Перезаписать",
}, },
@@ -242,7 +249,7 @@ const ru: SynapseTranslationMessages = {
title: "Данные пользователя", title: "Данные пользователя",
global: "Глобальные", global: "Глобальные",
rooms: "Комнаты", rooms: "Комнаты",
} },
}, },
rooms: { rooms: {
name: "Комната |||| Комнаты", name: "Комната |||| Комнаты",
@@ -308,10 +315,11 @@ const ru: SynapseTranslationMessages = {
assign_admin: "Назначить администратора", assign_admin: "Назначить администратора",
title: "Назначить администратора комнате %{roomName}", title: "Назначить администратора комнате %{roomName}",
confirm: "Назначить администратора", confirm: "Назначить администратора",
content: "Введите полную MXID пользователя, которого нужно назначить администратором.\nПредупреждение: для этого должен быть назначен хотя бы один локальный участник в качестве администратора.", content:
"Введите полную MXID пользователя, которого нужно назначить администратором.\nПредупреждение: для этого должен быть назначен хотя бы один локальный участник в качестве администратора.",
success: "Пользователь назначен администратором комнаты.", success: "Пользователь назначен администратором комнаты.",
failure: "Пользователь не может быть назначен администратором комнаты. %{errMsg}", failure: "Пользователь не может быть назначен администратором комнаты. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@@ -471,8 +479,8 @@ const ru: SynapseTranslationMessages = {
info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.", info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "Каталог комнат", name: "Каталог комнат",

View File

@@ -27,14 +27,13 @@ const fixedChineseMessages = {
}, },
message: { message: {
...chineseMessages.ra.message, ...chineseMessages.ra.message,
access_denied: access_denied: "您没有访问此页面的权限。",
"您没有访问此页面的权限。", authentication_error: "身份验证服务器返回错误,无法验证您的凭据。",
authentication_error:
"身份验证服务器返回错误,无法验证您的凭据。",
select_all_limit_reached: "选择的元素太多。只选择了前 %{max} 个元素。", select_all_limit_reached: "选择的元素太多。只选择了前 %{max} 个元素。",
placeholder_data_warning: "网络问题:数据刷新失败。",
}, },
}, },
} };
const zh: SynapseTranslationMessages = { const zh: SynapseTranslationMessages = {
...fixedChineseMessages, ...fixedChineseMessages,
@@ -85,7 +84,7 @@ const zh: SynapseTranslationMessages = {
goToPdf: "转到 PDF", goToPdf: "转到 PDF",
cards: { cards: {
importstats: { importstats: {
header: "导入用户", header: "分析用于导入用户",
users_total: "%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中", users_total: "%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客", guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员", admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
@@ -198,7 +197,8 @@ const zh: SynapseTranslationMessages = {
deactivate: "您必须提供一串密码来激活账户。", deactivate: "您必须提供一串密码来激活账户。",
suspend: "您必须提供一串密码来暂停账户。", suspend: "您必须提供一串密码来暂停账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了", erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。", erase_text:
"这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
erase_admin_error: "不允许删除自己的用户", erase_admin_error: "不允许删除自己的用户",
modify_managed_user_error: "不允许修改系统管理的用户。", modify_managed_user_error: "不允许修改系统管理的用户。",
username_available: "用户名可用", username_available: "用户名可用",
@@ -232,7 +232,7 @@ const zh: SynapseTranslationMessages = {
title: "账户数据", title: "账户数据",
global: "全局", global: "全局",
rooms: "房间", rooms: "房间",
} },
}, },
rooms: { rooms: {
name: "房间", name: "房间",
@@ -383,8 +383,8 @@ const zh: SynapseTranslationMessages = {
info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。", info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
}, },
}; };

View File

@@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { App } from "./App"; import { App } from "./App";
@@ -9,9 +8,9 @@ import { FetchConfig, GetConfig } from "./utils/config";
await FetchConfig(); await FetchConfig();
createRoot(document.getElementById("root")).render( createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>
<AppContext.Provider value={GetConfig()}> <AppContext.Provider value={GetConfig()}>
<App /> <App />
</AppContext.Provider> </AppContext.Provider>
</React.StrictMode> </React.StrictMode>
); );

View File

@@ -1,8 +1,6 @@
import polyglotI18nProvider from "ra-i18n-polyglot";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { AdminContext } from "react-admin"; import { AdminContext } from "react-admin";
import { BrowserRouter } from "react-router-dom";
import LoginPage from "./LoginPage"; import LoginPage from "./LoginPage";
import { AppContext } from "../Context"; import { AppContext } from "../Context";
@@ -15,11 +13,9 @@ describe("LoginForm", () => {
it("renders with no restriction to homeserver", async () => { it("renders with no restriction to homeserver", async () => {
await act(async () => { await act(async () => {
render( render(
<BrowserRouter> <AdminContext i18nProvider={i18nProvider}>
<AdminContext i18nProvider={i18nProvider}> <LoginPage />
<LoginPage /> </AdminContext>
</AdminContext>
</BrowserRouter>
); );
}); });
@@ -36,15 +32,18 @@ describe("LoginForm", () => {
it("renders with single restricted homeserver", () => { it("renders with single restricted homeserver", () => {
render( render(
<BrowserRouter> <AppContext.Provider
<AppContext.Provider value={{
value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }} restrictBaseUrl: "https://matrix.example.com",
> asManagedUsers: [],
menu: [],
corsCredentials: "include",
}}
>
<AdminContext i18nProvider={i18nProvider}> <AdminContext i18nProvider={i18nProvider}>
<LoginPage /> <LoginPage />
</AdminContext> </AdminContext>
</AppContext.Provider> </AppContext.Provider>
</BrowserRouter>
); );
screen.getByText(englishMessages.synapseadmin.auth.welcome); screen.getByText(englishMessages.synapseadmin.auth.welcome);
@@ -61,16 +60,15 @@ describe("LoginForm", () => {
it("renders with multiple restricted homeservers", async () => { it("renders with multiple restricted homeservers", async () => {
render( render(
<AppContext.Provider <AppContext.Provider
value={{ value={{
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"], restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
asManagedUsers: [], asManagedUsers: [],
menu: [], menu: [],
corsCredentials: "include",
}} }}
> >
<AdminContext i18nProvider={i18nProvider}> <AdminContext i18nProvider={i18nProvider}>
<BrowserRouter> <LoginPage />
<LoginPage />
</BrowserRouter>
</AdminContext> </AdminContext>
</AppContext.Provider> </AppContext.Provider>
); );

View File

@@ -1,6 +1,17 @@
import {
Avatar,
Box,
Button,
Card,
CardActions,
CircularProgress,
MenuItem,
Select,
Tab,
Tabs,
Typography,
} from "@mui/material";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
import { import {
Form, Form,
FormDataConsumer, FormDataConsumer,
@@ -15,8 +26,10 @@ import {
useLocales, useLocales,
} from "react-admin"; } from "react-admin";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import LoginFormBox from "../components/LoginFormBox";
import { useAppContext } from "../Context"; import { useAppContext } from "../Context";
import Footer from "../components/Footer";
import LoginFormBox from "../components/LoginFormBox";
import { import {
getServerVersion, getServerVersion,
getSupportedFeatures, getSupportedFeatures,
@@ -25,7 +38,6 @@ import {
isValidBaseUrl, isValidBaseUrl,
splitMxid, splitMxid,
} from "../synapse/matrix"; } from "../synapse/matrix";
import Footer from "../components/Footer";
export type LoginMethod = "credentials" | "accessToken"; export type LoginMethod = "credentials" | "accessToken";
@@ -47,7 +59,7 @@ const LoginPage = () => {
const translate = useTranslate(); const translate = useTranslate();
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url"); const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState(""); const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = new URLSearchParams(window.location.search).get("loginToken") const loginToken = new URLSearchParams(window.location.search).get("loginToken");
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials"); const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
useEffect(() => { useEffect(() => {
@@ -57,8 +69,8 @@ const LoginPage = () => {
console.log("SSO token is", loginToken); console.log("SSO token is", loginToken);
// Prevent further requests // Prevent further requests
const previousUrl = new URL(window.location.toString()) const previousUrl = new URL(window.location.toString());
previousUrl.searchParams.delete("loginToken") previousUrl.searchParams.delete("loginToken");
window.history.replaceState({}, "", previousUrl.toString()); window.history.replaceState({}, "", previousUrl.toString());
const baseUrl = localStorage.getItem("sso_base_url"); const baseUrl = localStorage.getItem("sso_base_url");
localStorage.removeItem("sso_base_url"); localStorage.removeItem("sso_base_url");
@@ -281,7 +293,7 @@ const LoginPage = () => {
{loading ? ( {loading ? (
<CircularProgress size={25} thickness={2} /> <CircularProgress size={25} thickness={2} />
) : ( ) : (
<Avatar sx={{ width: "120px", height: "120px" }} src="./images/logo.webp"/> <Avatar sx={{ width: "120px", height: "120px" }} src="./images/logo.webp" />
)} )}
</Box> </Box>
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box> <Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
@@ -300,37 +312,35 @@ const LoginPage = () => {
))} ))}
</Select> </Select>
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer> <FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
{loginMethod === "credentials" && <CardActions className="actions"> {loginMethod === "credentials" && (
<Button <CardActions className="actions">
variant="contained" <Button
type="submit" variant="contained"
color="primary" type="submit"
disabled={loading || !supportPassAuth} color="primary"
fullWidth disabled={loading || !supportPassAuth}
> fullWidth
{translate("ra.auth.sign_in")} >
</Button> {translate("ra.auth.sign_in")}
<Button </Button>
variant="contained" <Button
color="secondary" variant="contained"
onClick={handleSSO} color="secondary"
disabled={loading || ssoBaseUrl === ""} onClick={handleSSO}
fullWidth disabled={loading || ssoBaseUrl === ""}
> fullWidth
{translate("synapseadmin.auth.sso_sign_in")} >
</Button> {translate("synapseadmin.auth.sso_sign_in")}
</CardActions>} </Button>
{loginMethod === "accessToken" && <CardActions className="actions"> </CardActions>
<Button )}
variant="contained" {loginMethod === "accessToken" && (
type="submit" <CardActions className="actions">
color="primary" <Button variant="contained" type="submit" color="primary" disabled={loading} fullWidth>
disabled={loading} {translate("ra.auth.sign_in")}
fullWidth </Button>
> </CardActions>
{translate("ra.auth.sign_in")} )}
</Button>
</CardActions>}
</Box> </Box>
</Card> </Card>
</LoginFormBox> </LoginFormBox>

View File

@@ -1,10 +1,10 @@
import { MouseEvent } from "react";
import AutorenewIcon from "@mui/icons-material/Autorenew"; import AutorenewIcon from "@mui/icons-material/Autorenew";
import DestinationsIcon from "@mui/icons-material/CloudQueue"; import DestinationsIcon from "@mui/icons-material/CloudQueue";
import ErrorIcon from "@mui/icons-material/Error";
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";
import ErrorIcon from '@mui/icons-material/Error'; import { get } from "lodash";
import { MouseEvent } from "react";
import { import {
Button, Button,
Datagrid, Datagrid,
@@ -34,7 +34,6 @@ import {
} from "react-admin"; } from "react-admin";
import { DATE_FORMAT } from "../utils/date"; import { DATE_FORMAT } from "../utils/date";
import { get } from "lodash";
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@@ -101,19 +100,18 @@ const RetryDateField = (props: DateFieldProps) => {
return <DateField {...props} />; return <DateField {...props} />;
}; };
const destinationFieldRender = (record: RaRecord) => { const destinationFieldRender = (record: RaRecord) => {
if (record.retry_last_ts > 0) { if (record.retry_last_ts > 0) {
return ( return (
<> <>
<ErrorIcon fontSize="inherit" color="error" sx={{verticalAlign: "middle"}}/> <ErrorIcon fontSize="inherit" color="error" sx={{ verticalAlign: "middle" }} />
{record.destination} {record.destination}
</> </>
); );
} }
return <> {record.destination} </>; return <> {record.destination} </>;
} };
export const DestinationList = (props: ListProps) => { export const DestinationList = (props: ListProps) => {
const record = useRecordContext(props); const record = useRecordContext(props);
@@ -126,11 +124,28 @@ export const DestinationList = (props: ListProps) => {
perPage={50} perPage={50}
> >
<DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}> <DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<FunctionField source="destination" render={destinationFieldRender} label="resources.destinations.fields.destination" /> <FunctionField
<DateField source="failure_ts" showTime options={DATE_FORMAT} label="resources.destinations.fields.failure_ts" /> source="destination"
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} label="resources.destinations.fields.retry_last_ts" /> render={destinationFieldRender}
label="resources.destinations.fields.destination"
/>
<DateField
source="failure_ts"
showTime
options={DATE_FORMAT}
label="resources.destinations.fields.failure_ts"
/>
<RetryDateField
source="retry_last_ts"
showTime
options={DATE_FORMAT}
label="resources.destinations.fields.retry_last_ts"
/>
<TextField source="retry_interval" label="resources.destinations.fields.retry_interval" /> <TextField source="retry_interval" label="resources.destinations.fields.retry_interval" />
<TextField source="last_successful_stream_ordering" label="resources.destinations.fields.last_successful_stream_ordering" /> <TextField
source="last_successful_stream_ordering"
label="resources.destinations.fields.last_successful_stream_ordering"
/>
<DestinationReconnectButton /> <DestinationReconnectButton />
</DatagridConfigurable> </DatagridConfigurable>
</List> </List>

View File

@@ -41,11 +41,17 @@ export const RegistrationTokenList = (props: ListProps) => (
perPage={50} perPage={50}
> >
<DatagridConfigurable rowClick="edit"> <DatagridConfigurable rowClick="edit">
<TextField source="token" sortable={false} label="resources.registration_tokens.fields.token"/> <TextField source="token" sortable={false} label="resources.registration_tokens.fields.token" />
<NumberField source="uses_allowed" sortable={false} label="resources.registration_tokens.fields.uses_allowed" /> <NumberField source="uses_allowed" sortable={false} label="resources.registration_tokens.fields.uses_allowed" />
<NumberField source="pending" sortable={false} label="resources.registration_tokens.fields.pending" /> <NumberField source="pending" sortable={false} label="resources.registration_tokens.fields.pending" />
<NumberField source="completed" sortable={false} label="resources.registration_tokens.fields.completed" /> <NumberField source="completed" sortable={false} label="resources.registration_tokens.fields.completed" />
<DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} label="resources.registration_tokens.fields.expiry_time" /> <DateField
source="expiry_time"
showTime
options={DATE_FORMAT}
sortable={false}
label="resources.registration_tokens.fields.expiry_time"
/>
</DatagridConfigurable> </DatagridConfigurable>
</List> </List>
); );

View File

@@ -22,8 +22,8 @@ import {
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import { DATE_FORMAT } from "../utils/date";
import { ReportMediaContent } from "../components/media"; import { ReportMediaContent } from "../components/media";
import { DATE_FORMAT } from "../utils/date";
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@@ -93,7 +93,13 @@ export const ReportList = (props: ListProps) => (
<List {...props} pagination={<ReportPagination />} perPage={50} sort={{ field: "received_ts", order: "DESC" }}> <List {...props} pagination={<ReportPagination />} perPage={50} sort={{ field: "received_ts", order: "DESC" }}>
<DatagridConfigurable rowClick="show" bulkActionButtons={false}> <DatagridConfigurable rowClick="show" bulkActionButtons={false}>
<TextField source="id" sortable={false} label="resources.reports.fields.id" /> <TextField source="id" sortable={false} label="resources.reports.fields.id" />
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} label="resources.reports.fields.received_ts" /> <DateField
source="received_ts"
showTime
options={DATE_FORMAT}
sortable={true}
label="resources.reports.fields.received_ts"
/>
<TextField sortable={false} source="user_id" label="resources.reports.fields.user_id" /> <TextField sortable={false} source="user_id" label="resources.reports.fields.user_id" />
<TextField sortable={false} source="name" label="resources.reports.fields.name" /> <TextField sortable={false} source="name" label="resources.reports.fields.name" />
<TextField sortable={false} source="score" label="resources.reports.fields.score" /> <TextField sortable={false} source="score" label="resources.reports.fields.score" />

View File

@@ -1,4 +1,5 @@
import RoomDirectoryIcon from "@mui/icons-material/FolderShared"; import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
import { useMutation } from "@tanstack/react-query";
import { import {
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
@@ -25,9 +26,9 @@ import {
useRefresh, useRefresh,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "@tanstack/react-query";
import AvatarField from "../components/AvatarField";
import { MakeAdminBtn } from "./rooms"; import { MakeAdminBtn } from "./rooms";
import AvatarField from "../components/AvatarField";
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />; const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => { export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
@@ -141,11 +142,7 @@ export const RoomDirectoryList = () => (
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />} bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
omit={["room_id", "canonical_alias", "topic"]} omit={["room_id", "canonical_alias", "topic"]}
> >
<AvatarField <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} label="resources.rooms.fields.avatar" />
source="avatar_src"
sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar"
/>
<TextField source="name" sortable={false} label="resources.rooms.fields.name" /> <TextField source="name" sortable={false} label="resources.rooms.fields.name" />
<TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" /> <TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" />
<TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" /> <TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" />

View File

@@ -4,12 +4,19 @@ import UserIcon from "@mui/icons-material/Group";
import HttpsIcon from "@mui/icons-material/Https"; import HttpsIcon from "@mui/icons-material/Https";
import NoEncryptionIcon from "@mui/icons-material/NoEncryption"; import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
import PageviewIcon from "@mui/icons-material/Pageview"; import PageviewIcon from "@mui/icons-material/Pageview";
import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonIcon from "@mui/icons-material/Person";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import RoomIcon from "@mui/icons-material/ViewList"; import RoomIcon from "@mui/icons-material/ViewList";
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import PermMediaIcon from "@mui/icons-material/PermMedia"; import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { import {
BooleanField, BooleanField,
DateField, DateField,
@@ -40,27 +47,20 @@ import {
useNotify, useNotify,
DeleteButton, DeleteButton,
} from "react-admin"; } from "react-admin";
import { useDataProvider } from "react-admin";
import { Confirm } from "react-admin";
import TextField from "@mui/material/TextField";
import { import {
RoomDirectoryBulkUnpublishButton, RoomDirectoryBulkUnpublishButton,
RoomDirectoryBulkPublishButton, RoomDirectoryBulkPublishButton,
RoomDirectoryUnpublishButton, RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton, RoomDirectoryPublishButton,
} from "./room_directory"; } from "./room_directory";
import { DATE_FORMAT } from "../utils/date"; import AvatarField from "../components/AvatarField";
import DeleteRoomButton from "../components/DeleteRoomButton"; import DeleteRoomButton from "../components/DeleteRoomButton";
import { MediaIDField } from "../components/media"; import { MediaIDField } from "../components/media";
import AvatarField from "../components/AvatarField";
import { Room } from "../synapse/dataProvider"; import { Room } from "../synapse/dataProvider";
import { useMutation } from "@tanstack/react-query"; import { DATE_FORMAT } from "../utils/date";
import { useDataProvider } from "react-admin";
import { Confirm } from "react-admin";
import { useState } from "react";
import Button from "@mui/material/Button";
import PersonIcon from '@mui/icons-material/Person';
import Typography from "@mui/material/Typography";
import Alert from "@mui/material/Alert";
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@@ -110,7 +110,6 @@ export const MakeAdminBtn = () => {
return null; return null;
} }
const ownMXID = localStorage.getItem("user_id") || ""; const ownMXID = localStorage.getItem("user_id") || "";
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [userIdValue, setUserIdValue] = useState(ownMXID); const [userIdValue, setUserIdValue] = useState(ownMXID);
@@ -134,12 +133,12 @@ export const MakeAdminBtn = () => {
setOpen(false); setOpen(false);
setUserIdValue(""); setUserIdValue("");
}, },
onError: (err) => { onError: err => {
const errorMessage = err instanceof Error ? err.message : "Unknown error"; const errorMessage = err instanceof Error ? err.message : "Unknown error";
notify("resources.rooms.action.make_admin.failure", { type: "error", messageArgs: { errMsg: errorMessage } }); notify("resources.rooms.action.make_admin.failure", { type: "error", messageArgs: { errMsg: errorMessage } });
setOpen(false); setOpen(false);
setUserIdValue(""); setUserIdValue("");
} },
}); });
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -161,8 +160,16 @@ export const MakeAdminBtn = () => {
} }
}; };
return (<> return (
<Button size="small" onClick={(e) => { e.stopPropagation(); setOpen(true); }} disabled={isPending}> <>
<Button
size="small"
onClick={e => {
e.stopPropagation();
setOpen(true);
}}
disabled={isPending}
>
<PersonIcon /> {translate("resources.rooms.action.make_admin.assign_admin")} <PersonIcon /> {translate("resources.rooms.action.make_admin.assign_admin")}
</Button> </Button>
<Confirm <Confirm
@@ -171,20 +178,27 @@ export const MakeAdminBtn = () => {
onClose={handleDialogClose} onClose={handleDialogClose}
confirm="resources.rooms.action.make_admin.confirm" confirm="resources.rooms.action.make_admin.confirm"
cancel="ra.action.cancel" cancel="ra.action.cancel"
title={translate("resources.rooms.action.make_admin.title", { roomName: record.name ? record.name : record.room_id })} title={translate("resources.rooms.action.make_admin.title", {
content={<> roomName: record.name ? record.name : record.room_id,
<Typography sx={{ marginBottom: 2, whiteSpace: "pre-line"}}>{translate("resources.rooms.action.make_admin.content")}</Typography> })}
<TextField content={
type="text" <>
variant="filled" <Typography sx={{ marginBottom: 2, whiteSpace: "pre-line" }}>
value={userIdValue} {translate("resources.rooms.action.make_admin.content")}
onChange={handleChange} </Typography>
onKeyDown={handleKeyDown} <TextField
label={"Matrix ID"} type="text"
/> variant="filled"
</>} value={userIdValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
label={"Matrix ID"}
/>
</>
}
/> />
</>); </>
);
}; };
export const RoomShow = (props: ShowProps) => { export const RoomShow = (props: ShowProps) => {
@@ -194,11 +208,7 @@ export const RoomShow = (props: ShowProps) => {
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}> <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
<TabbedShowLayout> <TabbedShowLayout>
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}> <Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
<AvatarField <AvatarField source="avatar" sx={{ height: "120px", width: "120px" }} label="resources.rooms.fields.avatar" />
source="avatar"
sx={{ height: "120px", width: "120px" }}
label="resources.rooms.fields.avatar"
/>
<RaTextField source="room_id" /> <RaTextField source="room_id" />
<RaTextField source="name" /> <RaTextField source="name" />
<RaTextField source="topic" /> <RaTextField source="topic" />
@@ -301,7 +311,11 @@ export const RoomShow = (props: ShowProps) => {
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}> <Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
<RaTextField source="type" sortable={false} /> <RaTextField source="type" sortable={false} />
<DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} /> <DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} />
<FunctionField source="content" sortable={false} render={record => `${JSON.stringify(record.content, null, 2)}`} /> <FunctionField
source="content"
sortable={false}
render={record => `${JSON.stringify(record.content, null, 2)}`}
/>
<ReferenceField source="sender" reference="users" sortable={false}> <ReferenceField source="sender" reference="users" sortable={false}>
<RaTextField source="id" /> <RaTextField source="id" />
</ReferenceField> </ReferenceField>
@@ -373,7 +387,13 @@ export const RoomList = (props: ListProps) => {
bulkActionButtons={<RoomBulkActionButtons />} bulkActionButtons={<RoomBulkActionButtons />}
omit={["joined_local_members", "state_events", "version", "federatable"]} omit={["joined_local_members", "state_events", "version", "federatable"]}
> >
<ReferenceField reference="rooms" source="id" label="resources.users.fields.avatar" link={false} sortable={false}> <ReferenceField
reference="rooms"
source="id"
label="resources.users.fields.avatar"
link={false}
sortable={false}
>
<AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} />
</ReferenceField> </ReferenceField>
<RaTextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> <RaTextField source="id" label="resources.rooms.fields.room_id" sortable={false} />
@@ -390,7 +410,15 @@ export const RoomList = (props: ListProps) => {
}} }}
/> />
</WrapperField> </WrapperField>
<FunctionField source="name" render={record => record["name"] || record["canonical_alias"] || record["id"]} label="resources.rooms.fields.name" /> <FunctionField
source="name"
sx={{
wordBreak: "break-word",
overflowWrap: "break-word",
}}
render={record => record["name"] || record["canonical_alias"] || record["id"]}
label="resources.rooms.fields.name"
/>
<RaTextField source="joined_members" label="resources.rooms.fields.joined_members" /> <RaTextField source="joined_members" label="resources.rooms.fields.joined_members" />
<RaTextField source="joined_local_members" label="resources.rooms.fields.joined_local_members" /> <RaTextField source="joined_local_members" label="resources.rooms.fields.joined_local_members" />
<RaTextField source="state_events" label="resources.rooms.fields.state_events" /> <RaTextField source="state_events" label="resources.rooms.fields.state_events" />

View File

@@ -1,19 +1,19 @@
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";
import DocumentScannerIcon from "@mui/icons-material/DocumentScanner";
import GetAppIcon from "@mui/icons-material/GetApp"; import GetAppIcon from "@mui/icons-material/GetApp";
import UserIcon from "@mui/icons-material/Group"; import UserIcon from "@mui/icons-material/Group";
import LockClockIcon from "@mui/icons-material/LockClock";
import NotificationsIcon from "@mui/icons-material/Notifications"; 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 ScienceIcon from "@mui/icons-material/Science"; import ScienceIcon from "@mui/icons-material/Science";
import LockClockIcon from '@mui/icons-material/LockClock'; import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import DocumentScannerIcon from "@mui/icons-material/DocumentScanner";
import { useEffect, useState } from "react";
import { Alert, Typography } from "@mui/material"; import { Alert, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { import {
ArrayInput, ArrayInput,
ArrayField, ArrayField,
@@ -67,23 +67,23 @@ import {
useCreate, useCreate,
useRedirect, useRedirect,
} from "react-admin"; } from "react-admin";
import { useFormContext } from "react-hook-form";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { MakeAdminBtn } from "./rooms";
import AvatarField from "../components/AvatarField"; import AvatarField from "../components/AvatarField";
import DeleteUserButton from "../components/DeleteUserButton"; import DeleteUserButton from "../components/DeleteUserButton";
import { isASManaged } from "../utils/mxid";
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { DATE_FORMAT } from "../utils/date";
import DeviceRemoveButton from "../components/DeviceRemoveButton"; import DeviceRemoveButton from "../components/DeviceRemoveButton";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
import { generateRandomPassword } from "../utils/password";
import { useFormContext } from "react-hook-form";
import ExperimentalFeaturesList from "../components/ExperimentalFeatures"; import ExperimentalFeaturesList from "../components/ExperimentalFeatures";
import UserRateLimits from "../components/UserRateLimits"; import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
import { MakeAdminBtn } from "./rooms";
import UserAccountData from "../components/UserAccountData"; import UserAccountData from "../components/UserAccountData";
import UserRateLimits from "../components/UserRateLimits";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
import { DATE_FORMAT } from "../utils/date";
import decodeURLComponent from "../utils/decodeURLComponent"; import decodeURLComponent from "../utils/decodeURLComponent";
import { isASManaged } from "../utils/mxid";
import { generateRandomPassword } from "../utils/password";
const choices_medium = [ const choices_medium = [
{ id: "email", name: "resources.users.email" }, { id: "email", name: "resources.users.email" },
@@ -118,7 +118,11 @@ const userFilters = [
<BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />, <BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />,
]; ];
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean; asManagedUserIsSelected: boolean }> = props => { const UserPreventSelfDelete: React.FC<{
children: React.ReactNode;
ownUserIsSelected: boolean;
asManagedUserIsSelected: boolean;
}> = props => {
const ownUserIsSelected = props.ownUserIsSelected; const ownUserIsSelected = props.ownUserIsSelected;
const asManagedUserIsSelected = props.asManagedUserIsSelected; const asManagedUserIsSelected = props.asManagedUserIsSelected;
const notify = useNotify(); const notify = useNotify();
@@ -167,19 +171,39 @@ export const UserList = (props: ListProps) => (
<List <List
{...props} {...props}
filters={userFilters} filters={userFilters}
filterDefaultValues={{ guests: true, deactivated: false, locked: false, suspended: false }} filterDefaultValues={{ guests: false, deactivated: false, locked: false, suspended: false }}
sort={{ field: "name", order: "ASC" }} sort={{ field: "name", order: "ASC" }}
actions={<UserListActions />} actions={<UserListActions />}
pagination={<UserPagination />} pagination={<UserPagination />}
perPage={50} perPage={50}
> >
<DatagridConfigurable <DatagridConfigurable
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`} rowClick={(id: Identifier, resource: string) => `/${resource}/${encodeURIComponent(id)}`}
bulkActionButtons={<UserBulkActionButtons />} bulkActionButtons={<UserBulkActionButtons />}
> >
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" label="resources.users.fields.avatar" /> <AvatarField
<TextField source="id" sortBy="name" label="resources.users.fields.id" /> source="avatar_src"
<TextField source="displayname" label="resources.users.fields.displayname" /> sx={{ height: "40px", width: "40px" }}
sortBy="avatar_url"
label="resources.users.fields.avatar"
/>
<TextField
source="id"
sx={{
wordBreak: "break-word",
overflowWrap: "break-word",
}}
sortBy="name"
label="resources.users.fields.id"
/>
<TextField
source="displayname"
sx={{
wordBreak: "break-word",
overflowWrap: "break-word",
}}
label="resources.users.fields.displayname"
/>
<BooleanField source="is_guest" label="resources.users.fields.is_guest" /> <BooleanField source="is_guest" label="resources.users.fields.is_guest" />
<BooleanField source="admin" label="resources.users.fields.admin" /> <BooleanField source="admin" label="resources.users.fields.admin" />
<BooleanField source="deactivated" label="resources.users.fields.deactivated" /> <BooleanField source="deactivated" label="resources.users.fields.deactivated" />
@@ -214,13 +238,13 @@ const UserEditActions = () => {
<TopToolbar> <TopToolbar>
{!record?.deactivated && <ServerNoticeButton />} {!record?.deactivated && <ServerNoticeButton />}
{record && record.id && ( {record && record.id && (
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}> <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
<DeleteUserButton <DeleteUserButton
selectedIds={[record?.id]} selectedIds={[record?.id]}
confirmTitle="resources.users.helper.erase" confirmTitle="resources.users.helper.erase"
confirmContent="resources.users.helper.erase_text" confirmContent="resources.users.helper.erase_text"
/> />
</UserPreventSelfDelete> </UserPreventSelfDelete>
)} )}
</TopToolbar> </TopToolbar>
); );
@@ -235,18 +259,28 @@ export const UserCreate = (props: CreateProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [userIsAvailable, setUserIsAvailable] = useState<boolean | undefined>(); const [userIsAvailable, setUserIsAvailable] = useState<boolean | undefined>();
const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(<Typography component="span"></Typography>); const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(
<Typography component="span"></Typography>
);
const [formData, setFormData] = useState<Record<string, any>>({}); const [formData, setFormData] = useState<Record<string, any>>({});
const [create] = useCreate(); const [create] = useCreate();
const checkAvailability = async(event: React.FocusEvent<HTMLInputElement>) => { const checkAvailability = async (event: React.FocusEvent<HTMLInputElement>) => {
const username = event.target.value; const username = event.target.value;
const result: UsernameAvailabilityResult = await dataProvider.checkUsernameAvailability(username); const result: UsernameAvailabilityResult = await dataProvider.checkUsernameAvailability(username);
setUserIsAvailable(!!result?.available); setUserIsAvailable(!!result?.available);
if (result?.available) { if (result?.available) {
setUserAvailabilityEl(<Typography component="span" variant="body2" sx={{ color: theme.palette.success.main }}> {translate("resources.users.helper.username_available")}</Typography>); setUserAvailabilityEl(
<Typography component="span" variant="body2" sx={{ color: theme.palette.success.main }}>
{translate("resources.users.helper.username_available")}
</Typography>
);
} else { } else {
setUserAvailabilityEl(<Typography component="span" variant="body2" sx={{ color: theme.palette.warning.main }}> {result?.error || "unknown error"}</Typography>); setUserAvailabilityEl(
<Typography component="span" variant="body2" sx={{ color: theme.palette.warning.main }}>
{result?.error || "unknown error"}
</Typography>
);
} }
}; };
@@ -257,12 +291,18 @@ export const UserCreate = (props: CreateProps) => {
return; return;
} }
create("users", { data: data }, { create(
onSuccess: (resource: User) => { "users",
notify("ra.notification.created", { messageArgs: { smart_count: 1 } }); { data: data },
redirect(() => { return `users/${resource.id}` }); {
onSuccess: (resource: User) => {
notify("ra.notification.created", { messageArgs: { smart_count: 1 } });
redirect(() => {
return `users/${encodeURIComponent(resource.id as string)}`;
});
},
} }
}); );
}; };
const handleConfirm = () => { const handleConfirm = () => {
@@ -275,39 +315,48 @@ export const UserCreate = (props: CreateProps) => {
}; };
const updateUser = () => { const updateUser = () => {
create("users", { data: formData }, { create(
onSuccess: (resource: User) => { "users",
notify("ra.notification.updated", { messageArgs: { smart_count: 1 } }); { data: formData },
redirect(() => { return `users/${resource.id}` }); {
onSuccess: (resource: User) => {
notify("ra.notification.updated", { messageArgs: { smart_count: 1 } });
redirect(() => {
return `users/${encodeURIComponent(resource.id as string)}`;
});
},
} }
}); );
} };
return <Create return (
{...props} <Create {...props}>
> <SimpleForm onSubmit={postSave}>
<SimpleForm <TextInput
onSubmit={postSave} source="id"
> autoComplete="off"
<TextInput source="id" autoComplete="off" validate={validateUser} onBlur={checkAvailability} helperText={userAvailabilityEl}/> validate={validateUser}
<TextInput source="displayname" validate={maxLength(256)} /> onBlur={checkAvailability}
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" /> helperText={userAvailabilityEl}
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable /> />
<BooleanInput source="admin" /> <TextInput source="displayname" validate={maxLength(256)} />
<ArrayInput source="threepids"> <UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
<SimpleFormIterator disableReordering> <SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
<SelectInput source="medium" choices={choices_medium} validate={required()} /> <BooleanInput source="admin" />
<TextInput source="address" validate={validateAddress} /> <ArrayInput source="threepids">
</SimpleFormIterator> <SimpleFormIterator disableReordering>
</ArrayInput> <SelectInput source="medium" choices={choices_medium} validate={required()} />
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso"> <TextInput source="address" validate={validateAddress} />
<SimpleFormIterator disableReordering> </SimpleFormIterator>
<TextInput source="auth_provider" validate={required()} /> </ArrayInput>
<TextInput source="external_id" label="resources.users.fields.id" validate={required()} /> <ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
</SimpleFormIterator> <SimpleFormIterator disableReordering>
</ArrayInput> <TextInput source="auth_provider" validate={required()} />
</SimpleForm> <TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
<Confirm </SimpleFormIterator>
</ArrayInput>
</SimpleForm>
<Confirm
isOpen={open} isOpen={open}
title="resources.users.action.overwrite_title" title="resources.users.action.overwrite_title"
content="resources.users.action.overwrite_content" content="resources.users.action.overwrite_content"
@@ -316,7 +365,8 @@ export const UserCreate = (props: CreateProps) => {
confirm="resources.users.action.overwrite_confirm" confirm="resources.users.action.overwrite_confirm"
cancel="resources.users.action.overwrite_cancel" cancel="resources.users.action.overwrite_cancel"
/> />
</Create> </Create>
);
}; };
const UserTitle = () => { const UserTitle = () => {
@@ -326,7 +376,7 @@ const UserTitle = () => {
return null; return null;
} }
let username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : "" const username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : "";
return ( return (
<span> <span>
{translate("resources.users.name", { {translate("resources.users.name", {
@@ -352,7 +402,10 @@ const UserEditToolbar = () => {
<div className={ToolbarClasses.defaultToolbar}> <div className={ToolbarClasses.defaultToolbar}>
<Toolbar sx={{ justifyContent: "space-between" }}> <Toolbar sx={{ justifyContent: "space-between" }}>
<SaveButton /> <SaveButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}> <UserPreventSelfDelete
ownUserIsSelected={ownUserIsSelected}
asManagedUserIsSelected={asManagedUserIsSelected}
>
<DeleteButton /> <DeleteButton />
</UserPreventSelfDelete> </UserPreventSelfDelete>
</Toolbar> </Toolbar>
@@ -395,8 +448,18 @@ const UserPasswordInput = props => {
return ( return (
<> <>
<PasswordInput {...props} helperText={asManagedUserIsSelected ? "resources.users.helper.modify_managed_user_error" : (record ? "resources.users.helper.password" : "resources.users.helper.create_password")} disabled={asManagedUserIsSelected} /> <PasswordInput
<Button {...props}
helperText={
asManagedUserIsSelected
? "resources.users.helper.modify_managed_user_error"
: record
? "resources.users.helper.password"
: "resources.users.helper.create_password"
}
disabled={asManagedUserIsSelected}
/>
<Button
variant="outlined" variant="outlined"
label="resources.users.action.generate_password" label="resources.users.action.generate_password"
onClick={generatePassword} onClick={generatePassword}
@@ -411,11 +474,17 @@ export const UserEdit = (props: EditProps) => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic" queryOptions={{ <Edit
meta: { {...props}
include: ["features"] // Tell your dataProvider to include features title={<UserTitle />}
} actions={<UserEditActions />}
}}> mutationMode="pessimistic"
queryOptions={{
meta: {
include: ["features"], // Tell your dataProvider to include features
},
}}
>
<TabbedForm toolbar={<UserEditToolbar />}> <TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}> <FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} /> <AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} />
@@ -425,16 +494,26 @@ export const UserEdit = (props: EditProps) => {
label="resources.users.fields.avatar" label="resources.users.fields.avatar"
accept={{ "image/*": [".png", ".jpg"] }} accept={{ "image/*": [".png", ".jpg"] }}
> >
<ImageField source="src" title="Avatar" sx={{ '& img': { <ImageField
width: "120px !important", source="src"
height: "120px !important", title="Avatar"
objectFit: "cover !important", sx={{
borderRadius: '50% !important', "& img": {
}}} /> width: "120px !important",
height: "120px !important",
objectFit: "cover !important",
borderRadius: "50% !important",
},
}}
/>
</ImageInput> </ImageInput>
<TextInput source="id" readOnly /> <TextInput source="id" readOnly />
<TextInput source="displayname" /> <TextInput source="displayname" />
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" /> <UserPasswordInput
source="password"
autoComplete="new-password"
helperText="resources.users.helper.password"
/>
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable /> <SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
<UserBooleanInput source="locked" /> <UserBooleanInput source="locked" />
@@ -500,13 +579,16 @@ export const UserEdit = (props: EditProps) => {
perPage={10} perPage={10}
sort={{ field: "created_ts", order: "DESC" }} sort={{ field: "created_ts", order: "DESC" }}
> >
<Datagrid sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton/>}> <Datagrid sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton />}>
<MediaIDField source="media_id" /> <MediaIDField source="media_id" />
<DateField source="created_ts" showTime options={DATE_FORMAT} /> <DateField source="created_ts" showTime options={DATE_FORMAT} />
<DateField source="last_access_ts" showTime options={DATE_FORMAT} /> <DateField source="last_access_ts" showTime options={DATE_FORMAT} />
<NumberField source="media_length" /> <NumberField source="media_length" />
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} /> <TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
<FunctionField source="upload_name" render={record => record.upload_name ? decodeURLComponent(record.upload_name) : ""} /> <FunctionField
source="upload_name"
render={record => (record.upload_name ? decodeURLComponent(record.upload_name) : "")}
/>
<TextField source="quarantined_by" /> <TextField source="quarantined_by" />
<QuarantineMediaButton label="resources.quarantine_media.action.name" /> <QuarantineMediaButton label="resources.quarantine_media.action.name" />
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" /> <ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
@@ -516,22 +598,46 @@ export const UserEdit = (props: EditProps) => {
</FormTab> </FormTab>
<FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms"> <FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms">
<ReferenceManyField reference="joined_rooms" target="user_id" label={false} perPage={10} pagination={<Pagination />}> <ReferenceManyField
<Datagrid sx={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}> reference="joined_rooms"
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> target="user_id"
<AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> label={false}
</ReferenceField> perPage={10}
<TextField source="id" label="resources.rooms.fields.room_id" sortable={false}/> pagination={<Pagination />}
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.name" link={false} sortable={false}> >
<TextField source="name" /> <Datagrid sx={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}>
</ReferenceField> <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.joined_members" link={false} sortable={false}> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} />
<TextField source="joined_members" sortable={false} /> </ReferenceField>
</ReferenceField> <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} />
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> <ReferenceField
<MakeAdminBtn /> reference="rooms"
</ReferenceField> source="id"
</Datagrid> label="resources.rooms.fields.name"
link={false}
sortable={false}
>
<TextField
source="name"
sx={{
wordBreak: "break-word",
overflowWrap: "break-word",
}}
/>
</ReferenceField>
<ReferenceField
reference="rooms"
source="id"
label="resources.rooms.fields.joined_members"
link={false}
sortable={false}
>
<TextField source="joined_members" sortable={false} />
</ReferenceField>
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
<MakeAdminBtn />
</ReferenceField>
</Datagrid>
</ReferenceManyField> </ReferenceManyField>
</FormTab> </FormTab>
@@ -563,7 +669,7 @@ export const UserEdit = (props: EditProps) => {
</FormTab> </FormTab>
<FormTab label="synapseadmin.users.tabs.account_data" icon={<DocumentScannerIcon />} path="accountdata"> <FormTab label="synapseadmin.users.tabs.account_data" icon={<DocumentScannerIcon />} path="accountdata">
<UserAccountData /> <UserAccountData />
</FormTab> </FormTab>
</TabbedForm> </TabbedForm>
</Edit> </Edit>

View File

@@ -1,7 +1,7 @@
import fetchMock from "jest-fetch-mock"; import fetchMock from "jest-fetch-mock";
import { HttpError } from "ra-core";
import authProvider from "./authProvider"; import authProvider from "./authProvider";
import { HttpError } from "ra-core";
fetchMock.enableMocks(); fetchMock.enableMocks();
@@ -28,13 +28,14 @@ describe("authProvider", () => {
password: "secret", password: "secret",
}); });
expect(ret).toEqual({redirectTo: "/"}); expect(ret).toEqual({ redirectTo: "/" });
expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/v3/login", { expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}', body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
credentials: "same-origin",
method: "POST", method: "POST",
}); });
expect(localStorage.getItem("base_url")).toEqual("http://example.com"); expect(localStorage.getItem("base_url")).toEqual("http://example.com");
@@ -59,13 +60,14 @@ describe("authProvider", () => {
loginToken: "login_token", loginToken: "login_token",
}); });
expect(ret).toEqual({redirectTo: "/"}); expect(ret).toEqual({ redirectTo: "/" });
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/v3/login", { expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}', body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
credentials: "same-origin",
method: "POST", method: "POST",
}); });
expect(localStorage.getItem("base_url")).toEqual("https://example.com"); expect(localStorage.getItem("base_url")).toEqual("https://example.com");
@@ -85,9 +87,11 @@ describe("authProvider", () => {
expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/v3/logout", { expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/v3/logout", {
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer foo", Authorization: "Bearer foo",
}), }),
method: "POST", method: "POST",
credentials: "same-origin",
user: { authenticated: true, token: "Bearer foo" }, user: { authenticated: true, token: "Bearer foo" },
}); });
expect(localStorage.getItem("access_token")).toBeNull(); expect(localStorage.getItem("access_token")).toBeNull();
@@ -100,11 +104,15 @@ describe("authProvider", () => {
}); });
it("should reject if error.status is 401", async () => { it("should reject if error.status is 401", async () => {
await expect(authProvider.checkError(new HttpError("test-error", 401, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined(); await expect(
authProvider.checkError(new HttpError("test-error", 401, { errcode: "test-errcode", error: "test-error" }))
).rejects.toBeDefined();
}); });
it("should reject if error.status is 403", async () => { it("should reject if error.status is 403", async () => {
await expect(authProvider.checkError(new HttpError("test-error", 403, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined(); await expect(
authProvider.checkError(new HttpError("test-error", 403, { errcode: "test-errcode", error: "test-error" }))
).rejects.toBeDefined();
}); });
}); });

View File

@@ -1,9 +1,9 @@
import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin"; import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
import { MatrixError, displayError } from "../utils/error";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { FetchConfig, ClearConfig, GetConfig } from "../utils/config"; import { FetchConfig, ClearConfig, GetConfig } from "../utils/config";
import decodeURLComponent from "../utils/decodeURLComponent"; import decodeURLComponent from "../utils/decodeURLComponent";
import { MatrixError, displayError } from "../utils/error";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const authProvider: AuthProvider = { const authProvider: AuthProvider = {
// called when the user attempts to log in // called when the user attempts to log in
@@ -23,6 +23,11 @@ const authProvider: AuthProvider = {
console.log("login "); console.log("login ");
let options: Options = { let options: Options = {
method: "POST", method: "POST",
credentials: GetConfig().corsCredentials as RequestCredentials,
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
body: JSON.stringify( body: JSON.stringify(
Object.assign( Object.assign(
{ {
@@ -52,14 +57,15 @@ const authProvider: AuthProvider = {
if (!base_url) { if (!base_url) {
// there is some kind of bug with base_url being present in the form, but not submitted // there is some kind of bug with base_url being present in the form, but not submitted
// ref: https://github.com/etkecc/synapse-admin/issues/14 // ref: https://github.com/etkecc/synapse-admin/issues/14
localStorage.removeItem("base_url") localStorage.removeItem("base_url");
throw new Error("Homeserver URL is required."); throw new Error("Homeserver URL is required.");
} }
base_url = base_url.replace(/\/+$/g, ""); base_url = base_url.replace(/\/+$/g, "");
localStorage.setItem("base_url", base_url); localStorage.setItem("base_url", base_url);
const decoded_base_url = decodeURLComponent(base_url); const decoded_base_url = decodeURLComponent(base_url);
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login"); const login_api_url =
decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
let response; let response;
@@ -68,7 +74,7 @@ const authProvider: AuthProvider = {
// this a login with an already obtained access token, let's just validate it // this a login with an already obtained access token, let's just validate it
options = { options = {
headers: new Headers({ headers: new Headers({
Accept: 'application/json', Accept: "application/json",
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}), }),
}; };
@@ -90,19 +96,16 @@ const authProvider: AuthProvider = {
pageToRedirectTo = "/server_status"; pageToRedirectTo = "/server_status";
} }
return Promise.resolve({redirectTo: pageToRedirectTo}); return Promise.resolve({ redirectTo: pageToRedirectTo });
} catch(err) { } catch (err) {
const error = err as HttpError; const error = err as HttpError;
const errorStatus = error.status; const errorStatus = error.status;
const errorBody = error.body as MatrixError; const errorBody = error.body as MatrixError;
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message); const errMsg = errorBody?.errcode
? displayError(errorBody.errcode, errorStatus, errorBody.error)
: displayError("M_INVALID", errorStatus, error.message);
return Promise.reject( return Promise.reject(new HttpError(errMsg, errorStatus));
new HttpError(
errMsg,
errorStatus,
)
);
} }
}, },
getIdentity: async () => { getIdentity: async () => {
@@ -151,6 +154,11 @@ const authProvider: AuthProvider = {
const options: Options = { const options: Options = {
method: "POST", method: "POST",
credentials: GetConfig().corsCredentials as RequestCredentials,
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
user: { user: {
authenticated: true, authenticated: true,
token: `Bearer ${access_token}`, token: `Bearer ${access_token}`,
@@ -173,7 +181,7 @@ const authProvider: AuthProvider = {
const status = err.status; const status = err.status;
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
return Promise.reject({message: displayError(errorBody.errcode, status, errorBody.error)}); return Promise.reject({ message: displayError(errorBody.errcode, status, errorBody.error) });
} }
return Promise.resolve(); return Promise.resolve();
}, },
@@ -182,8 +190,6 @@ const authProvider: AuthProvider = {
const access_token = localStorage.getItem("access_token"); const access_token = localStorage.getItem("access_token");
return typeof access_token === "string" ? Promise.resolve() : Promise.reject(); return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
}, },
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => Promise.resolve(),
}; };
export default authProvider; export default authProvider;

View File

@@ -13,8 +13,9 @@ import {
withLifecycleCallbacks, withLifecycleCallbacks,
} from "react-admin"; } from "react-admin";
import { returnMXID } from "../utils/mxid"; import { GetConfig } from "../utils/config";
import { MatrixError, displayError } from "../utils/error"; import { MatrixError, displayError } from "../utils/error";
import { returnMXID } from "../utils/mxid";
const CACHED_MANY_REF: Record<string, any> = {}; const CACHED_MANY_REF: Record<string, any> = {};
@@ -22,6 +23,7 @@ const CACHED_MANY_REF: Record<string, any> = {};
const jsonClient = async (url: string, options: Options = {}) => { const jsonClient = async (url: string, options: Options = {}) => {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem("access_token");
console.log("httpClient " + url); console.log("httpClient " + url);
options.credentials = GetConfig().corsCredentials as RequestCredentials;
if (token !== null) { if (token !== null) {
options.user = { options.user = {
authenticated: true, authenticated: true,
@@ -35,7 +37,7 @@ const jsonClient = async (url: string, options: Options = {}) => {
const error = err as HttpError; const error = err as HttpError;
const errorStatus = error.status; const errorStatus = error.status;
const errorBody = error.body as MatrixError; const errorBody = error.body as MatrixError;
const errMsg = !!errorBody?.errcode const errMsg = errorBody?.errcode
? displayError(errorBody.errcode, errorStatus, errorBody.error) ? displayError(errorBody.errcode, errorStatus, errorBody.error)
: displayError("M_INVALID", errorStatus, error.message); : displayError("M_INVALID", errorStatus, error.message);
@@ -252,9 +254,7 @@ export interface UploadMediaResult {
} }
export interface ExperimentalFeaturesModel { export interface ExperimentalFeaturesModel {
features: { features: Record<string, boolean>;
[key: string]: boolean;
};
} }
export interface RateLimitsModel { export interface RateLimitsModel {
@@ -264,13 +264,9 @@ export interface RateLimitsModel {
export interface AccountDataModel { export interface AccountDataModel {
account_data: { account_data: {
global: { global: Record<string, object>;
[key: string]: object; rooms: Record<string, object>;
}, };
rooms: {
[key: string]: object;
};
}
} }
export interface UsernameAvailabilityResult { export interface UsernameAvailabilityResult {
@@ -289,7 +285,7 @@ export interface ServerStatusComponent {
url: string; url: string;
icon: string; icon: string;
text: string; text: string;
} };
} }
export interface ServerStatusResponse { export interface ServerStatusResponse {
@@ -324,8 +320,22 @@ export interface ServerCommand {
additionalArgs?: string; additionalArgs?: string;
} }
export interface ServerCommandsResponse { export type ServerCommandsResponse = Record<string, ServerCommand>;
[command: string]: ServerCommand;
export interface ScheduledCommand {
args: string;
command: string;
id: string;
is_recurring: boolean;
scheduled_at: string;
}
export interface RecurringCommand {
args: string;
command: string;
id: string;
scheduled_at: string;
time: string;
} }
export interface SynapseDataProvider extends DataProvider { export interface SynapseDataProvider extends DataProvider {
@@ -343,6 +353,14 @@ export interface SynapseDataProvider extends DataProvider {
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>; getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>; deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
getServerCommands: (etkeAdminUrl: string) => Promise<ServerCommandsResponse>; getServerCommands: (etkeAdminUrl: string) => Promise<ServerCommandsResponse>;
getScheduledCommands: (etkeAdminUrl: string) => Promise<ScheduledCommand[]>;
getRecurringCommands: (etkeAdminUrl: string) => Promise<RecurringCommand[]>;
createScheduledCommand: (etkeAdminUrl: string, command: Partial<ScheduledCommand>) => Promise<ScheduledCommand>;
updateScheduledCommand: (etkeAdminUrl: string, command: ScheduledCommand) => Promise<ScheduledCommand>;
deleteScheduledCommand: (etkeAdminUrl: string, id: string) => Promise<{ success: boolean }>;
createRecurringCommand: (etkeAdminUrl: string, command: Partial<RecurringCommand>) => Promise<RecurringCommand>;
updateRecurringCommand: (etkeAdminUrl: string, command: RecurringCommand) => Promise<RecurringCommand>;
deleteRecurringCommand: (etkeAdminUrl: string, id: string) => Promise<{ success: boolean }>;
} }
const resourceMap = { const resourceMap = {
@@ -689,21 +707,23 @@ const baseDataProvider: SynapseDataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = base_url + res.path; const endpoint_url = base_url + res.path;
const responses = await Promise.all(params.ids.map(id => { const responses = await Promise.all(
// edge case: when user is external / federated, homeserver will return error, as querying external users via params.ids.map(id => {
// /_synapse/admin/v2/users is not allowed. // edge case: when user is external / federated, homeserver will return error, as querying external users via
// That leads to an issue when a user is referenced (e.g., in room state datagrid) - the user cell is just empty. // /_synapse/admin/v2/users is not allowed.
// To avoid that, we fake the response with one specific field (name) which is used in the datagrid. // That leads to an issue when a user is referenced (e.g., in room state datagrid) - the user cell is just empty.
if (homeserver && resource === "users") { // To avoid that, we fake the response with one specific field (name) which is used in the datagrid.
if (!(<string>id).endsWith(homeserver)) { if (homeserver && resource === "users") {
const json = { if (!(id as string).endsWith(homeserver)) {
const json = {
name: id, name: id,
}; };
return Promise.resolve({ json }); return Promise.resolve({ json });
}
} }
} return jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`);
return jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`); })
})); );
return { return {
data: responses.map(({ json }) => res.map(json)), data: responses.map(({ json }) => res.map(json)),
total: responses.length, total: responses.length,
@@ -722,8 +742,6 @@ const baseDataProvider: SynapseDataProvider = {
dir: getSearchOrder(order), dir: getSearchOrder(order),
}; };
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
@@ -732,16 +750,16 @@ const baseDataProvider: SynapseDataProvider = {
const ref = res.reference(params.id); const ref = res.reference(params.id);
const endpoint_url = `${homeserver}${ref.endpoint}?${new URLSearchParams(filterUndefined(query)).toString()}`; const endpoint_url = `${homeserver}${ref.endpoint}?${new URLSearchParams(filterUndefined(query)).toString()}`;
let CACHE_KEY = ref.endpoint; const CACHE_KEY = ref.endpoint;
let jsonData = []; let jsonData = [];
let total = 0; let total = 0;
if (CACHED_MANY_REF[CACHE_KEY]) { if (CACHED_MANY_REF[CACHE_KEY]) {
jsonData = CACHED_MANY_REF[CACHE_KEY]["data"].slice(from, from + perPage); jsonData = CACHED_MANY_REF[CACHE_KEY]["data"].slice(from, from + perPage);
total = CACHED_MANY_REF[CACHE_KEY]["total"]; total = CACHED_MANY_REF[CACHE_KEY]["total"];
} else { } else {
const { json } = await jsonClient(endpoint_url); const { json } = await jsonClient(endpoint_url);
jsonData = json[res.data] jsonData = json[res.data];
total = res.total(json, from, perPage); total = res.total(json, from, perPage);
if (resource === "joined_rooms") { if (resource === "joined_rooms") {
// cache will be applied only for joined_rooms // cache will be applied only for joined_rooms
@@ -965,9 +983,9 @@ const baseDataProvider: SynapseDataProvider = {
return json as AccountDataModel; return json as AccountDataModel;
}, },
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => { setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
const filtered = Object.entries(rateLimits). const filtered = Object.entries(rateLimits)
filter(([key, value]) => value !== null && value !== undefined). .filter(([key, value]) => value !== null && value !== undefined)
reduce((obj, [key, value]) => { .reduce((obj, [key, value]) => {
obj[key] = value; obj[key] = value;
return obj; return obj;
}, {}); }, {});
@@ -976,7 +994,7 @@ const baseDataProvider: SynapseDataProvider = {
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`; const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
if (Object.keys(filtered).length === 0) { if (Object.keys(filtered).length === 0) {
await jsonClient(endpoint_url, { method: "DELETE" }); await jsonClient(endpoint_url, { method: "DELETE" });
return return;
} }
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify(filtered) }); await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify(filtered) });
@@ -1008,7 +1026,7 @@ const baseDataProvider: SynapseDataProvider = {
throw error; throw error;
} }
}, },
getServerRunningProcess: async (etkeAdminUrl: string, burstCache: boolean = false): Promise<ServerProcessResponse> => { getServerRunningProcess: async (etkeAdminUrl: string, burstCache = false): Promise<ServerProcessResponse> => {
const locked_at = ""; const locked_at = "";
const command = ""; const command = "";
@@ -1020,8 +1038,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(serverURL, { const response = await fetch(serverURL, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
@@ -1043,7 +1061,7 @@ const baseDataProvider: SynapseDataProvider = {
return { locked_at, command }; return { locked_at, command };
}, },
getServerStatus: async (etkeAdminUrl: string, burstCache: boolean = false): Promise<ServerStatusResponse> => { getServerStatus: async (etkeAdminUrl: string, burstCache = false): Promise<ServerStatusResponse> => {
let serverURL = `${etkeAdminUrl}/status`; let serverURL = `${etkeAdminUrl}/status`;
if (burstCache) { if (burstCache) {
serverURL += `?time=${new Date().getTime()}`; serverURL += `?time=${new Date().getTime()}`;
@@ -1052,8 +1070,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(serverURL, { const response = await fetch(serverURL, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error getting server status: ${response.status} ${response.statusText}`); console.error(`Error getting server status: ${response.status} ${response.statusText}`);
@@ -1072,7 +1090,10 @@ const baseDataProvider: SynapseDataProvider = {
return { success: false, ok: false, host: "", results: [] }; return { success: false, ok: false, host: "", results: [] };
}, },
getServerNotifications: async (serverNotificationsUrl: string, burstCache: boolean = false): Promise<ServerNotificationsResponse> => { getServerNotifications: async (
serverNotificationsUrl: string,
burstCache = false
): Promise<ServerNotificationsResponse> => {
let serverURL = `${serverNotificationsUrl}/notifications`; let serverURL = `${serverNotificationsUrl}/notifications`;
if (burstCache) { if (burstCache) {
serverURL += `?time=${new Date().getTime()}`; serverURL += `?time=${new Date().getTime()}`;
@@ -1081,8 +1102,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(serverURL, { const response = await fetch(serverURL, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error getting server notifications: ${response.status} ${response.statusText}`); console.error(`Error getting server notifications: ${response.status} ${response.statusText}`);
@@ -1111,9 +1132,9 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(`${serverNotificationsUrl}/notifications`, { const response = await fetch(`${serverNotificationsUrl}/notifications`, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
}, },
method: "DELETE" method: "DELETE",
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error deleting server notifications: ${response.status} ${response.statusText}`); console.error(`Error deleting server notifications: ${response.status} ${response.statusText}`);
@@ -1122,7 +1143,7 @@ const baseDataProvider: SynapseDataProvider = {
const status = response.status; const status = response.status;
if (status === 204) { if (status === 204) {
const result = { success: true } const result = { success: true };
return result; return result;
} }
} catch (error) { } catch (error) {
@@ -1135,8 +1156,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(`${serverCommandsUrl}/commands`, { const response = await fetch(`${serverCommandsUrl}/commands`, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error fetching server commands: ${response.status} ${response.statusText}`); console.error(`Error fetching server commands: ${response.status} ${response.statusText}`);
@@ -1161,15 +1182,15 @@ const baseDataProvider: SynapseDataProvider = {
const endpoint_url = `${serverCommandsUrl}/commands`; const endpoint_url = `${serverCommandsUrl}/commands`;
const body = { const body = {
command: command, command: command,
...additionalArgs ...additionalArgs,
} };
const response = await fetch(endpoint_url, { const response = await fetch(endpoint_url, {
method: "POST", method: "POST",
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
@@ -1184,13 +1205,219 @@ const baseDataProvider: SynapseDataProvider = {
if (status === 204) { if (status === 204) {
return { return {
success: true, success: true,
} };
} }
return { return {
success: false, success: false,
};
},
getScheduledCommands: async (scheduledCommandsUrl: string) => {
try {
const response = await fetch(`${scheduledCommandsUrl}/schedules`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
});
if (!response.ok) {
console.error(`Error fetching scheduled commands: ${response.status} ${response.statusText}`);
return [];
}
const status = response.status;
if (status === 200) {
const json = await response.json();
return json as ScheduledCommand[];
}
return [];
} catch (error) {
console.error("Error fetching scheduled commands, error");
} }
} return [];
},
getRecurringCommands: async (recurringCommandsUrl: string) => {
try {
const response = await fetch(`${recurringCommandsUrl}/recurrings`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
});
if (!response.ok) {
console.error(`Error fetching recurring commands: ${response.status} ${response.statusText}`);
return [];
}
const status = response.status;
if (status === 200) {
const json = await response.json();
return json as RecurringCommand[];
}
return [];
} catch (error) {
console.error("Error fetching recurring commands, error");
}
return [];
},
createScheduledCommand: async (scheduledCommandsUrl: string, command: Partial<ScheduledCommand>) => {
try {
const response = await fetch(`${scheduledCommandsUrl}/schedules`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
body: JSON.stringify(command),
});
if (!response.ok) {
console.error(`Error creating scheduled command: ${response.status} ${response.statusText}`);
throw new Error("Failed to create scheduled command");
}
if (response.status === 204) {
return command as ScheduledCommand;
}
const json = await response.json();
return json as ScheduledCommand;
} catch (error) {
console.error("Error creating scheduled command", error);
throw error;
}
},
updateScheduledCommand: async (scheduledCommandsUrl: string, command: ScheduledCommand) => {
try {
// Use the base endpoint without ID and use PUT for upsert
const response = await fetch(`${scheduledCommandsUrl}/schedules`, {
method: "PUT", // Using PUT on the base endpoint
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
body: JSON.stringify(command),
});
if (!response.ok) {
const jsonErr = JSON.parse(await response.text());
console.error(`Error updating scheduled command: ${response.status} ${response.statusText}`);
throw new Error(jsonErr.error);
}
// According to docs, successful response is 204 No Content
if (response.status === 204) {
// Return the command object we sent since the server doesn't return data
return command;
}
// If server does return data (though docs suggest it returns 204)
const json = await response.json();
console.log("JSON", json);
return json as ScheduledCommand;
} catch (error) {
console.error("Error updating scheduled command", error);
throw error;
}
},
deleteScheduledCommand: async (scheduledCommandsUrl: string, id: string) => {
try {
const response = await fetch(`${scheduledCommandsUrl}/schedules/${id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
});
if (!response.ok) {
console.error(`Error deleting scheduled command: ${response.status} ${response.statusText}`);
return { success: false };
}
return { success: true };
} catch (error) {
console.error("Error deleting scheduled command", error);
return { success: false };
}
},
createRecurringCommand: async (recurringCommandsUrl: string, command: Partial<RecurringCommand>) => {
try {
const response = await fetch(`${recurringCommandsUrl}/recurrings`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
body: JSON.stringify(command),
});
if (!response.ok) {
console.error(`Error creating recurring command: ${response.status} ${response.statusText}`);
throw new Error("Failed to create recurring command");
}
if (response.status === 204) {
// Return the command object we sent since the server doesn't return data
return command as RecurringCommand;
}
const json = await response.json();
return json as RecurringCommand;
} catch (error) {
console.error("Error creating recurring command", error);
throw error;
}
},
updateRecurringCommand: async (recurringCommandsUrl: string, command: RecurringCommand) => {
try {
const response = await fetch(`${recurringCommandsUrl}/recurrings`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
body: JSON.stringify(command),
});
if (!response.ok) {
console.error(`Error updating recurring command: ${response.status} ${response.statusText}`);
throw new Error("Failed to update recurring command");
}
if (response.status === 204) {
// Return the command object we sent since the server doesn't return data
return command as RecurringCommand;
}
const json = await response.json();
return json as RecurringCommand;
} catch (error) {
console.error("Error updating recurring command", error);
throw error;
}
},
deleteRecurringCommand: async (recurringCommandsUrl: string, id: string) => {
try {
const response = await fetch(`${recurringCommandsUrl}/recurrings/${id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
},
});
if (!response.ok) {
console.error(`Error deleting recurring command: ${response.status} ${response.statusText}`);
return { success: false };
}
return { success: true };
} catch (error) {
console.error("Error deleting recurring command", error);
return { success: false };
}
},
}; };
const dataProvider = withLifecycleCallbacks(baseDataProvider, [ const dataProvider = withLifecycleCallbacks(baseDataProvider, [

View File

@@ -1,5 +1,6 @@
export interface Config { export interface Config {
restrictBaseUrl: string | string[]; restrictBaseUrl: string | string[];
corsCredentials: string;
asManagedUsers: RegExp[]; asManagedUsers: RegExp[];
menu: MenuItem[]; menu: MenuItem[];
etkeccAdmin?: string; etkeccAdmin?: string;
@@ -16,17 +17,18 @@ export const WellKnownKey = "cc.etke.synapse-admin";
// current configuration // current configuration
let config: Config = { let config: Config = {
restrictBaseUrl: "", restrictBaseUrl: "",
corsCredentials: "same-origin",
asManagedUsers: [], asManagedUsers: [],
menu: [], menu: [],
etkeccAdmin: "" etkeccAdmin: "",
}; };
export const FetchConfig = async () => { export const FetchConfig = async () => {
// load config.json and honor vite base url (import.meta.env.BASE_URL) // load config.json and honor vite base url (import.meta.env.BASE_URL)
// if that url doesn't have a trailing slash - add it // if that url doesn't have a trailing slash - add it
let configJSONUrl = "config.json" let configJSONUrl = "config.json";
if (import.meta.env.BASE_URL) { if (import.meta.env.BASE_URL) {
configJSONUrl = `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}config.json`; configJSONUrl = `${import.meta.env.BASE_URL.replace(/\/?$/, "/")}config.json`;
} }
try { try {
const resp = await fetch(configJSONUrl); const resp = await fetch(configJSONUrl);
@@ -50,7 +52,10 @@ export const FetchConfig = async () => {
const resp = await fetch(`${protocol}://${homeserver}/.well-known/matrix/client`); const resp = await fetch(`${protocol}://${homeserver}/.well-known/matrix/client`);
const configWK = await resp.json(); const configWK = await resp.json();
if (!configWK[WellKnownKey]) { if (!configWK[WellKnownKey]) {
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK); console.log(
`Loaded ${protocol}://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`,
configWK
);
} else { } else {
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client`, configWK); console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client`, configWK);
LoadConfig(configWK[WellKnownKey]); LoadConfig(configWK[WellKnownKey]);
@@ -59,7 +64,7 @@ export const FetchConfig = async () => {
console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e); console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e);
} }
} }
} };
// load config from context // load config from context
// we deliberately processing each key separately to avoid overwriting the whole config, loosing some keys, and messing // we deliberately processing each key separately to avoid overwriting the whole config, loosing some keys, and messing
@@ -69,6 +74,10 @@ export const LoadConfig = (context: any) => {
config.restrictBaseUrl = context.restrictBaseUrl as string | string[]; config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
} }
if (context?.corsCredentials) {
config.corsCredentials = context.corsCredentials;
}
if (context?.asManagedUsers) { if (context?.asManagedUsers) {
config.asManagedUsers = context.asManagedUsers.map((regex: string) => new RegExp(regex)); config.asManagedUsers = context.asManagedUsers.map((regex: string) => new RegExp(regex));
} }
@@ -84,13 +93,12 @@ export const LoadConfig = (context: any) => {
if (context?.etkeccAdmin) { if (context?.etkeccAdmin) {
config.etkeccAdmin = context.etkeccAdmin; config.etkeccAdmin = context.etkeccAdmin;
} }
} };
// get config // get config
export const GetConfig = (): Config => { export const GetConfig = (): Config => {
return config; return config;
} };
// clear config // clear config
export const ClearConfig = () => { export const ClearConfig = () => {
@@ -98,4 +106,4 @@ export const ClearConfig = () => {
config = {} as Config; config = {} as Config;
// session // session
localStorage.clear(); localStorage.clear();
} };

View File

@@ -51,4 +51,4 @@ export const getTimeSince = (dateToCompare: string) => {
if (diffInMinutes < 30 * 24 * 60) return `${Math.floor(diffInMinutes / (7 * 24 * 60))} weeks`; if (diffInMinutes < 30 * 24 * 60) return `${Math.floor(diffInMinutes / (7 * 24 * 60))} weeks`;
if (diffInMinutes < 60 * 24 * 60) return "1 month"; if (diffInMinutes < 60 * 24 * 60) return "1 month";
return `${Math.floor(diffInMinutes / (30 * 24 * 60))} months`; return `${Math.floor(diffInMinutes / (30 * 24 * 60))} months`;
}; };

View File

@@ -10,6 +10,6 @@ const decodeURLComponent = (str: any): any => {
} catch (e) { } catch (e) {
return str; return str;
} }
} };
export default decodeURLComponent; export default decodeURLComponent;

View File

@@ -1,6 +1,6 @@
export type MatrixError = { export interface MatrixError {
errcode: string; errcode: string;
error: string; error: string;
} }
export const displayError = (errcode: string, status: number, message: string) => `${errcode} (${status}): ${message}`; export const displayError = (errcode: string, status: number, message: string) => `${errcode} (${status}): ${message}`;

View File

@@ -1,12 +1,12 @@
export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string, mediaId: string } => { export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string; mediaId: string } => {
const re = /^mxc:\/\/([^/]+)\/(\w+)/; const re = /^mxc:\/\/([^/]+)\/([\w-]+)$/;
const ret = re.exec(mxcUrl); const ret = re.exec(mxcUrl);
if (ret == null) { if (ret == null) {
throw new Error("Invalid mxcUrl"); throw new Error("Invalid mxcUrl");
} }
const serverName = ret[1]; const serverName = ret[1];
const mediaId = ret[2]; const mediaId = ret[2];
return { serverName, mediaId }; return { serverName, mediaId };
}; };
export type MediaType = "thumbnail" | "original"; export type MediaType = "thumbnail" | "original";

View File

@@ -1,13 +1,13 @@
import AnnouncementIcon from '@mui/icons-material/Announcement'; import AnnouncementIcon from "@mui/icons-material/Announcement";
import EngineeringIcon from '@mui/icons-material/Engineering'; import EngineeringIcon from "@mui/icons-material/Engineering";
import HelpCenterIcon from '@mui/icons-material/HelpCenter'; import HelpCenterIcon from "@mui/icons-material/HelpCenter";
import SupportAgentIcon from '@mui/icons-material/SupportAgent'; import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import PieChartIcon from "@mui/icons-material/PieChart";
import PieChartIcon from '@mui/icons-material/PieChart'; import PriceCheckIcon from "@mui/icons-material/PriceCheck";
import UpgradeIcon from '@mui/icons-material/Upgrade'; import RestartAltIcon from "@mui/icons-material/RestartAlt";
import RouterIcon from '@mui/icons-material/Router'; import RouterIcon from "@mui/icons-material/Router";
import PriceCheckIcon from '@mui/icons-material/PriceCheck'; import SupportAgentIcon from "@mui/icons-material/SupportAgent";
import RestartAltIcon from '@mui/icons-material/RestartAlt'; import UpgradeIcon from "@mui/icons-material/Upgrade";
export const Icons = { export const Icons = {
Announcement: AnnouncementIcon, Announcement: AnnouncementIcon,

View File

@@ -1,4 +1,5 @@
import { Identifier } from "ra-core"; import { Identifier } from "ra-core";
import { GetConfig } from "../utils/config"; import { GetConfig } from "../utils/config";
const mxidPattern = /^@[^@:]+:[^@:]+$/; const mxidPattern = /^@[^@:]+:[^@:]+$/;
@@ -56,6 +57,6 @@ export function returnMXID(input: string | Identifier): string {
} }
// If input is not a valid MXID, assume it's a localpart and construct the MXID // If input is not a valid MXID, assume it's a localpart and construct the MXID
const localpart = typeof input === 'string' && inputStr.startsWith('@') ? inputStr.slice(1) : inputStr; const localpart = typeof input === "string" && inputStr.startsWith("@") ? inputStr.slice(1) : inputStr;
return `@${localpart}:${homeserver}`; return `@${localpart}:${homeserver}`;
} }

View File

@@ -1,7 +1,6 @@
import { vitePluginVersionMark } from "vite-plugin-version-mark";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { vitePluginVersionMark } from "vite-plugin-version-mark";
export default defineConfig({ export default defineConfig({
base: "./", base: "./",
@@ -16,7 +15,7 @@ export default defineConfig({
ifMeta: false, ifMeta: false,
ifLog: false, ifLog: false,
ifGlobal: true, ifGlobal: true,
outputFile: (version) => ({ outputFile: version => ({
path: "manifest.json", path: "manifest.json",
content: JSON.stringify({ content: JSON.stringify({
name: "Synapse Admin", name: "Synapse Admin",
@@ -24,20 +23,23 @@ export default defineConfig({
description: "Synapse Admin is an admin console for synapse Matrix homeserver with additional features.", description: "Synapse Admin is an admin console for synapse Matrix homeserver with additional features.",
categories: ["productivity", "utilities"], categories: ["productivity", "utilities"],
orientation: "landscape", orientation: "landscape",
icons: [{ icons: [
src: "favicon.ico", {
sizes: "32x32", src: "favicon.ico",
type: "image/x-icon" sizes: "32x32",
},{ type: "image/x-icon",
src: "images/logo.webp", },
sizes: "512x512", {
type: "image/webp", src: "images/logo.webp",
purpose: "any maskable" sizes: "512x512",
}], type: "image/webp",
purpose: "any maskable",
},
],
start_url: ".", start_url: ".",
display: "standalone", display: "standalone",
theme_color: "#000000", theme_color: "#000000",
background_color: "#ffffff" background_color: "#ffffff",
}), }),
}), }),
}), }),

1696
yarn.lock

File diff suppressed because it is too large Load Diff