Merge branch 'master' into user_devices

This commit is contained in:
Dirk Klimpel 2020-07-08 13:27:24 +02:00 committed by GitHub
commit 9c7cf0ba91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 90 additions and 41 deletions

View File

@ -4,6 +4,28 @@
This project is built using [react-admin](https://marmelab.com/react-admin/). 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.13.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)

BIN
screenshots.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

@ -1,4 +1,5 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import Avatar from "@material-ui/core/Avatar";
import PersonPinIcon from "@material-ui/icons/PersonPin"; import PersonPinIcon from "@material-ui/icons/PersonPin";
import ContactMailIcon from "@material-ui/icons/ContactMail"; import ContactMailIcon from "@material-ui/icons/ContactMail";
import DevicesIcon from "@material-ui/icons/Devices"; import DevicesIcon from "@material-ui/icons/Devices";
@ -19,7 +20,6 @@ import {
FormTab, FormTab,
BooleanField, BooleanField,
BooleanInput, BooleanInput,
ImageField,
PasswordInput, PasswordInput,
TextField, TextField,
TextInput, TextInput,
@ -34,6 +34,19 @@ import {
} from "react-admin"; } from "react-admin";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { RemoveDeviceButton } from "./devices"; import { RemoveDeviceButton } from "./devices";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles({
small: {
height: "40px",
width: "40px",
},
large: {
height: "120px",
width: "120px",
float: "right",
},
});
const UserPagination = props => ( const UserPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
@ -64,40 +77,36 @@ const UserBulkActionButtons = props => {
); );
}; };
export const UserList = props => ( const AvatarField = ({ source, className, record = {} }) => (
<List <Avatar src={record[source]} className={className} />
{...props}
filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit">
<ReferenceField
source="Avatar"
reference="users"
link={false}
sortable={false}
>
<ImageField source="avatar_url" title="displayname" />
</ReferenceField>
<TextField source="id" sortable={false} />
{/* 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>
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} />
<BooleanField source="deactivated" sortable={false} />
</Datagrid>
</List>
); );
export const UserList = props => {
const classes = useStyles();
return (
<List
{...props}
filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit">
<AvatarField
source="avatar_src"
sortable={false}
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 // https://matrix.org/docs/spec/appendices#user-identifiers
const validateUser = regex( const validateUser = regex(
/^@[a-z0-9._=\-/]+:.*/, /^@[a-z0-9._=\-/]+:.*/,
@ -145,12 +154,15 @@ const UserTitle = ({ record }) => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<span> <span>
{translate("resources.users.name")}{" "} {translate("resources.users.name", {
smart_count: 1,
})}{" "}
{record ? `"${record.displayname}"` : ""} {record ? `"${record.displayname}"` : ""}
</span> </span>
); );
}; };
export const UserEdit = props => { export const UserEdit = props => {
const classes = useStyles();
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />}> <Edit {...props} title={<UserTitle />}>
@ -159,6 +171,11 @@ export const UserEdit = props => {
label={translate("resources.users.name", { smart_count: 1 })} label={translate("resources.users.name", { smart_count: 1 })}
icon={<PersonPinIcon />} icon={<PersonPinIcon />}
> >
<AvatarField
source="avatar_src"
sortable={false}
className={classes.large}
/>
<TextInput source="id" disabled /> <TextInput source="id" disabled />
<TextInput source="displayname" /> <TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" /> <PasswordInput source="password" autoComplete="new-password" />

View File

@ -36,6 +36,7 @@ export default {
displayname: "Anzeigename", displayname: "Anzeigename",
password: "Passwort", password: "Passwort",
avatar_url: "Avatar URL", avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium", medium: "Medium",
threepids: "3PIDs", threepids: "3PIDs",
address: "Adresse", address: "Adresse",

View File

@ -35,6 +35,7 @@ export default {
displayname: "Displayname", displayname: "Displayname",
password: "Password", password: "Password",
avatar_url: "Avatar URL", avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium", medium: "Medium",
threepids: "3PIDs", threepids: "3PIDs",
address: "Address", address: "Address",

View File

@ -14,24 +14,32 @@ const jsonClient = (url, options = {}) => {
return fetchUtils.fetchJson(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 = { const resourceMap = {
users: { users: {
path: "/_synapse/admin/v2/users", path: "/_synapse/admin/v2/users",
map: u => ({ map: u => ({
...u, ...u,
id: u.name, id: u.name,
avatar_src: mxcUrlToHttp(u.avatar_url),
is_guest: !!u.is_guest, is_guest: !!u.is_guest,
admin: !!u.admin, admin: !!u.admin,
deactivated: !!u.deactivated, deactivated: !!u.deactivated,
// need timestamp in milliseconds // need timestamp in milliseconds
creation_ts_ms: u.creation_ts * 1000, creation_ts_ms: u.creation_ts * 1000,
}), }),
total: json => json.total,
data: "users", data: "users",
total: (json, from, perPage) => {
return json.next_token
? parseInt(json.next_token, 10) + perPage
: from + json.users.length;
},
getMany: id => ({ getMany: id => ({
endpoint: `/_synapse/admin/v2/users/${id}`, endpoint: `/_synapse/admin/v2/users/${id}`,
}), }),