Merge branch 'master' into increase_export

This commit is contained in:
Michael Albert 2020-07-10 15:26:14 +02:00 committed by GitHub
commit 5dfeb0abe2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 545 additions and 125 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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`.
-->
<footer
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
Synapse-Admin <b>(%REACT_APP_VERSION%)</b> by Awesome Technologies Innovationslabor GmbH
</a>
</footer>
</body>
</html>

BIN
screenshots.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

@ -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}
/>
<Resource name="rooms" list={RoomList} icon={RoomIcon} />
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} />
<Resource name="connections" />
<Resource name="servernotices" />
</Admin>

View File

@ -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 (
<div>
<div className={classes.input}>
@ -189,6 +221,7 @@ const LoginPage = ({ theme }) => {
fullWidth
/>
</div>
<div className={classes.serverVersion}>{serverVersion}</div>
</div>
);
};

View File

@ -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 => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
export const RoomList = props => (
<List {...props} pagination={<RoomPagination />}>
<Datagrid>
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 (
<Typography component="span" variant="body2">
<Tooltip title={translate(ariaLabel, { _: ariaLabel })}>
{value === true ? (
<HttpsIcon data-testid="true" htmlColor="limegreen" />
) : (
<NoEncryptionIcon data-testid="false" color="error" />
)}
</Tooltip>
</Typography>
);
}
return (
<Typography component="span" variant="body2">
{emptyText}
</Typography>
);
};
const RoomTitle = ({ record }) => {
const translate = useTranslate();
var name = "";
if (record) {
name = record.name !== "" ? record.name : record.id;
}
return (
<span>
{translate("resources.rooms.name", 1)} {name}
</span>
);
};
export const RoomShow = props => {
const translate = useTranslate();
return (
<Show {...props} title={<RoomTitle />}>
<TabbedShowLayout>
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
<TextField source="room_id" />
<TextField source="name" />
<TextField source="canonical_alias" />
<TextField source="creator" />
</Tab>
<Tab
label="synapseadmin.rooms.tabs.detail"
icon={<PageviewIcon />}
path="detail"
>
<TextField source="joined_members" />
<TextField source="joined_local_members" />
<TextField source="state_events" />
<TextField source="version" />
<TextField
source="encryption"
emptyText={translate("resources.rooms.enums.unencrypted")}
/>
</Tab>
<Tab
label="synapseadmin.rooms.tabs.permission"
icon={<VisibilityIcon />}
path="permission"
>
<BooleanField source="federatable" />
<BooleanField source="public" />
<SelectField
source="join_rules"
choices={[
{ id: "public", name: "resources.rooms.enums.join_rules.public" },
{ id: "knock", name: "resources.rooms.enums.join_rules.knock" },
{ id: "invite", name: "resources.rooms.enums.join_rules.invite" },
{
id: "private",
name: "resources.rooms.enums.join_rules.private",
},
]}
/>
<SelectField
source="guest_access"
choices={[
{
id: "can_join",
name: "resources.rooms.enums.guest_access.can_join",
},
{
id: "forbidden",
name: "resources.rooms.enums.guest_access.forbidden",
},
]}
/>
<SelectField
source="history_visibility"
choices={[
{
id: "invited",
name: "resources.rooms.enums.history_visibility.invited",
},
{
id: "joined",
name: "resources.rooms.enums.history_visibility.joined",
},
{
id: "shared",
name: "resources.rooms.enums.history_visibility.shared",
},
{
id: "world_readable",
name: "resources.rooms.enums.history_visibility.world_readable",
},
]}
/>
</Tab>
</TabbedShowLayout>
</Show>
);
};
const RoomFilter = ({ ...props }) => {
const translate = useTranslate();
return (
<Filter {...props}>
<Chip
label={translate("resources.rooms.fields.joined_local_members")}
source="joined_local_members"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.state_events")}
source="state_events"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.version")}
source="version"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.federatable")}
source="federatable"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
</Filter>
);
};
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 (
<List
{...props}
pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }}
filters={<RoomFilter />}
>
<Datagrid rowClick="show">
<EncryptionField
source="is_encrypted"
sortBy="encryption"
label={<HttpsIcon />}
/>
<TextField source="name" />
<TextField source="joined_members" />
{localMembersFilter && <TextField source="joined_local_members" />}
{stateEventsFilter && <TextField source="state_events" />}
{versionFilter && <TextField source="version" />}
{federateableFilter && <BooleanField source="federatable" />}
<BooleanField source="public" />
</Datagrid>
</List>
);
);
};
function mapStateToProps(state) {
return {
roomFilters: state.admin.resources.rooms.list.params.displayedFilters,
};
}
export const RoomList = connect(mapStateToProps)(FilterableRoomList);

View File

@ -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,7 +118,9 @@ const UserBulkActionButtons = props => {
);
};
export const UserList = props => (
export const UserList = props => {
const classes = useStyles();
return (
<List
{...props}
filters={<UserFilter />}
@ -114,30 +130,20 @@ export const UserList = props => (
pagination={<UserPagination />}
>
<Datagrid rowClick="edit">
<ReferenceField
source="Avatar"
reference="users"
link={false}
<AvatarField
source="avatar_src"
sortable={false}
>
<ImageField source="avatar_url" title="displayname" />
</ReferenceField>
<TextField source="id" />
{/* Hack since the users endpoint does not give displaynames in the list*/}
<ReferenceField
source="name"
reference="users"
link={false}
sortable={false}
>
<TextField source="displayname" />
</ReferenceField>
className={classes.small}
/>
<TextField source="id" sortable={false} />
<TextField source="displayname" sortable={false} />
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} />
<BooleanField source="deactivated" sortable={false} />
</Datagrid>
</List>
);
);
};
// https://matrix.org/docs/spec/appendices#user-identifiers
const validateUser = regex(
@ -186,15 +192,24 @@ const UserTitle = ({ record }) => {
const translate = useTranslate();
return (
<span>
{translate("resources.users.name")}{" "}
{translate("resources.users.name", {
smart_count: 1,
})}{" "}
{record ? `"${record.displayname}"` : ""}
</span>
);
};
export const UserEdit = props => (
export const UserEdit = props => {
const classes = useStyles();
return (
<Edit {...props} title={<UserTitle />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label="resources.users.name" icon={<PersonPinIcon />}>
<AvatarField
source="avatar_src"
sortable={false}
className={classes.large}
/>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
@ -203,6 +218,25 @@ export const UserEdit = props => (
source="deactivated"
helperText="resources.users.helper.deactivate"
/>
<DateField
source="creation_ts_ms"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
/>
<TextField source="consent_version" />
</FormTab>
<FormTab
label="resources.users.threepid"
icon={<ContactMailIcon />}
path="threepid"
>
<ArrayInput source="threepids">
<SimpleFormIterator>
<SelectInput
@ -219,8 +253,14 @@ export const UserEdit = props => (
<FormTab
label="resources.connections.name"
icon={<SettingsInputComponentIcon />}
path="connections"
>
<ReferenceField
reference="connections"
source="id"
addLabel={false}
link={false}
>
<ReferenceField reference="connections" source="id" addLabel={false}>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
@ -251,4 +291,5 @@ export const UserEdit = props => (
</FormTab>
</TabbedForm>
</Edit>
);
);
};

View File

@ -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: {

View File

@ -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: {

View File

@ -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();

View File

@ -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"