Compare commits
38 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
687e3364b0 | ||
![]() |
149f5737dd | ||
![]() |
c2867e44c6 | ||
![]() |
f58562cd5b | ||
![]() |
92845a8640 | ||
![]() |
4fd18007de | ||
![]() |
005be8eec5 | ||
![]() |
402457640c | ||
![]() |
c3dd0facd2 | ||
![]() |
2fa5600d24 | ||
![]() |
c35416ec13 | ||
![]() |
9e6e31a728 | ||
![]() |
d7677af085 | ||
![]() |
dc0ba15828 | ||
![]() |
6527565a18 | ||
![]() |
c0a4004fd3 | ||
![]() |
97384d1ebb | ||
![]() |
41768eb157 | ||
![]() |
0c89493b58 | ||
![]() |
d5d1be794d | ||
![]() |
b2dc623448 | ||
![]() |
fa90a82745 | ||
![]() |
b55532e93e | ||
![]() |
880270bfe6 | ||
![]() |
7c21692a1e | ||
![]() |
3b69e78bb8 | ||
![]() |
a6caa0714e | ||
![]() |
83473dd3fa | ||
![]() |
aad90760a5 | ||
![]() |
cb853bf342 | ||
![]() |
8d45ae2886 | ||
![]() |
2e151cf198 | ||
![]() |
52c17bc5ca | ||
![]() |
d79d825ab4 | ||
![]() |
28b80d7fa2 | ||
![]() |
9735ffbc19 | ||
![]() |
f4d0e19d21 | ||
![]() |
14d1c904c0 |
29
README.md
29
README.md
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
* [Fork differences](#fork-differences)
|
* [Fork differences](#fork-differences)
|
||||||
* [Changes](#changes)
|
* [Changes](#changes)
|
||||||
|
* [exclusive for etke.cc customers](#exclusive-for-etkecc-customers)
|
||||||
* [Development](#development)
|
* [Development](#development)
|
||||||
* [Support](#support)
|
* [Support](#support)
|
||||||
* [Configuration](#configuration)
|
* [Configuration](#configuration)
|
||||||
@@ -58,6 +59,8 @@ The full list is described below in the [Changes](#changes) section.
|
|||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
The following changes are already implemented:
|
The following changes are already implemented:
|
||||||
|
|
||||||
* 🛑 [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
* 🛑 [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
||||||
@@ -97,11 +100,20 @@ The following changes are already implemented:
|
|||||||
* 🛑 [Prevent accidental user overwrites](https://github.com/etkecc/synapse-admin/pull/139)
|
* 🛑 [Prevent accidental user overwrites](https://github.com/etkecc/synapse-admin/pull/139)
|
||||||
* 🔍 [Allow providing login form details via GET params](https://github.com/etkecc/synapse-admin/pull/140)
|
* 🔍 [Allow providing login form details via GET params](https://github.com/etkecc/synapse-admin/pull/140)
|
||||||
* 🎨 [Add preferred theme colors to login page and footer](https://github.com/etkecc/synapse-admin/pull/155)
|
* 🎨 [Add preferred theme colors to login page and footer](https://github.com/etkecc/synapse-admin/pull/155)
|
||||||
* 🔰 [Add "Assign Admin" button to the rooms](https://github.com/etkecc/synapse-admin/pull/156)
|
* 🛡️ [Add "Assign Admin" button to the rooms](https://github.com/etkecc/synapse-admin/pull/156)
|
||||||
* 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158)
|
* 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158)
|
||||||
* 🤖 [User Badges](https://github.com/etkecc/synapse-admin/pull/160)
|
* 🏷️ [User Badges](https://github.com/etkecc/synapse-admin/pull/160)
|
||||||
|
* 🔄 [Allow prefilling any fields on the login form via GET params](https://github.com/etkecc/synapse-admin/pull/181)
|
||||||
|
* 🎞️ [Add "Media" tab for rooms](https://github.com/etkecc/synapse-admin/pull/196)
|
||||||
|
* 📞 [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)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
#### exclusive for [etke.cc](https://etke.cc) customers
|
||||||
|
|
||||||
|
We at [etke.cc](https://etke.cc) attempting to develop everything open-source, but some things are too specific to be used by anyone else.
|
||||||
|
The following list contains such features - they are only available for [etke.cc](https://etke.cc) customers.
|
||||||
|
|
||||||
|
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
@@ -111,7 +123,8 @@ _the list will be updated as new changes are added_
|
|||||||
This command initializes the development environment (local Synapse server and Postgres DB),
|
This command initializes the development environment (local Synapse server and Postgres DB),
|
||||||
and launches the app in a dev mode at `http://localhost:5173`
|
and launches the app in a dev mode at `http://localhost:5173`
|
||||||
|
|
||||||
After that open `http://localhost:5173` in your browser, login using the following credentials:
|
After that open [http://localhost:5173](http://localhost:5173?username=admin&password=admin&server=http://localhost:8008) in your browser,
|
||||||
|
login using the following credentials:
|
||||||
|
|
||||||
* Login: admin
|
* Login: admin
|
||||||
* Password: admin
|
* Password: admin
|
||||||
@@ -148,13 +161,9 @@ services:
|
|||||||
|
|
||||||
### Prefilling login form
|
### Prefilling login form
|
||||||
|
|
||||||
You can prefill `username` and `homeserver` fields on the login page using GET parameters, example:
|
You can prefill all fields on the login page using GET parameters.
|
||||||
|
|
||||||
```
|
[Documentation](./docs/prefill-login-form.md)
|
||||||
https://matrix.example.com/synapse-admin/?username=admin&server=matrix.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
That way `username` and `homeserver` fields will be pre-filled with `admin` and `https://matrix.example.com` respectively.
|
|
||||||
|
|
||||||
|
|
||||||
### Restricting available homeserver
|
### Restricting available homeserver
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
synapse:
|
synapse:
|
||||||
image: ghcr.io/element-hq/synapse:latest
|
image: ghcr.io/element-hq/synapse:develop
|
||||||
entrypoint: python
|
entrypoint: python
|
||||||
command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
|
command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
|
||||||
ports:
|
ports:
|
||||||
|
39
docs/README.md
Normal file
39
docs/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
Synapse Admin documentation is under construction right now, so PRs are greatly appreciated!
|
||||||
|
|
||||||
|
Table of contents:
|
||||||
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [Features](#features)
|
||||||
|
* [Deployment](#deployment)
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc -->
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
[Full configuration documentation](./config.md)
|
||||||
|
|
||||||
|
Specific configuration options:
|
||||||
|
|
||||||
|
* [Restricting available homeserver](./restrict-hs.md)
|
||||||
|
* [System / Appservice-managed Users](./system-users.md)
|
||||||
|
* [Custom Menu Items](./custom-menu.md)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* [User Badges](./user-badges.md)
|
||||||
|
* [Prefilling the Login Form](./prefill-login-form.md)
|
||||||
|
|
||||||
|
for [etke.cc](https://etke.cc) customers only:
|
||||||
|
|
||||||
|
> **Note:** The following features are only available for etke.cc customers. Due to specifics of the implementation,
|
||||||
|
they are not available for any other Synapse Admin deployments.
|
||||||
|
|
||||||
|
* [Server Status icon](../src/components/etke.cc/README.md#server-status-icon)
|
||||||
|
* [Server Status page](../src/components/etke.cc/README.md#server-status-page)
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
* [Serving Synapse Admin behind a reverse proxy](./reverse-proxy.md)
|
48
docs/prefill-login-form.md
Normal file
48
docs/prefill-login-form.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Prefilling the Login Form
|
||||||
|
|
||||||
|
In some cases you may wish to prefill/preset the login form fields when sharing a link to a Synapse Admin instance.
|
||||||
|
This can be done by adding the following query parameters to the URL:
|
||||||
|
|
||||||
|
* `username` - The username to prefill in the username field.
|
||||||
|
* `server` - The server to prefill in the homeserver url field.
|
||||||
|
|
||||||
|
The following query params will work only if the Synapse Admin is loaded from `localhost` or `127.0.0.1`:
|
||||||
|
|
||||||
|
* `password` - The password to prefill in the password field (credentials auth). **NEVER** use this in production.
|
||||||
|
* `accessToken` - The access token to prefill in the access token field (access token auth). **NEVER** use this in production.
|
||||||
|
|
||||||
|
> **WARNING**: Never use the `password` or `accessToken` query parameters in production as they can be easily extracted
|
||||||
|
from the URL. These are only meant for development purposes and local environments.
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
https://admin.etke.cc?username=admin&server=https://matrix.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This will open `Credentials` (username/password) login form with the username field prefilled with `admin` and the
|
||||||
|
Homeserver URL field prefilled with `https://matrix.example.com`.
|
||||||
|
|
||||||
|
### Development and Local environments
|
||||||
|
|
||||||
|
**With Password**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
http://localhost:8080?username=admin&server=https://matrix.example.com&password=secret
|
||||||
|
```
|
||||||
|
|
||||||
|
This will open `Credentials` (username/password) login form with the username field prefilled with `admin`, the
|
||||||
|
Homeserver URL field prefilled with `https://matrix.example.com` and the password field prefilled with `secret`.
|
||||||
|
|
||||||
|
|
||||||
|
**With Access Token**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
http://localhost:8080?server=https://matrix.example.com&accessToken=secret
|
||||||
|
```
|
||||||
|
|
||||||
|
This will open `Access Token` login form with the Homeserver URL field prefilled with `https://matrix.example.com` and
|
||||||
|
the access token field prefilled with `secret`.
|
34
package.json
34
package.json
@@ -14,17 +14,17 @@
|
|||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.13.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.0.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.13",
|
||||||
"@types/node": "^22.9.3",
|
"@types/node": "^22.10.1",
|
||||||
"@types/papaparse": "^5.3.15",
|
"@types/papaparse": "^5.3.15",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"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",
|
||||||
@@ -34,38 +34,38 @@
|
|||||||
"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.3.3",
|
"prettier": "^3.4.2",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.3.1",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.15.0",
|
"typescript-eslint": "^8.17.0",
|
||||||
"vite": "^5.4.11",
|
"vite": "^6.0.3",
|
||||||
"vite-plugin-version-mark": "^0.1.2"
|
"vite-plugin-version-mark": "^0.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.5",
|
"@emotion/react": "^11.13.5",
|
||||||
"@emotion/styled": "^11.13.5",
|
"@emotion/styled": "^11.13.5",
|
||||||
"@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.1.8",
|
"@mui/icons-material": "^6.1.9",
|
||||||
"@mui/material": "^6.1.8",
|
"@mui/material": "^6.1.10",
|
||||||
"@mui/utils": "^5.16.6",
|
"@mui/utils": "^5.16.8",
|
||||||
"@tanstack/react-query": "^5.61.3",
|
"@tanstack/react-query": "^5.62.3",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"ra-core": "^5.3.4",
|
"ra-core": "^5.4.1",
|
||||||
"ra-i18n-polyglot": "^5.3.4",
|
"ra-i18n-polyglot": "^5.3.4",
|
||||||
"ra-language-english": "^5.3.4",
|
"ra-language-english": "^5.3.4",
|
||||||
"ra-language-farsi": "^5.0.0",
|
"ra-language-farsi": "^5.1.0",
|
||||||
"ra-language-french": "^5.4.0",
|
"ra-language-french": "^5.4.1",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"ra-language-russian": "^4.14.2",
|
"ra-language-russian": "^4.14.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-admin": "^5.4.0",
|
"react-admin": "^5.4.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.54.0",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-router": "^6.26.2",
|
"react-router": "^6.26.2",
|
||||||
"react-router-dom": "^6.28.0"
|
"react-router-dom": "^6.28.0"
|
||||||
|
BIN
screenshots/etke.cc/server-status/indicator.webp
Normal file
BIN
screenshots/etke.cc/server-status/indicator.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
screenshots/etke.cc/server-status/page.webp
Normal file
BIN
screenshots/etke.cc/server-status/page.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
@@ -2,7 +2,7 @@ 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 { createContext, useContext } from "react";
|
|
||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
|
|
||||||
import AdminLayout from "./components/AdminLayout";
|
import AdminLayout from "./components/AdminLayout";
|
||||||
@@ -24,7 +24,7 @@ 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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Config } from "./utils/config";
|
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@@ -64,6 +64,7 @@ export const App = () => (
|
|||||||
>
|
>
|
||||||
<CustomRoutes>
|
<CustomRoutes>
|
||||||
<Route path="/import_users" element={<UserImport />} />
|
<Route path="/import_users" element={<UserImport />} />
|
||||||
|
<Route path="/server_status" element={<ServerStatusPage />} />
|
||||||
</CustomRoutes>
|
</CustomRoutes>
|
||||||
<Resource {...users} />
|
<Resource {...users} />
|
||||||
<Resource {...rooms} />
|
<Resource {...rooms} />
|
||||||
@@ -86,8 +87,4 @@ export const App = () => (
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AppContext = createContext({});
|
|
||||||
|
|
||||||
export const useAppContext = () => useContext(AppContext) as Config;
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
7
src/Context.tsx
Normal file
7
src/Context.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
import { Config } from "./utils/config";
|
||||||
|
|
||||||
|
export const AppContext = createContext<Config>({} as Config);
|
||||||
|
|
||||||
|
export const useAppContext = () => useContext(AppContext) as Config;
|
@@ -4,6 +4,7 @@ import { useEffect, useState, Suspense } from "react";
|
|||||||
import { Icons, DefaultIcon } from "../utils/icons";
|
import { Icons, DefaultIcon } from "../utils/icons";
|
||||||
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
|
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
||||||
|
|
||||||
const AdminUserMenu = () => {
|
const AdminUserMenu = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -48,6 +49,7 @@ const AdminUserMenu = () => {
|
|||||||
const AdminAppBar = () => {
|
const AdminAppBar = () => {
|
||||||
return (<AppBar userMenu={<AdminUserMenu />}>
|
return (<AppBar userMenu={<AdminUserMenu />}>
|
||||||
<TitlePortal />
|
<TitlePortal />
|
||||||
|
<ServerStatusBadge />
|
||||||
<InspectorButton />
|
<InspectorButton />
|
||||||
</AppBar>);
|
</AppBar>);
|
||||||
};
|
};
|
||||||
@@ -59,7 +61,7 @@ const AdminMenu = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Menu {...props}>
|
<Menu {...props}>
|
||||||
<Menu.ResourceItems />
|
<Menu.ResourceItems />
|
||||||
{menu.map((item, index) => {
|
{menu && menu.map((item, index) => {
|
||||||
const { url, icon, label } = item;
|
const { url, icon, label } = item;
|
||||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||||
|
|
||||||
|
30
src/components/etke.cc/README.md
Normal file
30
src/components/etke.cc/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# etke.cc-specific components
|
||||||
|
|
||||||
|
This directory contains [etke.cc](https://etke.cc)-specific components, unusable for any other purposes and/or configuration.
|
||||||
|
|
||||||
|
We at [etke.cc](https://etke.cc) attempting to develop everything open-source, but some things are too specific to be used by anyone else. This directory contains such components - they are only available for [etke.cc](https://etke.cc) customers.
|
||||||
|
|
||||||
|
Due to the specifics mentioned above, these components are documented here rather than in the [docs](../../../docs/README.md), plus they are not supported as part of the Synapse Admin open-source project (i.e.: no issues, no PRs, no support, no requests, etc.).
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Server Status icon
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the application bar the new monitoring icon is displayed that shows the current server status, and has the following color dot (and tooltip indicators):
|
||||||
|
|
||||||
|
* 🟢 (green) - the server is up and running, everything is fine, no issues detected
|
||||||
|
* 🟡 (yellow) - the server is up and running, but there is a command in progress (likely [maintenance](https://etke.cc/help/extras/scheduler/#maintenance)), so some temporary issues may occur - that's totally fine
|
||||||
|
* 🔴 (red) - there is at least 1 issue with one of the server's components
|
||||||
|
|
||||||
|
### Server Status page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When you click on the [Server Status icon](#server-status-icon) in the application bar, you will be redirected to the
|
||||||
|
Server Status page. This page contains the following information:
|
||||||
|
|
||||||
|
* Overall server status (up/updating/has issues)
|
||||||
|
* Details about the currently running command (if any)
|
||||||
|
* Details about the server's components statuses (up/down with error details and suggested actions) by categories
|
169
src/components/etke.cc/ServerStatusBadge.tsx
Normal file
169
src/components/etke.cc/ServerStatusBadge.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { Avatar, Badge, 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 { useNavigate } from "react-router";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { ServerProcessResponse, ServerStatusResponse } from "../../synapse/dataProvider";
|
||||||
|
|
||||||
|
interface StyledBadgeProps extends BadgeProps {
|
||||||
|
backgroundColor: string;
|
||||||
|
badgeColor: string
|
||||||
|
theme?: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledBadge = styled(Badge, { shouldForwardProp: (prop) => !['badgeColor', 'backgroundColor'].includes(prop as string) })<StyledBadgeProps>
|
||||||
|
(({ theme, backgroundColor, badgeColor }) => ({
|
||||||
|
'& .MuiBadge-badge': {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
color: badgeColor,
|
||||||
|
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
|
||||||
|
'&::after': {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'ripple 2.5s infinite ease-in-out',
|
||||||
|
border: '1px solid currentColor',
|
||||||
|
content: '""',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'@keyframes ripple': {
|
||||||
|
'0%': {
|
||||||
|
transform: 'scale(.8)',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'scale(2.4)',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// every 5 minutes
|
||||||
|
const SERVER_STATUS_INTERVAL_TIME = 5 * 60 * 1000;
|
||||||
|
// every 5 seconds
|
||||||
|
const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000;
|
||||||
|
|
||||||
|
const useServerStatus = () => {
|
||||||
|
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { ok: false, success: false, host: "", results: [] });
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const isOkay = serverStatus.ok;
|
||||||
|
const successCheck = serverStatus.success;
|
||||||
|
|
||||||
|
const checkServerStatus = async () => {
|
||||||
|
const serverStatus: ServerStatusResponse = await dataProvider.getServerStatus(etkeccAdmin);
|
||||||
|
setServerStatus({
|
||||||
|
ok: serverStatus.ok,
|
||||||
|
success: serverStatus.success,
|
||||||
|
host: serverStatus.host,
|
||||||
|
results: serverStatus.results,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let serverStatusInterval: NodeJS.Timeout;
|
||||||
|
if (etkeccAdmin) {
|
||||||
|
checkServerStatus();
|
||||||
|
setTimeout(() => {
|
||||||
|
// start the interval after 10 seconds to avoid too many requests
|
||||||
|
serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME);
|
||||||
|
}, 10000);
|
||||||
|
} else {
|
||||||
|
setServerStatus({ ok: false, success: false, host: "", results: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (serverStatusInterval) {
|
||||||
|
clearInterval(serverStatusInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [etkeccAdmin]);
|
||||||
|
|
||||||
|
return { isOkay, successCheck };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useCurrentServerProcess = () => {
|
||||||
|
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const { command, locked_at } = serverProcess;
|
||||||
|
|
||||||
|
const checkServerRunningProcess = async () => {
|
||||||
|
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin);
|
||||||
|
setServerProcess({
|
||||||
|
...serverProcess,
|
||||||
|
command: serverProcess.command,
|
||||||
|
locked_at: serverProcess.locked_at
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let serverCheckInterval: NodeJS.Timeout;
|
||||||
|
if (etkeccAdmin) {
|
||||||
|
checkServerRunningProcess();
|
||||||
|
setTimeout(() => {
|
||||||
|
serverCheckInterval = setInterval(checkServerRunningProcess, SERVER_CURRENT_PROCCESS_INTERVAL_TIME);
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
setServerProcess({ command: "", locked_at: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (serverCheckInterval) {
|
||||||
|
clearInterval(serverCheckInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [etkeccAdmin]);
|
||||||
|
|
||||||
|
return { command, locked_at };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerStatusBadge = () => {
|
||||||
|
const { isOkay, successCheck } = useServerStatus();
|
||||||
|
const { command, locked_at } = useCurrentServerProcess();
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (!successCheck) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleServerStatusClick = () => {
|
||||||
|
navigate("/server_status");
|
||||||
|
};
|
||||||
|
|
||||||
|
let tooltipText = "Click to view Server Status";
|
||||||
|
let badgeBackgroundColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
||||||
|
let badgeColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
||||||
|
|
||||||
|
if (command && locked_at) {
|
||||||
|
badgeBackgroundColor = theme.palette.warning.main;
|
||||||
|
badgeColor = theme.palette.warning.main;
|
||||||
|
tooltipText = `Running: ${command}; ${tooltipText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 }}}>
|
||||||
|
<Tooltip title={tooltipText} sx={{ cursor: "pointer" }}>
|
||||||
|
<StyledBadge
|
||||||
|
overlap="circular"
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
|
variant="dot"
|
||||||
|
backgroundColor={badgeBackgroundColor}
|
||||||
|
badgeColor={badgeColor}
|
||||||
|
>
|
||||||
|
<Avatar sx={{ height: 24, width: 24, background: theme.palette.mode === "dark" ? theme.palette.background.default : "#2196f3" }}>
|
||||||
|
<MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} />
|
||||||
|
</Avatar>
|
||||||
|
</StyledBadge>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerStatusBadge;
|
144
src/components/etke.cc/ServerStatusPage.tsx
Normal file
144
src/components/etke.cc/ServerStatusPage.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { useStore } from "ra-core";
|
||||||
|
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 EngineeringIcon from '@mui/icons-material/Engineering';
|
||||||
|
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
|
||||||
|
|
||||||
|
const getTimeSince = (date: string) => {
|
||||||
|
const now = new Date();
|
||||||
|
const past = new Date(date);
|
||||||
|
const diffInMinutes = Math.floor((now.getTime() - past.getTime()) / (1000 * 60));
|
||||||
|
|
||||||
|
if (diffInMinutes < 1) return "a couple of seconds";
|
||||||
|
if (diffInMinutes === 1) return "1 minute";
|
||||||
|
return `${diffInMinutes} minutes`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusChip = ({ isOkay, size = "medium", command }: { isOkay: boolean, size?: "small" | "medium", command?: string }) => {
|
||||||
|
let label = "OK";
|
||||||
|
let icon = <CheckIcon />;
|
||||||
|
let color: ChipProps["color"] = "success";
|
||||||
|
if (!isOkay) {
|
||||||
|
label = "Error";
|
||||||
|
icon = <CloseIcon />;
|
||||||
|
color = "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command) {
|
||||||
|
label = command;
|
||||||
|
color = "warning";
|
||||||
|
icon = <EngineeringIcon />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip icon={icon} label={label} color={color} variant="outlined" size={size} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerComponentText = ({ text }: { text: string }) => {
|
||||||
|
return <Typography variant="body1" dangerouslySetInnerHTML={{ __html: text }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerStatusPage = () => {
|
||||||
|
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
||||||
|
ok: false,
|
||||||
|
success: false,
|
||||||
|
host: "",
|
||||||
|
results: [],
|
||||||
|
});
|
||||||
|
const [ serverProcess, setServerProcess ] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
|
||||||
|
const { command, locked_at } = serverProcess;
|
||||||
|
const successCheck = serverStatus.success;
|
||||||
|
const isOkay = serverStatus.ok;
|
||||||
|
const host = serverStatus.host;
|
||||||
|
const results = serverStatus.results;
|
||||||
|
|
||||||
|
let groupedResults: Record<string, ServerStatusComponent[]> = {};
|
||||||
|
for (const result of results) {
|
||||||
|
if (!groupedResults[result.category]) {
|
||||||
|
groupedResults[result.category] = [];
|
||||||
|
}
|
||||||
|
groupedResults[result.category].push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!successCheck) {
|
||||||
|
return (
|
||||||
|
<Paper elevation={3} sx={{ p: 3, mt: 3 }}>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
|
<CloseIcon color="error" />
|
||||||
|
<Typography color="error">
|
||||||
|
Unable to fetch server status. Please try again later.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
<Stack spacing={1} direction="row" alignItems="center">
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<Typography variant="h4">Status:</Typography>
|
||||||
|
<StatusChip isOkay={isOkay} command={command} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h5" color="primary" fontWeight="medium">
|
||||||
|
{host}
|
||||||
|
</Typography>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack spacing={2} direction="row">
|
||||||
|
{Object.keys(groupedResults).map((category, idx) => (
|
||||||
|
<Box key={`category_${category}`} sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="h5" mb={1}>
|
||||||
|
{category}
|
||||||
|
</Typography>
|
||||||
|
<Paper elevation={2} sx={{ p: 3 }}>
|
||||||
|
<Stack spacing={1} divider={<Divider />}>
|
||||||
|
{groupedResults[category].map((result, idx) => (
|
||||||
|
<Box key={`${category}_${idx}`}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<StatusChip isOkay={result.ok} size="small" />
|
||||||
|
{result.label.url ? (
|
||||||
|
<Link href={result.label.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<ServerComponentText text={result.label.text} />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<ServerComponentText text={result.label.text} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{result.reason && <Typography color="text.secondary" dangerouslySetInnerHTML={{ __html: result.reason }}/>}
|
||||||
|
{(!result.ok && result.help) && (
|
||||||
|
<Link href={result.help} target="_blank" rel="noopener noreferrer" sx={{ mt: 1 }}>
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerStatusPage;
|
@@ -314,6 +314,7 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
|
|||||||
export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const notify = useNotify();
|
||||||
const isImage = mimetype && mimetype.startsWith("image/");
|
const isImage = mimetype && mimetype.startsWith("image/");
|
||||||
|
|
||||||
const openFileInNewTab = (blobURL: string) => {
|
const openFileInNewTab = (blobURL: string) => {
|
||||||
@@ -340,12 +341,25 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
|||||||
const handleFile = async (preview: boolean) => {
|
const handleFile = async (preview: boolean) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetchAuthenticatedMedia(mxcURL, "original");
|
const response = await fetchAuthenticatedMedia(mxcURL, "original");
|
||||||
const blob = await response.blob();
|
|
||||||
const blobURL = URL.createObjectURL(blob);
|
if (response.ok) {
|
||||||
if (preview) {
|
const blob = await response.blob();
|
||||||
openFileInNewTab(blobURL);
|
const blobURL = URL.createObjectURL(blob);
|
||||||
|
if (preview) {
|
||||||
|
openFileInNewTab(blobURL);
|
||||||
|
} else {
|
||||||
|
downloadFile(blobURL);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
downloadFile(blobURL);
|
const body = await response.json();
|
||||||
|
notify("resources.room_media.action.error", {
|
||||||
|
messageArgs: {
|
||||||
|
errcode: body.errcode,
|
||||||
|
errstatus: response.status,
|
||||||
|
error: body.error,
|
||||||
|
},
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -391,8 +405,16 @@ export const MediaIDField = ({ source }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
|
let uploadName = mediaID;
|
||||||
const mxcURL = `mxc://${homeserver}/${mediaID}`;
|
if (get(record, "upload_name")) {
|
||||||
|
uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mxcURL = mediaID;
|
||||||
|
if (!mediaID.startsWith(`mxc://${homeserver}`)) {
|
||||||
|
// this is user's media, where mediaID doesn't have the mxc://home_server/ prefix as it has in the rooms
|
||||||
|
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}/>;
|
||||||
};
|
};
|
||||||
|
@@ -64,6 +64,7 @@ const de: SynapseTranslationMessages = {
|
|||||||
members: "Mitglieder",
|
members: "Mitglieder",
|
||||||
detail: "Details",
|
detail: "Details",
|
||||||
permission: "Berechtigungen",
|
permission: "Berechtigungen",
|
||||||
|
media: "Medien",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Allgemein", detail: "Details" } },
|
reports: { tabs: { basic: "Allgemein", detail: "Details" } },
|
||||||
@@ -159,11 +160,13 @@ const de: SynapseTranslationMessages = {
|
|||||||
is_guest: "Gast",
|
is_guest: "Gast",
|
||||||
admin: "Server Administrator",
|
admin: "Server Administrator",
|
||||||
locked: "Gesperrt",
|
locked: "Gesperrt",
|
||||||
|
suspended: "Suspendiert",
|
||||||
deactivated: "Deaktiviert",
|
deactivated: "Deaktiviert",
|
||||||
erased: "Gelöscht",
|
erased: "Gelöscht",
|
||||||
guests: "Zeige Gäste",
|
guests: "Zeige Gäste",
|
||||||
show_deactivated: "Zeige deaktivierte Benutzer",
|
show_deactivated: "Zeige deaktivierte Benutzer",
|
||||||
show_locked: "Zeige gesperrte Benutzer",
|
show_locked: "Zeige gesperrte Benutzer",
|
||||||
|
show_suspended: "Zeige suspendierte Benutzer",
|
||||||
user_id: "Suche Benutzer",
|
user_id: "Suche Benutzer",
|
||||||
displayname: "Anzeigename",
|
displayname: "Anzeigename",
|
||||||
password: "Passwort",
|
password: "Passwort",
|
||||||
@@ -181,6 +184,7 @@ 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.",
|
||||||
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.",
|
||||||
@@ -429,6 +433,18 @@ const de: SynapseTranslationMessages = {
|
|||||||
sender: "Absender",
|
sender: "Absender",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Medien",
|
||||||
|
fields: {
|
||||||
|
media_id: "Medien ID",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
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: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Raumverzeichnis",
|
name: "Raumverzeichnis",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -38,6 +38,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
members: "Members",
|
members: "Members",
|
||||||
detail: "Details",
|
detail: "Details",
|
||||||
permission: "Permissions",
|
permission: "Permissions",
|
||||||
|
media: "Media",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Basic", detail: "Details" } },
|
reports: { tabs: { basic: "Basic", detail: "Details" } },
|
||||||
@@ -132,11 +133,13 @@ const en: SynapseTranslationMessages = {
|
|||||||
is_guest: "Guest",
|
is_guest: "Guest",
|
||||||
admin: "Server Administrator",
|
admin: "Server Administrator",
|
||||||
locked: "Locked",
|
locked: "Locked",
|
||||||
|
suspended: "Suspended",
|
||||||
deactivated: "Deactivated",
|
deactivated: "Deactivated",
|
||||||
erased: "Erased",
|
erased: "Erased",
|
||||||
guests: "Show guests",
|
guests: "Show guests",
|
||||||
show_deactivated: "Show deactivated users",
|
show_deactivated: "Show deactivated users",
|
||||||
show_locked: "Show locked users",
|
show_locked: "Show locked users",
|
||||||
|
show_suspended: "Show suspended users",
|
||||||
user_id: "Search user",
|
user_id: "Search user",
|
||||||
displayname: "Displayname",
|
displayname: "Displayname",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
@@ -154,6 +157,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
password: "Changing password will log user out of all sessions.",
|
password: "Changing password will log user out of all sessions.",
|
||||||
create_password: "Generate a strong and secure password using the button below.",
|
create_password: "Generate a strong and secure password using the button below.",
|
||||||
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.",
|
||||||
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.",
|
||||||
@@ -401,6 +405,18 @@ const en: SynapseTranslationMessages = {
|
|||||||
sender: "Sender",
|
sender: "Sender",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Media",
|
||||||
|
fields: {
|
||||||
|
media_id: "Media ID",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "This is a list of media that has been uploaded to the room. It is not possible to delete media that has been uploaded to external media repositories.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}",
|
||||||
|
},
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Room directory",
|
name: "Room directory",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -32,6 +32,7 @@ const fa: SynapseTranslationMessages = {
|
|||||||
members: "اعضا",
|
members: "اعضا",
|
||||||
detail: "جزئیات",
|
detail: "جزئیات",
|
||||||
permission: "مجوزها",
|
permission: "مجوزها",
|
||||||
|
media: "رسانه ها",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
|
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
|
||||||
@@ -126,9 +127,12 @@ const fa: SynapseTranslationMessages = {
|
|||||||
is_guest: "مهمان",
|
is_guest: "مهمان",
|
||||||
admin: "مدیر سرور",
|
admin: "مدیر سرور",
|
||||||
deactivated: "غیرفعال",
|
deactivated: "غیرفعال",
|
||||||
|
locked: "قفل شده",
|
||||||
|
suspended: "معلق",
|
||||||
guests: "نمایش مهمانان",
|
guests: "نمایش مهمانان",
|
||||||
show_deactivated: "نمایش کاربران غیرفعال شده",
|
show_deactivated: "نمایش کاربران غیرفعال شده",
|
||||||
show_locked: "نمایش کاربران قفل شده",
|
show_locked: "نمایش کاربران قفل شده",
|
||||||
|
show_suspended: "نمایش کاربران معلق",
|
||||||
user_id: "جستجوی کاربر",
|
user_id: "جستجوی کاربر",
|
||||||
displayname: "نام نمایشی",
|
displayname: "نام نمایشی",
|
||||||
password: "رمز عبور",
|
password: "رمز عبور",
|
||||||
@@ -146,6 +150,7 @@ const fa: SynapseTranslationMessages = {
|
|||||||
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
||||||
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
|
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
|
||||||
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
||||||
|
suspend: "کاربران معلق نمی توانند وارد شوند و پیام های آنها به دیگران نمایش داده نمی شود.",
|
||||||
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
||||||
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
|
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
|
||||||
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
||||||
@@ -380,6 +385,18 @@ const fa: SynapseTranslationMessages = {
|
|||||||
sender: "فرستنده",
|
sender: "فرستنده",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "رسانه ها",
|
||||||
|
fields: {
|
||||||
|
media_id: "شناسه رسانه",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "راهنمای اتاق",
|
name: "راهنمای اتاق",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -32,6 +32,7 @@ const fr: SynapseTranslationMessages = {
|
|||||||
members: "Membres",
|
members: "Membres",
|
||||||
detail: "Détails",
|
detail: "Détails",
|
||||||
permission: "Permissions",
|
permission: "Permissions",
|
||||||
|
media: "Médias",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Informations de base", detail: "Détails" } },
|
reports: { tabs: { basic: "Informations de base", detail: "Détails" } },
|
||||||
@@ -129,10 +130,12 @@ const fr: SynapseTranslationMessages = {
|
|||||||
is_guest: "Visiteur",
|
is_guest: "Visiteur",
|
||||||
admin: "Administrateur du serveur",
|
admin: "Administrateur du serveur",
|
||||||
locked: "Verrouillé",
|
locked: "Verrouillé",
|
||||||
|
suspended: "Suspendu",
|
||||||
deactivated: "Désactivé",
|
deactivated: "Désactivé",
|
||||||
guests: "Afficher les visiteurs",
|
guests: "Afficher les visiteurs",
|
||||||
show_deactivated: "Afficher les utilisateurs désactivés",
|
show_deactivated: "Afficher les utilisateurs désactivés",
|
||||||
show_locked: "Afficher les utilisateurs verrouillés",
|
show_locked: "Afficher les utilisateurs verrouillés",
|
||||||
|
show_suspended: "Afficher les utilisateurs suspendus",
|
||||||
user_id: "Rechercher un utilisateur",
|
user_id: "Rechercher un utilisateur",
|
||||||
displayname: "Nom d'affichage",
|
displayname: "Nom d'affichage",
|
||||||
password: "Mot de passe",
|
password: "Mot de passe",
|
||||||
@@ -149,6 +152,7 @@ const fr: SynapseTranslationMessages = {
|
|||||||
password: "Changer le mot de passe déconnectera l'utilisateur de toutes les sessions.",
|
password: "Changer le mot de passe déconnectera l'utilisateur de toutes les sessions.",
|
||||||
create_password: "Générer un mot de passe fort et sécurisé en utilisant le bouton ci-dessous.",
|
create_password: "Générer un mot de passe fort et sécurisé en utilisant le bouton ci-dessous.",
|
||||||
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.",
|
||||||
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.",
|
||||||
@@ -383,6 +387,18 @@ const fr: SynapseTranslationMessages = {
|
|||||||
sender: "Expéditeur",
|
sender: "Expéditeur",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Médias",
|
||||||
|
fields: {
|
||||||
|
media_id: "Identifiant du média",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
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: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Répertoire des salons",
|
name: "Répertoire des salons",
|
||||||
fields: {
|
fields: {
|
||||||
|
16
src/i18n/index.d.ts
vendored
16
src/i18n/index.d.ts
vendored
@@ -31,6 +31,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
members: string;
|
members: string;
|
||||||
detail: string;
|
detail: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
|
media: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
reports: { tabs: { basic: string; detail: string } };
|
reports: { tabs: { basic: string; detail: string } };
|
||||||
@@ -124,11 +125,13 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
is_guest: string;
|
is_guest: string;
|
||||||
admin: string;
|
admin: string;
|
||||||
locked?: string; // TODO: fa, zh
|
locked?: string; // TODO: fa, zh
|
||||||
|
suspended: string;
|
||||||
deactivated: string;
|
deactivated: string;
|
||||||
erased?: string; // TODO: fa, fr, it, zh
|
erased?: string; // TODO: fa, fr, it, zh
|
||||||
guests: string;
|
guests: string;
|
||||||
show_deactivated: string;
|
show_deactivated: string;
|
||||||
show_locked?: string; // TODO: de, fa, fr, it, zh
|
show_locked?: string; // TODO: de, fa, fr, it, zh
|
||||||
|
show_suspended: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
displayname: string;
|
displayname: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -146,6 +149,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
password: string;
|
password: string;
|
||||||
create_password: string;
|
create_password: string;
|
||||||
deactivate: string;
|
deactivate: string;
|
||||||
|
suspend: string;
|
||||||
erase: string;
|
erase: string;
|
||||||
erase_text: string;
|
erase_text: string;
|
||||||
erase_admin_error: string;
|
erase_admin_error: string;
|
||||||
@@ -393,6 +397,18 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
sender: string;
|
sender: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
room_media?: {
|
||||||
|
name: string;
|
||||||
|
fields: {
|
||||||
|
media_id: string;
|
||||||
|
};
|
||||||
|
helper: {
|
||||||
|
info: string;
|
||||||
|
};
|
||||||
|
action: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
room_directory?: {
|
room_directory?: {
|
||||||
name: string;
|
name: string;
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -32,6 +32,7 @@ const it: SynapseTranslationMessages = {
|
|||||||
members: "Membro",
|
members: "Membro",
|
||||||
detail: "Dettagli",
|
detail: "Dettagli",
|
||||||
permission: "Permessi",
|
permission: "Permessi",
|
||||||
|
media: "Media",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Semplice", detail: "Dettagli" } },
|
reports: { tabs: { basic: "Semplice", detail: "Dettagli" } },
|
||||||
@@ -126,10 +127,12 @@ const it: SynapseTranslationMessages = {
|
|||||||
is_guest: "Ospite",
|
is_guest: "Ospite",
|
||||||
admin: "Amministratore",
|
admin: "Amministratore",
|
||||||
locked: "Bloccato",
|
locked: "Bloccato",
|
||||||
|
suspended: "Sospeso",
|
||||||
deactivated: "Disattivato",
|
deactivated: "Disattivato",
|
||||||
guests: "Mostra gli ospiti",
|
guests: "Mostra gli ospiti",
|
||||||
show_deactivated: "Mostra gli utenti disattivati",
|
show_deactivated: "Mostra gli utenti disattivati",
|
||||||
show_locked: "Mostra gli utenti bloccati",
|
show_locked: "Mostra gli utenti bloccati",
|
||||||
|
show_suspended: "Mostra gli utenti sospesi",
|
||||||
user_id: "Cerca utente",
|
user_id: "Cerca utente",
|
||||||
displayname: "Nickname",
|
displayname: "Nickname",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
@@ -147,6 +150,7 @@ const it: SynapseTranslationMessages = {
|
|||||||
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
||||||
create_password: "Genera una password forte e sicura utilizzando il pulsante sottostante.",
|
create_password: "Genera una password forte e sicura utilizzando il pulsante sottostante.",
|
||||||
deactivate: "Devi fornire una password per riattivare l'account.",
|
deactivate: "Devi fornire una password per riattivare l'account.",
|
||||||
|
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.",
|
||||||
@@ -376,6 +380,18 @@ const it: SynapseTranslationMessages = {
|
|||||||
sender: "Mittente",
|
sender: "Mittente",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Media",
|
||||||
|
fields: {
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Elenco delle stanze",
|
name: "Elenco delle stanze",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -59,6 +59,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
members: "Участники",
|
members: "Участники",
|
||||||
detail: "Подробности",
|
detail: "Подробности",
|
||||||
permission: "Права доступа",
|
permission: "Права доступа",
|
||||||
|
media: "Медиа",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "Основные", detail: "Подробности" } },
|
reports: { tabs: { basic: "Основные", detail: "Подробности" } },
|
||||||
@@ -162,11 +163,13 @@ const ru: SynapseTranslationMessages = {
|
|||||||
is_guest: "Гость",
|
is_guest: "Гость",
|
||||||
admin: "Администратор сервера",
|
admin: "Администратор сервера",
|
||||||
locked: "Заблокирован",
|
locked: "Заблокирован",
|
||||||
|
suspended: "Приостановлен",
|
||||||
deactivated: "Деактивирован",
|
deactivated: "Деактивирован",
|
||||||
erased: "Удалён",
|
erased: "Удалён",
|
||||||
guests: "Показывать гостей",
|
guests: "Показывать гостей",
|
||||||
show_deactivated: "Показывать деактивированных",
|
show_deactivated: "Показывать деактивированных",
|
||||||
show_locked: "Показывать заблокированных",
|
show_locked: "Показывать заблокированных",
|
||||||
|
show_suspended: "Показывать приостановленных",
|
||||||
user_id: "Поиск пользователя",
|
user_id: "Поиск пользователя",
|
||||||
displayname: "Отображаемое имя",
|
displayname: "Отображаемое имя",
|
||||||
password: "Пароль",
|
password: "Пароль",
|
||||||
@@ -184,6 +187,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
password: "Смена пароля завершит все сессии пользователя.",
|
password: "Смена пароля завершит все сессии пользователя.",
|
||||||
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
|
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
|
||||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
|
suspend: "Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.",
|
||||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
||||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||||
@@ -434,6 +438,18 @@ const ru: SynapseTranslationMessages = {
|
|||||||
sender: "Отправитель",
|
sender: "Отправитель",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "Медиа",
|
||||||
|
fields: {
|
||||||
|
media_id: "ID медиа",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Каталог комнат",
|
name: "Каталог комнат",
|
||||||
fields: {
|
fields: {
|
||||||
|
@@ -60,6 +60,7 @@ const zh: SynapseTranslationMessages = {
|
|||||||
members: "成员",
|
members: "成员",
|
||||||
detail: "细节",
|
detail: "细节",
|
||||||
permission: "权限",
|
permission: "权限",
|
||||||
|
media: "媒体",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "基本", detail: "细节" } },
|
reports: { tabs: { basic: "基本", detail: "细节" } },
|
||||||
@@ -153,10 +154,13 @@ const zh: SynapseTranslationMessages = {
|
|||||||
name: "用户名",
|
name: "用户名",
|
||||||
is_guest: "访客",
|
is_guest: "访客",
|
||||||
admin: "服务器管理员",
|
admin: "服务器管理员",
|
||||||
|
locked: "锁定",
|
||||||
deactivated: "被禁用",
|
deactivated: "被禁用",
|
||||||
|
suspended: "被锁定",
|
||||||
guests: "显示访客",
|
guests: "显示访客",
|
||||||
show_deactivated: "显示被禁用的账户",
|
show_deactivated: "显示被禁用的账户",
|
||||||
show_locked: "显示被锁定的账户",
|
show_locked: "显示被锁定的账户",
|
||||||
|
show_suspended: "显示被锁定的账户",
|
||||||
user_id: "搜索用户",
|
user_id: "搜索用户",
|
||||||
displayname: "显示名字",
|
displayname: "显示名字",
|
||||||
password: "密码",
|
password: "密码",
|
||||||
@@ -172,6 +176,7 @@ const zh: SynapseTranslationMessages = {
|
|||||||
password: "更改密码会使用户注销所有会话。",
|
password: "更改密码会使用户注销所有会话。",
|
||||||
create_password: "使用下面的按钮生成一个强大和安全的密码。",
|
create_password: "使用下面的按钮生成一个强大和安全的密码。",
|
||||||
deactivate: "您必须提供一串密码来激活账户。",
|
deactivate: "您必须提供一串密码来激活账户。",
|
||||||
|
suspend: "您必须提供一串密码来暂停账户。",
|
||||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||||
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
||||||
erase_admin_error: "不允许删除自己的用户",
|
erase_admin_error: "不允许删除自己的用户",
|
||||||
@@ -344,6 +349,18 @@ const zh: SynapseTranslationMessages = {
|
|||||||
media_length: "媒体文件长度",
|
media_length: "媒体文件长度",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "媒体",
|
||||||
|
fields: {
|
||||||
|
media_id: "媒体ID",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}"
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default zh;
|
export default zh;
|
||||||
|
@@ -2,7 +2,8 @@ import React from "react";
|
|||||||
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import {App, AppContext } from "./App";
|
import { App } from "./App";
|
||||||
|
import { AppContext } from "./Context";
|
||||||
import { FetchConfig, GetConfig } from "./utils/config";
|
import { FetchConfig, GetConfig } from "./utils/config";
|
||||||
|
|
||||||
await FetchConfig();
|
await FetchConfig();
|
||||||
|
@@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react";
|
|||||||
import { AdminContext } from "react-admin";
|
import { AdminContext } from "react-admin";
|
||||||
|
|
||||||
import LoginPage from "./LoginPage";
|
import LoginPage from "./LoginPage";
|
||||||
import { AppContext } from "../App";
|
import { AppContext } from "../Context";
|
||||||
import englishMessages from "../i18n/en";
|
import englishMessages from "../i18n/en";
|
||||||
|
|
||||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
||||||
@@ -33,7 +33,9 @@ describe("LoginForm", () => {
|
|||||||
|
|
||||||
it("renders with single restricted homeserver", () => {
|
it("renders with single restricted homeserver", () => {
|
||||||
render(
|
render(
|
||||||
<AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com" }}>
|
<AppContext.Provider
|
||||||
|
value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}
|
||||||
|
>
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AdminContext>
|
</AdminContext>
|
||||||
@@ -56,6 +58,8 @@ describe("LoginForm", () => {
|
|||||||
<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: [],
|
||||||
|
menu: [],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
|
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import LoginFormBox from "../components/LoginFormBox";
|
import LoginFormBox from "../components/LoginFormBox";
|
||||||
import { useAppContext } from "../App";
|
import { useAppContext } from "../Context";
|
||||||
import {
|
import {
|
||||||
getServerVersion,
|
getServerVersion,
|
||||||
getSupportedFeatures,
|
getSupportedFeatures,
|
||||||
@@ -171,11 +171,24 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const hostname = window.location.hostname;
|
||||||
const username = params.get("username");
|
const username = params.get("username");
|
||||||
|
const password = params.get("password");
|
||||||
|
const accessToken = params.get("accessToken");
|
||||||
let serverURL = params.get("server");
|
let serverURL = params.get("server");
|
||||||
if (username) {
|
if (username) {
|
||||||
form.setValue("username", username);
|
form.setValue("username", username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||||
|
if (password) {
|
||||||
|
form.setValue("password", password);
|
||||||
|
}
|
||||||
|
if (accessToken) {
|
||||||
|
setLoginMethod("accessToken");
|
||||||
|
form.setValue("accessToken", accessToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (serverURL) {
|
if (serverURL) {
|
||||||
const isFullUrl = serverURL.match(/^(http|https):\/\//);
|
const isFullUrl = serverURL.match(/^(http|https):\/\//);
|
||||||
if (!isFullUrl) {
|
if (!isFullUrl) {
|
||||||
|
@@ -123,6 +123,7 @@ export const DestinationList = (props: ListProps) => {
|
|||||||
filters={destinationFilters}
|
filters={destinationFilters}
|
||||||
pagination={<DestinationPagination />}
|
pagination={<DestinationPagination />}
|
||||||
sort={{ field: "destination", order: "ASC" }}
|
sort={{ field: "destination", order: "ASC" }}
|
||||||
|
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 source="destination" render={destinationFieldRender} label="resources.destinations.fields.destination" />
|
||||||
|
@@ -38,7 +38,7 @@ export const RegistrationTokenList = (props: ListProps) => (
|
|||||||
filters={registrationTokenFilters}
|
filters={registrationTokenFilters}
|
||||||
filterDefaultValues={{ valid: true }}
|
filterDefaultValues={{ valid: true }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
perPage={500}
|
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"/>
|
||||||
|
@@ -90,7 +90,7 @@ const ReportShowActions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ReportList = (props: ListProps) => (
|
export const ReportList = (props: ListProps) => (
|
||||||
<List {...props} pagination={<ReportPagination />} 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" />
|
||||||
|
@@ -135,7 +135,7 @@ const RoomDirectoryListActions = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const RoomDirectoryList = () => (
|
export const RoomDirectoryList = () => (
|
||||||
<List pagination={<RoomDirectoryPagination />} perPage={100} actions={<RoomDirectoryListActions />}>
|
<List pagination={<RoomDirectoryPagination />} perPage={50} actions={<RoomDirectoryListActions />}>
|
||||||
<DatagridConfigurable
|
<DatagridConfigurable
|
||||||
rowClick={id => "/rooms/" + id + "/show"}
|
rowClick={id => "/rooms/" + id + "/show"}
|
||||||
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
||||||
|
@@ -7,12 +7,12 @@ import PageviewIcon from "@mui/icons-material/Pageview";
|
|||||||
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 Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
EditButton,
|
|
||||||
WrapperField,
|
WrapperField,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DatagridConfigurable,
|
DatagridConfigurable,
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
useListContext,
|
useListContext,
|
||||||
useNotify,
|
useNotify,
|
||||||
|
DeleteButton,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
} from "./room_directory";
|
} from "./room_directory";
|
||||||
import { DATE_FORMAT } from "../utils/date";
|
import { DATE_FORMAT } from "../utils/date";
|
||||||
import DeleteRoomButton from "../components/DeleteRoomButton";
|
import DeleteRoomButton from "../components/DeleteRoomButton";
|
||||||
|
import { MediaIDField } from "../components/media";
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
import { Room } from "../synapse/dataProvider";
|
import { Room } from "../synapse/dataProvider";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
@@ -58,6 +60,8 @@ import { useState } from "react";
|
|||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import Typography from "@mui/material/Typography";
|
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]} />;
|
||||||
|
|
||||||
const RoomTitle = () => {
|
const RoomTitle = () => {
|
||||||
@@ -231,6 +235,16 @@ export const RoomShow = (props: ShowProps) => {
|
|||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab label="synapseadmin.rooms.tabs.media" icon={<PermMediaIcon />} path="media">
|
||||||
|
<Alert severity="warning">{translate("resources.room_media.helper.info")}</Alert>
|
||||||
|
<ReferenceManyField reference="room_media" target="room_id" label={false}>
|
||||||
|
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
|
<MediaIDField source="media_id" />
|
||||||
|
<DeleteButton mutationMode="pessimistic" redirect={false} />
|
||||||
|
</Datagrid>
|
||||||
|
</ReferenceManyField>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab label="synapseadmin.rooms.tabs.permission" icon={<VisibilityIcon />} path="permission">
|
<Tab label="synapseadmin.rooms.tabs.permission" icon={<VisibilityIcon />} path="permission">
|
||||||
<BooleanField source="federatable" />
|
<BooleanField source="federatable" />
|
||||||
<BooleanField source="public" />
|
<BooleanField source="public" />
|
||||||
@@ -352,6 +366,7 @@ export const RoomList = (props: ListProps) => {
|
|||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
filters={roomFilters}
|
filters={roomFilters}
|
||||||
actions={<RoomListActions />}
|
actions={<RoomListActions />}
|
||||||
|
perPage={50}
|
||||||
>
|
>
|
||||||
<DatagridConfigurable
|
<DatagridConfigurable
|
||||||
rowClick="show"
|
rowClick="show"
|
||||||
|
@@ -37,6 +37,7 @@ export const UserMediaStatsList = (props: ListProps) => (
|
|||||||
filters={userMediaStatsFilters}
|
filters={userMediaStatsFilters}
|
||||||
pagination={<UserMediaStatsPagination />}
|
pagination={<UserMediaStatsPagination />}
|
||||||
sort={{ field: "media_length", order: "DESC" }}
|
sort={{ field: "media_length", order: "DESC" }}
|
||||||
|
perPage={50}
|
||||||
>
|
>
|
||||||
<DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
|
<DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
|
||||||
<TextField source="user_id" label="resources.users.fields.id" />
|
<TextField source="user_id" label="resources.users.fields.id" />
|
||||||
|
@@ -111,6 +111,7 @@ const userFilters = [
|
|||||||
<BooleanInput source="guests" alwaysOn />,
|
<BooleanInput source="guests" alwaysOn />,
|
||||||
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
||||||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" 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 => {
|
||||||
@@ -162,10 +163,11 @@ export const UserList = (props: ListProps) => (
|
|||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={userFilters}
|
filters={userFilters}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
|
filterDefaultValues={{ guests: true, 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}
|
||||||
>
|
>
|
||||||
<DatagridConfigurable
|
<DatagridConfigurable
|
||||||
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
|
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
|
||||||
@@ -178,6 +180,7 @@ export const UserList = (props: ListProps) => (
|
|||||||
<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" />
|
||||||
<BooleanField source="locked" label="resources.users.fields.locked" />
|
<BooleanField source="locked" label="resources.users.fields.locked" />
|
||||||
|
<BooleanField source="suspended" label="resources.users.fields.suspended" />
|
||||||
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
|
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
|
||||||
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
</DatagridConfigurable>
|
</DatagridConfigurable>
|
||||||
@@ -188,7 +191,7 @@ export const UserList = (props: ListProps) => (
|
|||||||
// here only local part of user_id
|
// here only local part of user_id
|
||||||
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||||||
// storage.getItem("home_server").length is not valid here
|
// storage.getItem("home_server").length is not valid here
|
||||||
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
|
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-\+/]+$/, "synapseadmin.users.invalid_user_id")];
|
||||||
|
|
||||||
const validateAddress = [required(), maxLength(255)];
|
const validateAddress = [required(), maxLength(255)];
|
||||||
|
|
||||||
@@ -432,6 +435,7 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
<UserBooleanInput source="locked" />
|
<UserBooleanInput source="locked" />
|
||||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||||
|
<UserBooleanInput source="suspended" helperText="resources.users.helper.suspend" />
|
||||||
<BooleanInput source="erased" disabled />
|
<BooleanInput source="erased" disabled />
|
||||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
<TextField source="consent_version" />
|
<TextField source="consent_version" />
|
||||||
|
@@ -138,6 +138,7 @@ export interface User {
|
|||||||
external_ids: ExternalId[];
|
external_ids: ExternalId[];
|
||||||
user_type?: string;
|
user_type?: string;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
|
suspended?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
@@ -265,6 +266,31 @@ export interface UsernameAvailabilityResult {
|
|||||||
errcode?: string;
|
errcode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerStatusComponent {
|
||||||
|
ok: boolean;
|
||||||
|
category: string;
|
||||||
|
reason: string;
|
||||||
|
url: string;
|
||||||
|
help: string;
|
||||||
|
label: {
|
||||||
|
url: string;
|
||||||
|
icon: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerStatusResponse {
|
||||||
|
success: boolean;
|
||||||
|
ok: boolean;
|
||||||
|
host: string;
|
||||||
|
results: ServerStatusComponent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerProcessResponse {
|
||||||
|
locked_at?: string;
|
||||||
|
command?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||||
@@ -273,6 +299,8 @@ export interface SynapseDataProvider extends DataProvider {
|
|||||||
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
||||||
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
||||||
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||||
|
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
||||||
|
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
@@ -357,6 +385,20 @@ const resourceMap = {
|
|||||||
data: "members",
|
data: "members",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
},
|
},
|
||||||
|
room_media: {
|
||||||
|
map: (mediaId: string) => ({
|
||||||
|
id: mediaId.replace("mxc://" + localStorage.getItem("home_server") + "/", ""),
|
||||||
|
media_id: mediaId.replace("mxc://" + localStorage.getItem("home_server") + "/", ""),
|
||||||
|
}),
|
||||||
|
reference: (id: Identifier) => ({
|
||||||
|
endpoint: `/_synapse/admin/v1/room/${id}/media`,
|
||||||
|
}),
|
||||||
|
total: json => json.total,
|
||||||
|
data: "local",
|
||||||
|
delete: (params: DeleteParams) => ({
|
||||||
|
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
room_state: {
|
room_state: {
|
||||||
map: (rs: RoomState) => ({
|
map: (rs: RoomState) => ({
|
||||||
...rs,
|
...rs,
|
||||||
@@ -548,7 +590,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
|||||||
const baseDataProvider: SynapseDataProvider = {
|
const baseDataProvider: SynapseDataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
const { user_id, name, guests, deactivated, locked, suspended, search_term, destination, valid } = params.filter;
|
||||||
const { page, perPage } = params.pagination as PaginationPayload;
|
const { page, perPage } = params.pagination as PaginationPayload;
|
||||||
const { field, order } = params.sort as SortPayload;
|
const { field, order } = params.sort as SortPayload;
|
||||||
const from = (page - 1) * perPage;
|
const from = (page - 1) * perPage;
|
||||||
@@ -562,6 +604,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
guests: guests,
|
guests: guests,
|
||||||
deactivated: deactivated,
|
deactivated: deactivated,
|
||||||
locked: locked,
|
locked: locked,
|
||||||
|
suspended: suspended,
|
||||||
valid: valid,
|
valid: valid,
|
||||||
order_by: field,
|
order_by: field,
|
||||||
dir: getSearchOrder(order),
|
dir: getSearchOrder(order),
|
||||||
@@ -880,6 +923,60 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
getServerRunningProcess: async (runningProcessUrl: string): Promise<ServerProcessResponse> => {
|
||||||
|
const locked_at = "";
|
||||||
|
const command = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${runningProcessUrl}/lock`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error getting server running process: ${response.status} ${response.statusText}`);
|
||||||
|
return { locked_at, command };
|
||||||
|
}
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
return json as { locked_at: string; command: string };
|
||||||
|
}
|
||||||
|
if (status === 204) {
|
||||||
|
return { locked_at, command };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting server running process", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { locked_at, command };
|
||||||
|
},
|
||||||
|
getServerStatus: async (serverStatusUrl: string): Promise<ServerStatusResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${serverStatusUrl}/status`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error getting server status: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false, ok: false, host: "", results: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
if (status === 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
const result = { success: true, ...json } as ServerStatusResponse;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting server status", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, ok: false, host: "", results: [] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ export interface Config {
|
|||||||
restrictBaseUrl: string | string[];
|
restrictBaseUrl: string | string[];
|
||||||
asManagedUsers: RegExp[];
|
asManagedUsers: RegExp[];
|
||||||
menu: MenuItem[];
|
menu: MenuItem[];
|
||||||
|
etkeccAdmin?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
@@ -17,6 +18,7 @@ let config: Config = {
|
|||||||
restrictBaseUrl: "",
|
restrictBaseUrl: "",
|
||||||
asManagedUsers: [],
|
asManagedUsers: [],
|
||||||
menu: [],
|
menu: [],
|
||||||
|
etkeccAdmin: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FetchConfig = async () => {
|
export const FetchConfig = async () => {
|
||||||
@@ -38,9 +40,9 @@ export const FetchConfig = async () => {
|
|||||||
if (!configWK[WellKnownKey]) {
|
if (!configWK[WellKnownKey]) {
|
||||||
console.log(`Loaded https://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
|
console.log(`Loaded https://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Loaded https://${homeserver}.well-known/matrix/client`, configWK);
|
console.log(`Loaded https://${homeserver}.well-known/matrix/client`, configWK);
|
||||||
LoadConfig(configWK[WellKnownKey]);
|
LoadConfig(configWK[WellKnownKey]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`https://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
console.log(`https://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
||||||
}
|
}
|
||||||
@@ -49,6 +51,8 @@ export const FetchConfig = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
// with typescript types
|
||||||
export const LoadConfig = (context: any) => {
|
export const LoadConfig = (context: any) => {
|
||||||
if (context?.restrictBaseUrl) {
|
if (context?.restrictBaseUrl) {
|
||||||
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
|
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
|
||||||
@@ -65,6 +69,10 @@ export const LoadConfig = (context: any) => {
|
|||||||
if (menu.length > 0) {
|
if (menu.length > 0) {
|
||||||
config.menu = menu;
|
config.menu = menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context?.etkeccAdmin) {
|
||||||
|
config.etkeccAdmin = context.etkeccAdmin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get config
|
// get config
|
||||||
|
@@ -41,7 +41,6 @@ export function returnMXID(input: string | Identifier): string {
|
|||||||
const homeserver = localStorage.getItem("home_server");
|
const homeserver = localStorage.getItem("home_server");
|
||||||
|
|
||||||
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||||
const mxidPattern = /^@[^@:]+:[^@:]+$/;
|
|
||||||
if (isMXID(input)) {
|
if (isMXID(input)) {
|
||||||
return input as string; // Already a valid MXID
|
return input as string; // Already a valid MXID
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user