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)
|
||||
* [Changes](#changes)
|
||||
* [exclusive for etke.cc customers](#exclusive-for-etkecc-customers)
|
||||
* [Development](#development)
|
||||
* [Support](#support)
|
||||
* [Configuration](#configuration)
|
||||
@@ -58,6 +59,8 @@ The full list is described below in the [Changes](#changes) section.
|
||||
|
||||
### Changes
|
||||
|
||||
_the list will be updated as new changes are added_
|
||||
|
||||
The following changes are already implemented:
|
||||
|
||||
* 🛑 [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)
|
||||
* 🔍 [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 "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)
|
||||
* 🤖 [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
|
||||
|
||||
@@ -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),
|
||||
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
|
||||
* Password: admin
|
||||
@@ -148,13 +161,9 @@ services:
|
||||
|
||||
### 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.
|
||||
|
||||
```
|
||||
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.
|
||||
[Documentation](./docs/prefill-login-form.md)
|
||||
|
||||
|
||||
### Restricting available homeserver
|
||||
|
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
synapse:
|
||||
image: ghcr.io/element-hq/synapse:latest
|
||||
image: ghcr.io/element-hq/synapse:develop
|
||||
entrypoint: python
|
||||
command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
|
||||
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",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@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",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/node": "^22.9.3",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/react": "^18.3.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"eslint": "^9.15.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
@@ -34,38 +34,38 @@
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier": "^3.4.2",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-version-mark": "^0.1.2"
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^6.0.3",
|
||||
"vite-plugin-version-mark": "^0.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
"@haleos/ra-language-german": "^1.0.0",
|
||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||
"@mui/icons-material": "^6.1.8",
|
||||
"@mui/material": "^6.1.8",
|
||||
"@mui/utils": "^5.16.6",
|
||||
"@tanstack/react-query": "^5.61.3",
|
||||
"@mui/icons-material": "^6.1.9",
|
||||
"@mui/material": "^6.1.10",
|
||||
"@mui/utils": "^5.16.8",
|
||||
"@tanstack/react-query": "^5.62.3",
|
||||
"history": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"papaparse": "^5.4.1",
|
||||
"ra-core": "^5.3.4",
|
||||
"ra-core": "^5.4.1",
|
||||
"ra-i18n-polyglot": "^5.3.4",
|
||||
"ra-language-english": "^5.3.4",
|
||||
"ra-language-farsi": "^5.0.0",
|
||||
"ra-language-french": "^5.4.0",
|
||||
"ra-language-farsi": "^5.1.0",
|
||||
"ra-language-french": "^5.4.1",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"ra-language-russian": "^4.14.2",
|
||||
"react": "^18.3.1",
|
||||
"react-admin": "^5.4.0",
|
||||
"react-admin": "^5.4.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-is": "^18.3.1",
|
||||
"react-router": "^6.26.2",
|
||||
"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 { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import { Route } from "react-router-dom";
|
||||
|
||||
import AdminLayout from "./components/AdminLayout";
|
||||
@@ -24,7 +24,7 @@ import users from "./resources/users";
|
||||
import authProvider from "./synapse/authProvider";
|
||||
import dataProvider from "./synapse/dataProvider";
|
||||
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?
|
||||
const messages = {
|
||||
@@ -64,6 +64,7 @@ export const App = () => (
|
||||
>
|
||||
<CustomRoutes>
|
||||
<Route path="/import_users" element={<UserImport />} />
|
||||
<Route path="/server_status" element={<ServerStatusPage />} />
|
||||
</CustomRoutes>
|
||||
<Resource {...users} />
|
||||
<Resource {...rooms} />
|
||||
@@ -86,8 +87,4 @@ export const App = () => (
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
export const AppContext = createContext({});
|
||||
|
||||
export const useAppContext = () => useContext(AppContext) as Config;
|
||||
|
||||
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 { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
||||
import Footer from "./Footer";
|
||||
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
||||
|
||||
const AdminUserMenu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -48,6 +49,7 @@ const AdminUserMenu = () => {
|
||||
const AdminAppBar = () => {
|
||||
return (<AppBar userMenu={<AdminUserMenu />}>
|
||||
<TitlePortal />
|
||||
<ServerStatusBadge />
|
||||
<InspectorButton />
|
||||
</AppBar>);
|
||||
};
|
||||
@@ -59,7 +61,7 @@ const AdminMenu = (props) => {
|
||||
return (
|
||||
<Menu {...props}>
|
||||
<Menu.ResourceItems />
|
||||
{menu.map((item, index) => {
|
||||
{menu && menu.map((item, index) => {
|
||||
const { url, icon, label } = item;
|
||||
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 }) => {
|
||||
const translate = useTranslate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const notify = useNotify();
|
||||
const isImage = mimetype && mimetype.startsWith("image/");
|
||||
|
||||
const openFileInNewTab = (blobURL: string) => {
|
||||
@@ -340,6 +341,8 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
||||
const handleFile = async (preview: boolean) => {
|
||||
setLoading(true);
|
||||
const response = await fetchAuthenticatedMedia(mxcURL, "original");
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
if (preview) {
|
||||
@@ -347,6 +350,17 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
||||
} else {
|
||||
downloadFile(blobURL);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -391,8 +405,16 @@ export const MediaIDField = ({ source }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
|
||||
const mxcURL = `mxc://${homeserver}/${mediaID}`;
|
||||
let uploadName = 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}/>;
|
||||
};
|
||||
|
@@ -64,6 +64,7 @@ const de: SynapseTranslationMessages = {
|
||||
members: "Mitglieder",
|
||||
detail: "Details",
|
||||
permission: "Berechtigungen",
|
||||
media: "Medien",
|
||||
},
|
||||
},
|
||||
reports: { tabs: { basic: "Allgemein", detail: "Details" } },
|
||||
@@ -159,11 +160,13 @@ const de: SynapseTranslationMessages = {
|
||||
is_guest: "Gast",
|
||||
admin: "Server Administrator",
|
||||
locked: "Gesperrt",
|
||||
suspended: "Suspendiert",
|
||||
deactivated: "Deaktiviert",
|
||||
erased: "Gelöscht",
|
||||
guests: "Zeige Gäste",
|
||||
show_deactivated: "Zeige deaktivierte Benutzer",
|
||||
show_locked: "Zeige gesperrte Benutzer",
|
||||
show_suspended: "Zeige suspendierte Benutzer",
|
||||
user_id: "Suche Benutzer",
|
||||
displayname: "Anzeigename",
|
||||
password: "Passwort",
|
||||
@@ -181,6 +184,7 @@ const de: SynapseTranslationMessages = {
|
||||
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.",
|
||||
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_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.",
|
||||
@@ -429,6 +433,18 @@ const de: SynapseTranslationMessages = {
|
||||
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: {
|
||||
name: "Raumverzeichnis",
|
||||
fields: {
|
||||
|
@@ -38,6 +38,7 @@ const en: SynapseTranslationMessages = {
|
||||
members: "Members",
|
||||
detail: "Details",
|
||||
permission: "Permissions",
|
||||
media: "Media",
|
||||
}
|
||||
},
|
||||
reports: { tabs: { basic: "Basic", detail: "Details" } },
|
||||
@@ -132,11 +133,13 @@ const en: SynapseTranslationMessages = {
|
||||
is_guest: "Guest",
|
||||
admin: "Server Administrator",
|
||||
locked: "Locked",
|
||||
suspended: "Suspended",
|
||||
deactivated: "Deactivated",
|
||||
erased: "Erased",
|
||||
guests: "Show guests",
|
||||
show_deactivated: "Show deactivated users",
|
||||
show_locked: "Show locked users",
|
||||
show_suspended: "Show suspended users",
|
||||
user_id: "Search user",
|
||||
displayname: "Displayname",
|
||||
password: "Password",
|
||||
@@ -154,6 +157,7 @@ const en: SynapseTranslationMessages = {
|
||||
password: "Changing password will log user out of all sessions.",
|
||||
create_password: "Generate a strong and secure password using the button below.",
|
||||
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_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.",
|
||||
@@ -401,6 +405,18 @@ const en: SynapseTranslationMessages = {
|
||||
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: {
|
||||
name: "Room directory",
|
||||
fields: {
|
||||
|
@@ -32,6 +32,7 @@ const fa: SynapseTranslationMessages = {
|
||||
members: "اعضا",
|
||||
detail: "جزئیات",
|
||||
permission: "مجوزها",
|
||||
media: "رسانه ها",
|
||||
},
|
||||
},
|
||||
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
|
||||
@@ -126,9 +127,12 @@ const fa: SynapseTranslationMessages = {
|
||||
is_guest: "مهمان",
|
||||
admin: "مدیر سرور",
|
||||
deactivated: "غیرفعال",
|
||||
locked: "قفل شده",
|
||||
suspended: "معلق",
|
||||
guests: "نمایش مهمانان",
|
||||
show_deactivated: "نمایش کاربران غیرفعال شده",
|
||||
show_locked: "نمایش کاربران قفل شده",
|
||||
show_suspended: "نمایش کاربران معلق",
|
||||
user_id: "جستجوی کاربر",
|
||||
displayname: "نام نمایشی",
|
||||
password: "رمز عبور",
|
||||
@@ -146,6 +150,7 @@ const fa: SynapseTranslationMessages = {
|
||||
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
||||
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
|
||||
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
||||
suspend: "کاربران معلق نمی توانند وارد شوند و پیام های آنها به دیگران نمایش داده نمی شود.",
|
||||
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
||||
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
|
||||
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
||||
@@ -380,6 +385,18 @@ const fa: SynapseTranslationMessages = {
|
||||
sender: "فرستنده",
|
||||
},
|
||||
},
|
||||
room_media: {
|
||||
name: "رسانه ها",
|
||||
fields: {
|
||||
media_id: "شناسه رسانه",
|
||||
},
|
||||
helper: {
|
||||
info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.",
|
||||
},
|
||||
action: {
|
||||
error: "%{errcode} (%{errstatus}) %{error}"
|
||||
}
|
||||
},
|
||||
room_directory: {
|
||||
name: "راهنمای اتاق",
|
||||
fields: {
|
||||
|
@@ -32,6 +32,7 @@ const fr: SynapseTranslationMessages = {
|
||||
members: "Membres",
|
||||
detail: "Détails",
|
||||
permission: "Permissions",
|
||||
media: "Médias",
|
||||
},
|
||||
},
|
||||
reports: { tabs: { basic: "Informations de base", detail: "Détails" } },
|
||||
@@ -129,10 +130,12 @@ const fr: SynapseTranslationMessages = {
|
||||
is_guest: "Visiteur",
|
||||
admin: "Administrateur du serveur",
|
||||
locked: "Verrouillé",
|
||||
suspended: "Suspendu",
|
||||
deactivated: "Désactivé",
|
||||
guests: "Afficher les visiteurs",
|
||||
show_deactivated: "Afficher les utilisateurs désactivés",
|
||||
show_locked: "Afficher les utilisateurs verrouillés",
|
||||
show_suspended: "Afficher les utilisateurs suspendus",
|
||||
user_id: "Rechercher un utilisateur",
|
||||
displayname: "Nom d'affichage",
|
||||
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.",
|
||||
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.",
|
||||
suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.",
|
||||
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_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
||||
@@ -383,6 +387,18 @@ const fr: SynapseTranslationMessages = {
|
||||
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: {
|
||||
name: "Répertoire des salons",
|
||||
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;
|
||||
detail: string;
|
||||
permission: string;
|
||||
media: string;
|
||||
};
|
||||
};
|
||||
reports: { tabs: { basic: string; detail: string } };
|
||||
@@ -124,11 +125,13 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
is_guest: string;
|
||||
admin: string;
|
||||
locked?: string; // TODO: fa, zh
|
||||
suspended: string;
|
||||
deactivated: string;
|
||||
erased?: string; // TODO: fa, fr, it, zh
|
||||
guests: string;
|
||||
show_deactivated: string;
|
||||
show_locked?: string; // TODO: de, fa, fr, it, zh
|
||||
show_suspended: string;
|
||||
user_id: string;
|
||||
displayname: string;
|
||||
password: string;
|
||||
@@ -146,6 +149,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
password: string;
|
||||
create_password: string;
|
||||
deactivate: string;
|
||||
suspend: string;
|
||||
erase: string;
|
||||
erase_text: string;
|
||||
erase_admin_error: string;
|
||||
@@ -393,6 +397,18 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
sender: string;
|
||||
};
|
||||
};
|
||||
room_media?: {
|
||||
name: string;
|
||||
fields: {
|
||||
media_id: string;
|
||||
};
|
||||
helper: {
|
||||
info: string;
|
||||
};
|
||||
action: {
|
||||
error: string;
|
||||
};
|
||||
};
|
||||
room_directory?: {
|
||||
name: string;
|
||||
fields: {
|
||||
|
@@ -32,6 +32,7 @@ const it: SynapseTranslationMessages = {
|
||||
members: "Membro",
|
||||
detail: "Dettagli",
|
||||
permission: "Permessi",
|
||||
media: "Media",
|
||||
},
|
||||
},
|
||||
reports: { tabs: { basic: "Semplice", detail: "Dettagli" } },
|
||||
@@ -126,10 +127,12 @@ const it: SynapseTranslationMessages = {
|
||||
is_guest: "Ospite",
|
||||
admin: "Amministratore",
|
||||
locked: "Bloccato",
|
||||
suspended: "Sospeso",
|
||||
deactivated: "Disattivato",
|
||||
guests: "Mostra gli ospiti",
|
||||
show_deactivated: "Mostra gli utenti disattivati",
|
||||
show_locked: "Mostra gli utenti bloccati",
|
||||
show_suspended: "Mostra gli utenti sospesi",
|
||||
user_id: "Cerca utente",
|
||||
displayname: "Nickname",
|
||||
password: "Password",
|
||||
@@ -147,6 +150,7 @@ const it: SynapseTranslationMessages = {
|
||||
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.",
|
||||
deactivate: "Devi fornire una password per riattivare l'account.",
|
||||
suspend: "Sospendi l'utente",
|
||||
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_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||
@@ -376,6 +380,18 @@ const it: SynapseTranslationMessages = {
|
||||
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: {
|
||||
name: "Elenco delle stanze",
|
||||
fields: {
|
||||
|
@@ -59,6 +59,7 @@ const ru: SynapseTranslationMessages = {
|
||||
members: "Участники",
|
||||
detail: "Подробности",
|
||||
permission: "Права доступа",
|
||||
media: "Медиа",
|
||||
},
|
||||
},
|
||||
reports: { tabs: { basic: "Основные", detail: "Подробности" } },
|
||||
@@ -162,11 +163,13 @@ const ru: SynapseTranslationMessages = {
|
||||
is_guest: "Гость",
|
||||
admin: "Администратор сервера",
|
||||
locked: "Заблокирован",
|
||||
suspended: "Приостановлен",
|
||||
deactivated: "Деактивирован",
|
||||
erased: "Удалён",
|
||||
guests: "Показывать гостей",
|
||||
show_deactivated: "Показывать деактивированных",
|
||||
show_locked: "Показывать заблокированных",
|
||||
show_suspended: "Показывать приостановленных",
|
||||
user_id: "Поиск пользователя",
|
||||
displayname: "Отображаемое имя",
|
||||
password: "Пароль",
|
||||
@@ -184,6 +187,7 @@ const ru: SynapseTranslationMessages = {
|
||||
password: "Смена пароля завершит все сессии пользователя.",
|
||||
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
|
||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||
suspend: "Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.",
|
||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||
@@ -434,6 +438,18 @@ const ru: SynapseTranslationMessages = {
|
||||
sender: "Отправитель",
|
||||
},
|
||||
},
|
||||
room_media: {
|
||||
name: "Медиа",
|
||||
fields: {
|
||||
media_id: "ID медиа",
|
||||
},
|
||||
helper: {
|
||||
info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.",
|
||||
},
|
||||
action: {
|
||||
error: "%{errcode} (%{errstatus}) %{error}"
|
||||
}
|
||||
},
|
||||
room_directory: {
|
||||
name: "Каталог комнат",
|
||||
fields: {
|
||||
|
@@ -60,6 +60,7 @@ const zh: SynapseTranslationMessages = {
|
||||
members: "成员",
|
||||
detail: "细节",
|
||||
permission: "权限",
|
||||
media: "媒体",
|
||||
},
|
||||
},
|
||||
reports: { tabs: { basic: "基本", detail: "细节" } },
|
||||
@@ -153,10 +154,13 @@ const zh: SynapseTranslationMessages = {
|
||||
name: "用户名",
|
||||
is_guest: "访客",
|
||||
admin: "服务器管理员",
|
||||
locked: "锁定",
|
||||
deactivated: "被禁用",
|
||||
suspended: "被锁定",
|
||||
guests: "显示访客",
|
||||
show_deactivated: "显示被禁用的账户",
|
||||
show_locked: "显示被锁定的账户",
|
||||
show_suspended: "显示被锁定的账户",
|
||||
user_id: "搜索用户",
|
||||
displayname: "显示名字",
|
||||
password: "密码",
|
||||
@@ -172,6 +176,7 @@ const zh: SynapseTranslationMessages = {
|
||||
password: "更改密码会使用户注销所有会话。",
|
||||
create_password: "使用下面的按钮生成一个强大和安全的密码。",
|
||||
deactivate: "您必须提供一串密码来激活账户。",
|
||||
suspend: "您必须提供一串密码来暂停账户。",
|
||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
||||
erase_admin_error: "不允许删除自己的用户",
|
||||
@@ -344,6 +349,18 @@ const zh: SynapseTranslationMessages = {
|
||||
media_length: "媒体文件长度",
|
||||
},
|
||||
},
|
||||
room_media: {
|
||||
name: "媒体",
|
||||
fields: {
|
||||
media_id: "媒体ID",
|
||||
},
|
||||
helper: {
|
||||
info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。",
|
||||
},
|
||||
action: {
|
||||
error: "%{errcode} (%{errstatus}) %{error}"
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
export default zh;
|
||||
|
@@ -2,7 +2,8 @@ import React from "react";
|
||||
|
||||
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";
|
||||
|
||||
await FetchConfig();
|
||||
|
@@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react";
|
||||
import { AdminContext } from "react-admin";
|
||||
|
||||
import LoginPage from "./LoginPage";
|
||||
import { AppContext } from "../App";
|
||||
import { AppContext } from "../Context";
|
||||
import englishMessages from "../i18n/en";
|
||||
|
||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
||||
@@ -33,7 +33,9 @@ describe("LoginForm", () => {
|
||||
|
||||
it("renders with single restricted homeserver", () => {
|
||||
render(
|
||||
<AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com" }}>
|
||||
<AppContext.Provider
|
||||
value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}
|
||||
>
|
||||
<AdminContext i18nProvider={i18nProvider}>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
@@ -56,6 +58,8 @@ describe("LoginForm", () => {
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
||||
asManagedUsers: [],
|
||||
menu: [],
|
||||
}}
|
||||
>
|
||||
<AdminContext i18nProvider={i18nProvider}>
|
||||
|
@@ -16,7 +16,7 @@ import {
|
||||
} from "react-admin";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import LoginFormBox from "../components/LoginFormBox";
|
||||
import { useAppContext } from "../App";
|
||||
import { useAppContext } from "../Context";
|
||||
import {
|
||||
getServerVersion,
|
||||
getSupportedFeatures,
|
||||
@@ -171,11 +171,24 @@ const LoginPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const hostname = window.location.hostname;
|
||||
const username = params.get("username");
|
||||
const password = params.get("password");
|
||||
const accessToken = params.get("accessToken");
|
||||
let serverURL = params.get("server");
|
||||
if (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) {
|
||||
const isFullUrl = serverURL.match(/^(http|https):\/\//);
|
||||
if (!isFullUrl) {
|
||||
|
@@ -123,6 +123,7 @@ export const DestinationList = (props: ListProps) => {
|
||||
filters={destinationFilters}
|
||||
pagination={<DestinationPagination />}
|
||||
sort={{ field: "destination", order: "ASC" }}
|
||||
perPage={50}
|
||||
>
|
||||
<DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
||||
<FunctionField source="destination" render={destinationFieldRender} label="resources.destinations.fields.destination" />
|
||||
|
@@ -38,7 +38,7 @@ export const RegistrationTokenList = (props: ListProps) => (
|
||||
filters={registrationTokenFilters}
|
||||
filterDefaultValues={{ valid: true }}
|
||||
pagination={false}
|
||||
perPage={500}
|
||||
perPage={50}
|
||||
>
|
||||
<DatagridConfigurable rowClick="edit">
|
||||
<TextField source="token" sortable={false} label="resources.registration_tokens.fields.token"/>
|
||||
|
@@ -90,7 +90,7 @@ const ReportShowActions = () => {
|
||||
};
|
||||
|
||||
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}>
|
||||
<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" />
|
||||
|
@@ -135,7 +135,7 @@ const RoomDirectoryListActions = () => (
|
||||
);
|
||||
|
||||
export const RoomDirectoryList = () => (
|
||||
<List pagination={<RoomDirectoryPagination />} perPage={100} actions={<RoomDirectoryListActions />}>
|
||||
<List pagination={<RoomDirectoryPagination />} perPage={50} actions={<RoomDirectoryListActions />}>
|
||||
<DatagridConfigurable
|
||||
rowClick={id => "/rooms/" + id + "/show"}
|
||||
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
||||
|
@@ -7,12 +7,12 @@ import PageviewIcon from "@mui/icons-material/Pageview";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
import RoomIcon from "@mui/icons-material/ViewList";
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
EditButton,
|
||||
WrapperField,
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
useTranslate,
|
||||
useListContext,
|
||||
useNotify,
|
||||
DeleteButton,
|
||||
} from "react-admin";
|
||||
|
||||
import TextField from "@mui/material/TextField";
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
} from "./room_directory";
|
||||
import { DATE_FORMAT } from "../utils/date";
|
||||
import DeleteRoomButton from "../components/DeleteRoomButton";
|
||||
import { MediaIDField } from "../components/media";
|
||||
import AvatarField from "../components/AvatarField";
|
||||
import { Room } from "../synapse/dataProvider";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
@@ -58,6 +60,8 @@ 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 RoomTitle = () => {
|
||||
@@ -231,6 +235,16 @@ export const RoomShow = (props: ShowProps) => {
|
||||
</ReferenceManyField>
|
||||
</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">
|
||||
<BooleanField source="federatable" />
|
||||
<BooleanField source="public" />
|
||||
@@ -352,6 +366,7 @@ export const RoomList = (props: ListProps) => {
|
||||
sort={{ field: "name", order: "ASC" }}
|
||||
filters={roomFilters}
|
||||
actions={<RoomListActions />}
|
||||
perPage={50}
|
||||
>
|
||||
<DatagridConfigurable
|
||||
rowClick="show"
|
||||
|
@@ -37,6 +37,7 @@ export const UserMediaStatsList = (props: ListProps) => (
|
||||
filters={userMediaStatsFilters}
|
||||
pagination={<UserMediaStatsPagination />}
|
||||
sort={{ field: "media_length", order: "DESC" }}
|
||||
perPage={50}
|
||||
>
|
||||
<DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
|
||||
<TextField source="user_id" label="resources.users.fields.id" />
|
||||
|
@@ -111,6 +111,7 @@ const userFilters = [
|
||||
<BooleanInput source="guests" 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_suspended" source="suspended" alwaysOn />,
|
||||
];
|
||||
|
||||
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean; asManagedUserIsSelected: boolean }> = props => {
|
||||
@@ -162,10 +163,11 @@ export const UserList = (props: ListProps) => (
|
||||
<List
|
||||
{...props}
|
||||
filters={userFilters}
|
||||
filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
|
||||
filterDefaultValues={{ guests: true, deactivated: false, locked: false, suspended: false }}
|
||||
sort={{ field: "name", order: "ASC" }}
|
||||
actions={<UserListActions />}
|
||||
pagination={<UserPagination />}
|
||||
perPage={50}
|
||||
>
|
||||
<DatagridConfigurable
|
||||
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="deactivated" label="resources.users.fields.deactivated" />
|
||||
<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" />
|
||||
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||
</DatagridConfigurable>
|
||||
@@ -188,7 +191,7 @@ export const UserList = (props: ListProps) => (
|
||||
// here only local part of user_id
|
||||
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||||
// storage.getItem("home_server").length is not valid here
|
||||
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
|
||||
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-\+/]+$/, "synapseadmin.users.invalid_user_id")];
|
||||
|
||||
const validateAddress = [required(), maxLength(255)];
|
||||
|
||||
@@ -432,6 +435,7 @@ export const UserEdit = (props: EditProps) => {
|
||||
<BooleanInput source="admin" />
|
||||
<UserBooleanInput source="locked" />
|
||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||
<UserBooleanInput source="suspended" helperText="resources.users.helper.suspend" />
|
||||
<BooleanInput source="erased" disabled />
|
||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||
<TextField source="consent_version" />
|
||||
|
@@ -138,6 +138,7 @@ export interface User {
|
||||
external_ids: ExternalId[];
|
||||
user_type?: string;
|
||||
locked: boolean;
|
||||
suspended?: boolean;
|
||||
}
|
||||
|
||||
interface Device {
|
||||
@@ -265,6 +266,31 @@ export interface UsernameAvailabilityResult {
|
||||
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 {
|
||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||
@@ -273,6 +299,8 @@ export interface SynapseDataProvider extends DataProvider {
|
||||
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
||||
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
||||
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 = {
|
||||
@@ -357,6 +385,20 @@ const resourceMap = {
|
||||
data: "members",
|
||||
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: {
|
||||
map: (rs: RoomState) => ({
|
||||
...rs,
|
||||
@@ -548,7 +590,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
||||
const baseDataProvider: SynapseDataProvider = {
|
||||
getList: async (resource, params) => {
|
||||
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 { field, order } = params.sort as SortPayload;
|
||||
const from = (page - 1) * perPage;
|
||||
@@ -562,6 +604,7 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
guests: guests,
|
||||
deactivated: deactivated,
|
||||
locked: locked,
|
||||
suspended: suspended,
|
||||
valid: valid,
|
||||
order_by: field,
|
||||
dir: getSearchOrder(order),
|
||||
@@ -880,6 +923,60 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
}
|
||||
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[];
|
||||
asManagedUsers: RegExp[];
|
||||
menu: MenuItem[];
|
||||
etkeccAdmin?: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
@@ -17,6 +18,7 @@ let config: Config = {
|
||||
restrictBaseUrl: "",
|
||||
asManagedUsers: [],
|
||||
menu: [],
|
||||
etkeccAdmin: ""
|
||||
};
|
||||
|
||||
export const FetchConfig = async () => {
|
||||
@@ -49,6 +51,8 @@ export const FetchConfig = async () => {
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
if (context?.restrictBaseUrl) {
|
||||
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
|
||||
@@ -65,6 +69,10 @@ export const LoadConfig = (context: any) => {
|
||||
if (menu.length > 0) {
|
||||
config.menu = menu;
|
||||
}
|
||||
|
||||
if (context?.etkeccAdmin) {
|
||||
config.etkeccAdmin = context.etkeccAdmin;
|
||||
}
|
||||
}
|
||||
|
||||
// get config
|
||||
|
@@ -41,7 +41,6 @@ export function returnMXID(input: string | Identifier): string {
|
||||
const homeserver = localStorage.getItem("home_server");
|
||||
|
||||
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||
const mxidPattern = /^@[^@:]+:[^@:]+$/;
|
||||
if (isMXID(input)) {
|
||||
return input as string; // Already a valid MXID
|
||||
}
|
||||
|
Reference in New Issue
Block a user