diff --git a/README.md b/README.md index 903f4f1..4bae8a4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,28 @@ This project is built using [react-admin](https://marmelab.com/react-admin/). -Use `yarn install` after cloning this repo. +It needs at least Synapse v1.14.0 for all functions to work as expected! -Use `yarn start` to launch the webserver. +## Step-By-Step install: + +You have two options: + +1. Download the source code from github and run using nodejs +2. Run the Docker container + +Steps for 1): + +- make sure you have installed the following: git, yarn, nodejs +- download the source code: `git clone https://github.com/Awesome-Technologies/synapse-admin.git` +- change into downloaded directory: `cd synapse-admin` +- download dependencies: `yarn install` +- start web server: `yarn start` + +Steps for 2): + +- run the Docker container: `docker run -p 8080:80 awesometechnologies/synapse-admin` +- browse to http://localhost:8080 + +## Screenshots + +![Screenshots](./screenshots.jpg) diff --git a/package.json b/package.json index 7d754fe..002f90d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "synapse-admin", - "version": "0.1.0", + "version": "0.2.1", "description": "Admin GUI for the Matrix.org server Synapse", "author": "Awesome Technologies Innovationslabor GmbH", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "react-scripts": "^3.4.1" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", + "start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start", + "build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build", "fix:other": "yarn prettier --write", "fix:code": "yarn test:lint --fix", "fix": "yarn fix:code && yarn fix:other", diff --git a/public/index.html b/public/index.html index 4209361..2b90907 100644 --- a/public/index.html +++ b/public/index.html @@ -38,5 +38,12 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> + - + \ No newline at end of file diff --git a/screenshots.jpg b/screenshots.jpg new file mode 100644 index 0000000..9a08375 Binary files /dev/null and b/screenshots.jpg differ diff --git a/src/App.js b/src/App.js index f943e3f..b3c40c3 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,7 @@ import polyglotI18nProvider from "ra-i18n-polyglot"; import authProvider from "./synapse/authProvider"; import dataProvider from "./synapse/dataProvider"; import { UserList, UserCreate, UserEdit } from "./components/users"; -import { RoomList } from "./components/rooms"; +import { RoomList, RoomShow } from "./components/rooms"; import LoginPage from "./components/LoginPage"; import UserIcon from "@material-ui/icons/Group"; import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList"; @@ -35,7 +35,7 @@ const App = () => ( edit={UserEdit} icon={UserIcon} /> - + diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js index 6834625..3058b45 100644 --- a/src/components/LoginPage.js +++ b/src/components/LoginPage.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { fetchUtils, FormDataConsumer, @@ -29,7 +29,7 @@ const useStyles = makeStyles(theme => ({ main: { display: "flex", flexDirection: "column", - minHeight: "100vh", + minHeight: "calc(100vh - 1em)", alignItems: "center", justifyContent: "flex-start", background: "url(./images/floating-cogs.svg)", @@ -40,6 +40,7 @@ const useStyles = makeStyles(theme => ({ card: { minWidth: "30em", marginTop: "6em", + marginBottom: "6em", }, avatar: { margin: "1em", @@ -64,6 +65,12 @@ const useStyles = makeStyles(theme => ({ actions: { padding: "0 1em 1em 1em", }, + serverVersion: { + color: "#9e9e9e", + fontFamily: "Roboto, Helvetica, Arial, sans-serif", + marginBottom: "1em", + marginLeft: "0.5em", + }, })); const LoginPage = ({ theme }) => { @@ -137,6 +144,7 @@ const LoginPage = ({ theme }) => { const UserData = ({ formData }) => { const form = useForm(); + const [serverVersion, setServerVersion] = useState(""); const handleUsernameChange = _ => { if (formData.base_url) return; @@ -157,6 +165,30 @@ const LoginPage = ({ theme }) => { } }; + useEffect( + _ => { + if ( + !formData.base_url || + !formData.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+$/) + ) + return; + const versionUrl = `${formData.base_url}/_synapse/admin/v1/server_version`; + fetchUtils + .fetchJson(versionUrl, { method: "GET" }) + .then(({ json }) => { + setServerVersion( + `${translate("synapseadmin.auth.server_version")} ${ + json["server_version"] + }` + ); + }) + .catch(_ => { + setServerVersion(""); + }); + }, + [formData.base_url] + ); + return (
@@ -189,6 +221,7 @@ const LoginPage = ({ theme }) => { fullWidth />
+
{serverVersion}
); }; diff --git a/src/components/rooms.js b/src/components/rooms.js index 8cac414..4a8a4c9 100644 --- a/src/components/rooms.js +++ b/src/components/rooms.js @@ -1,17 +1,224 @@ import React from "react"; -import { Datagrid, List, TextField, Pagination } from "react-admin"; +import { connect } from "react-redux"; +import { + BooleanField, + Datagrid, + Filter, + List, + Pagination, + SelectField, + Show, + Tab, + TabbedShowLayout, + TextField, + useTranslate, +} from "react-admin"; +import get from "lodash/get"; +import { Tooltip, Typography, Chip } from "@material-ui/core"; +import HttpsIcon from "@material-ui/icons/Https"; +import NoEncryptionIcon from "@material-ui/icons/NoEncryption"; +import PageviewIcon from "@material-ui/icons/Pageview"; +import ViewListIcon from "@material-ui/icons/ViewList"; +import VisibilityIcon from "@material-ui/icons/Visibility"; const RoomPagination = props => ( ); -export const RoomList = props => ( - }> - - - - - - - -); +const EncryptionField = ({ source, record = {}, emptyText }) => { + const translate = useTranslate(); + const value = get(record, source); + let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true"; + + if (value === false || value === true) { + return ( + + + {value === true ? ( + + ) : ( + + )} + + + ); + } + + return ( + + {emptyText} + + ); +}; + +const RoomTitle = ({ record }) => { + const translate = useTranslate(); + var name = ""; + if (record) { + name = record.name !== "" ? record.name : record.id; + } + + return ( + + {translate("resources.rooms.name", 1)} {name} + + ); +}; + +export const RoomShow = props => { + const translate = useTranslate(); + return ( + }> + + }> + + + + + + + } + path="detail" + > + + + + + + + + } + path="permission" + > + + + + + + + + + ); +}; +const RoomFilter = ({ ...props }) => { + const translate = useTranslate(); + return ( + + + + + + + ); +}; + +const FilterableRoomList = ({ ...props }) => { + const filter = props.roomFilters; + const localMembersFilter = + filter && filter.joined_local_members ? true : false; + const stateEventsFilter = filter && filter.state_events ? true : false; + const versionFilter = filter && filter.version ? true : false; + const federateableFilter = filter && filter.federatable ? true : false; + + return ( + } + sort={{ field: "name", order: "ASC" }} + filters={} + > + + } + /> + + + {localMembersFilter && } + {stateEventsFilter && } + {versionFilter && } + {federateableFilter && } + + + + ); +}; + +function mapStateToProps(state) { + return { + roomFilters: state.admin.resources.rooms.list.params.displayedFilters, + }; +} + +export const RoomList = connect(mapStateToProps)(FilterableRoomList); diff --git a/src/components/users.js b/src/components/users.js index 57cdec2..42a3263 100644 --- a/src/components/users.js +++ b/src/components/users.js @@ -1,5 +1,7 @@ import React, { cloneElement, Fragment } from "react"; +import Avatar from "@material-ui/core/Avatar"; import PersonPinIcon from "@material-ui/icons/PersonPin"; +import ContactMailIcon from "@material-ui/icons/ContactMail"; import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent"; import { ArrayInput, @@ -17,7 +19,6 @@ import { FormTab, BooleanField, BooleanInput, - ImageField, PasswordInput, TextField, TextInput, @@ -35,6 +36,19 @@ import { sanitizeListRestProps, } from "react-admin"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles({ + small: { + height: "40px", + width: "40px", + }, + large: { + height: "120px", + width: "120px", + float: "right", + }, +}); const ListActions = ({ currentSort, @@ -104,40 +118,32 @@ const UserBulkActionButtons = props => { ); }; -export const UserList = props => ( - } - filterDefaultValues={{ guests: true, deactivated: false }} - actions={} - bulkActionButtons={} - pagination={} - > - - - - - - {/* Hack since the users endpoint does not give displaynames in the list*/} - - - - - - - - -); +export const UserList = props => { + const classes = useStyles(); + return ( + } + filterDefaultValues={{ guests: true, deactivated: false }} + actions={} + bulkActionButtons={} + pagination={} + > + + + + + + + + + + ); +}; // https://matrix.org/docs/spec/appendices#user-identifiers const validateUser = regex( @@ -186,69 +192,104 @@ const UserTitle = ({ record }) => { const translate = useTranslate(); return ( - {translate("resources.users.name")}{" "} + {translate("resources.users.name", { + smart_count: 1, + })}{" "} {record ? `"${record.displayname}"` : ""} ); }; -export const UserEdit = props => ( - }> - }> - }> - - - - - - - - - - - - - } - > - - { + const classes = useStyles(); + return ( + }> + }> + }> + + + + + + + + + + } + path="threepid" + > + + + + + + + + } + path="connections" + > + - - - - - - - - - - -); + + + + + + + + + + + + ); +}; diff --git a/src/i18n/de.js b/src/i18n/de.js index a467c1a..560e310 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -6,6 +6,7 @@ export default { auth: { base_url: "Heimserver URL", welcome: "Willkommen bei Synapse-admin", + server_version: "Synapse Version", username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'", protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen", url_error: "Keine gültige Matrix Server URL", @@ -14,6 +15,15 @@ export default { invalid_user_id: "Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver", }, + rooms: { + details: "Raumdetails", + tabs: { + basic: "Allgemein", + members: "Mitglieder", + detail: "Details", + permission: "Berechtigungen", + }, + }, }, resources: { users: { @@ -21,6 +31,7 @@ export default { name: "Benutzer", email: "E-Mail", msisdn: "Telefon", + threepid: "E-Mail / Telefon", fields: { avatar: "Avatar", id: "Benutzer-ID", @@ -34,9 +45,12 @@ export default { displayname: "Anzeigename", password: "Passwort", avatar_url: "Avatar URL", + avatar_src: "Avatar", medium: "Medium", threepids: "3PIDs", address: "Adresse", + creation_ts_ms: "Zeitpunkt der Erstellung", + consent_version: "Zugestimmte Geschäftsbedingungen", }, helper: { deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.", @@ -53,6 +67,36 @@ export default { name: "Name", canonical_alias: "Alias", joined_members: "Mitglieder", + joined_local_members: "Lokale Mitglieder", + state_events: "Ereignisse", + version: "Version", + is_encrypted: "Verschlüsselt", + encryption: "Verschlüsselungs-Algorithmus", + federatable: "Fö­de­rierbar", + public: "Öffentlich", + creator: "Ersteller", + join_rules: "Beitrittsregeln", + guest_access: "Gastzugriff", + history_visibility: "Historie-Sichtbarkeit", + }, + enums: { + join_rules: { + public: "Öffentlich", + knock: "Auf Anfrage", + invite: "Nur auf Einladung", + private: "Privat", + }, + guest_access: { + can_join: "Gäste können beitreten", + forbidden: "Gäste können nicht beitreten", + }, + history_visibility: { + invited: "Ab Einladung", + joined: "Ab Beitritt", + shared: "Ab Setzen der Einstellung", + world_readable: "Jeder", + }, + unencrypted: "Nicht verschlüsselt", }, }, connections: { diff --git a/src/i18n/en.js b/src/i18n/en.js index a7a6141..12ba1ae 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -14,6 +14,14 @@ export default { invalid_user_id: "Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver", }, + rooms: { + tabs: { + basic: "Basic", + members: "Members", + detail: "Details", + permission: "Permissions", + }, + }, }, resources: { users: { @@ -21,6 +29,7 @@ export default { name: "User |||| Users", email: "Email", msisdn: "Phone", + threepid: "Email / Phone", fields: { avatar: "Avatar", id: "User-ID", @@ -34,9 +43,12 @@ export default { displayname: "Displayname", password: "Password", avatar_url: "Avatar URL", + avatar_src: "Avatar", medium: "Medium", threepids: "3PIDs", address: "Address", + creation_ts_ms: "Creation timestamp", + consent_version: "Consent version", }, helper: { deactivate: "Deactivated users cannot be reactivated", @@ -53,6 +65,36 @@ export default { name: "Name", canonical_alias: "Alias", joined_members: "Members", + joined_local_members: "local members", + state_events: "State events", + version: "Version", + is_encrypted: "Encrypted", + encryption: "Encryption", + federatable: "Federatable", + public: "Public", + creator: "Creator", + join_rules: "Join rules", + guest_access: "Guest access", + history_visibility: "History visibility", + }, + enums: { + join_rules: { + public: "Public", + knock: "Knock", + invite: "Invite", + private: "Private", + }, + guest_access: { + can_join: "Guests can join", + forbidden: "Guests can not join", + }, + history_visibility: { + invited: "Since invited", + joined: "Since joined", + shared: "Since shared", + world_readable: "Anyone", + }, + unencrypted: "Unencrypted", }, }, connections: { diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index 6d5486c..cebc413 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -14,22 +14,32 @@ const jsonClient = (url, options = {}) => { return fetchUtils.fetchJson(url, options); }; +const mxcUrlToHttp = mxcUrl => { + const homeserver = localStorage.getItem("base_url"); + const re = /^mxc:\/\/([^/]+)\/(\w+)/; + var ret = re.exec(mxcUrl); + console.log("mxcClient " + ret); + if (ret == null) return null; + const serverName = ret[1]; + const mediaId = ret[2]; + return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`; +}; + const resourceMap = { users: { path: "/_synapse/admin/v2/users", map: u => ({ ...u, id: u.name, + avatar_src: mxcUrlToHttp(u.avatar_url), is_guest: !!u.is_guest, admin: !!u.admin, deactivated: !!u.deactivated, + // need timestamp in milliseconds + creation_ts_ms: u.creation_ts * 1000, }), data: "users", - total: (json, from, perPage) => { - return json.next_token - ? parseInt(json.next_token, 10) + perPage - : from + json.users.length; - }, + total: json => json.total, create: data => ({ endpoint: `/_synapse/admin/v2/users/${data.id}`, body: data, @@ -48,6 +58,9 @@ const resourceMap = { id: r.room_id, alias: r.canonical_alias, members: r.joined_members, + is_encrypted: !!r.encryption, + federatable: !!r.federatable, + public: !!r.public, }), data: "rooms", total: json => { @@ -86,11 +99,20 @@ function filterNullValues(key, value) { return value; } +function getSearchOrder(order) { + if (order === "DESC") { + return "b"; + } else { + return "f"; + } +} + const dataProvider = { getList: (resource, params) => { console.log("getList " + resource); const { user_id, guests, deactivated } = params.filter; const { page, perPage } = params.pagination; + const { field, order } = params.sort; const from = (page - 1) * perPage; const query = { from: from, @@ -98,6 +120,8 @@ const dataProvider = { user_id: user_id, guests: guests, deactivated: deactivated, + order_by: field, + dir: getSearchOrder(order), }; const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) return Promise.reject(); diff --git a/yarn.lock b/yarn.lock index c5be95c..72f4311 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11444,9 +11444,9 @@ websocket-driver@>=0.5.1: websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: version "1.0.5"