From 8501f19a03a80031cd27c06ccd0cc8ed6bb7f499 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:35:42 +0100 Subject: [PATCH] Add admin API for destinations (#213) * Add admin API for destinations * Add rooms and connections to federation/destinations --- README.md | 2 +- src/App.js | 11 +- src/components/destinations.js | 185 +++++++++++++++++++++++++++++++++ src/i18n/de.js | 12 +++ src/i18n/en.js | 12 +++ src/synapse/dataProvider.js | 40 ++++++- 6 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 src/components/destinations.js diff --git a/README.md b/README.md index d01d397..3b7a2e9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This project is built using [react-admin](https://marmelab.com/react-admin/). ### Supported Synapse -It needs at least [Synapse](https://github.com/matrix-org/synapse) v1.48.0 for all functions to work as expected! +It needs at least [Synapse](https://github.com/matrix-org/synapse) v1.52.0 for all functions to work as expected! You get your server version with the request `/_synapse/admin/v1/server_version`. See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html). diff --git a/src/App.js b/src/App.js index ee24fac..7a27d0c 100644 --- a/src/App.js +++ b/src/App.js @@ -7,13 +7,15 @@ import { UserList, UserCreate, UserEdit } from "./components/users"; import { RoomList, RoomShow } from "./components/rooms"; import { ReportList, ReportShow } from "./components/EventReports"; import LoginPage from "./components/LoginPage"; -import UserIcon from "@material-ui/icons/Group"; import ConfirmationNumberIcon from "@material-ui/icons/ConfirmationNumber"; +import CloudQueueIcon from "@material-ui/icons/CloudQueue"; import EqualizerIcon from "@material-ui/icons/Equalizer"; +import UserIcon from "@material-ui/icons/Group"; import { UserMediaStatsList } from "./components/statistics"; import RoomIcon from "@material-ui/icons/ViewList"; import ReportIcon from "@material-ui/icons/Warning"; import FolderSharedIcon from "@material-ui/icons/FolderShared"; +import { DestinationList, DestinationShow } from "./components/destinations"; import { ImportFeature } from "./components/ImportFeature"; import { RegistrationTokenCreate, @@ -72,6 +74,12 @@ const App = () => ( list={RoomDirectoryList} icon={FolderSharedIcon} /> + ( + ); diff --git a/src/components/destinations.js b/src/components/destinations.js new file mode 100644 index 0000000..542b89b --- /dev/null +++ b/src/components/destinations.js @@ -0,0 +1,185 @@ +import React from "react"; +import { + Button, + Datagrid, + DateField, + Filter, + List, + Pagination, + ReferenceField, + ReferenceManyField, + SearchInput, + Show, + Tab, + TabbedShowLayout, + TextField, + TopToolbar, + useRecordContext, + useDelete, + useNotify, + useRefresh, + useTranslate, +} from "react-admin"; +import AutorenewIcon from "@material-ui/icons/Autorenew"; +import FolderSharedIcon from "@material-ui/icons/FolderShared"; +import ViewListIcon from "@material-ui/icons/ViewList"; + +const DestinationPagination = props => ( + +); + +const date_format = { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", +}; + +const destinationRowStyle = (record, index) => ({ + backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white", +}); + +const DestinationFilter = ({ ...props }) => { + return ( + + + + ); +}; + +export const DestinationReconnectButton = props => { + const record = useRecordContext(); + const refresh = useRefresh(); + const notify = useNotify(); + const [handleReconnect, { isLoading }] = useDelete("destinations"); + + // Reconnect is not required if no error has occurred. (`failure_ts`) + if (!record || !record.failure_ts) return null; + + const handleClick = e => { + // Prevents redirection to the detail page when clicking in the list + e.stopPropagation(); + + handleReconnect( + { payload: { id: record.id } }, + { + onSuccess: () => { + notify("ra.notification.updated", { + messageArgs: { smart_count: 1 }, + }); + refresh(); + }, + onFailure: () => { + notify("ra.message.error", { type: "error" }); + }, + } + ); + }; + + return ( + + ); +}; + +const DestinationShowActions = props => ( + + + +); + +const DestinationTitle = props => { + const record = useRecordContext(); + const translate = useTranslate(); + return ( + + {translate("resources.destinations.name", 1)} {record.destination} + + ); +}; + +export const DestinationList = props => { + return ( + } + pagination={} + sort={{ field: "destination", order: "ASC" }} + bulkActionButtons={false} + > + `${basePath}/${id}/show/rooms`} + > + + + + + + + + + ); +}; + +export const DestinationShow = props => { + const translate = useTranslate(); + return ( + } + title={} + {...props} + > + + }> + + + + + + + + } + path="rooms" + > + } + perPage={50} + > + `/rooms/${id}/show`} + > + + + + + + + + + + + ); +}; diff --git a/src/i18n/de.js b/src/i18n/de.js index b98d4ba..0b77fd2 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -355,6 +355,18 @@ const de = { send_failure: "Beim Entfernen ist ein Fehler aufgetreten.", }, }, + destinations: { + name: "Föderation", + fields: { + destination: "Ziel", + failure_ts: "Fehlerzeitpunkt", + retry_last_ts: "Letzter Wiederholungsversuch", + retry_interval: "Wiederholungsintervall", + last_successful_stream_ordering: "letzte erfogreicher Stream", + stream_ordering: "Stream", + }, + action: { reconnect: "Neu verbinden" }, + }, registration_tokens: { name: "Registrierungstoken", fields: { diff --git a/src/i18n/en.js b/src/i18n/en.js index 6c5836c..d3ccda3 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -352,6 +352,18 @@ const en = { send_failure: "An error has occurred.", }, }, + destinations: { + name: "Federation", + fields: { + destination: "Destination", + failure_ts: "Failure timestamp", + retry_last_ts: "Last retry timestamp", + retry_interval: "Retry interval", + last_successful_stream_ordering: "Last successful stream", + stream_ordering: "Stream", + }, + action: { reconnect: "Reconnect" }, + }, }, registration_tokens: { name: "Registration tokens", diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index 27ffc94..db57ec7 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -281,6 +281,34 @@ const resourceMap = { method: "PUT", }), }, + destinations: { + path: "/_synapse/admin/v1/federation/destinations", + map: dst => ({ + ...dst, + id: dst.destination, + }), + data: "destinations", + total: json => { + return json.total; + }, + delete: params => ({ + endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`, + method: "POST", + }), + }, + destination_rooms: { + map: dstroom => ({ + ...dstroom, + id: dstroom.room_id, + }), + reference: id => ({ + endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`, + }), + data: "rooms", + total: json => { + return json.total; + }, + }, registration_tokens: { path: "/_synapse/admin/v1/registration_tokens", map: rt => ({ @@ -322,8 +350,15 @@ function getSearchOrder(order) { const dataProvider = { getList: (resource, params) => { console.log("getList " + resource); - const { user_id, name, guests, deactivated, search_term, valid } = - params.filter; + const { + user_id, + name, + guests, + deactivated, + search_term, + destination, + valid, + } = params.filter; const { page, perPage } = params.pagination; const { field, order } = params.sort; const from = (page - 1) * perPage; @@ -333,6 +368,7 @@ const dataProvider = { user_id: user_id, search_term: search_term, name: name, + destination: destination, guests: guests, deactivated: deactivated, valid: valid,