14 Commits
0.3.0 ... 0.5.0

Author SHA1 Message Date
Manuel Stahl
8db881a64d Use current node image as builder
Change-Id: I2bccf5bcd21e37625477695ee927d49dd4333ac4
2020-08-25 17:19:17 +02:00
Manuel Stahl
c09e59d47a yarn: Upgrade packages
- babel: 7.11

Change-Id: I6eebc1afbf54b6ce72e1b534f240e3e0aeb1c30a
2020-08-25 17:19:17 +02:00
Dirk Klimpel
8bdf76f27e Bugfix plural in user tab (#62) 2020-08-11 10:25:05 +02:00
Michael Albert
7c9a87bc86 Show room members in room detail view
Change-Id: I7d82f728de0e503d4834f6815bde000c7a0204a3
2020-08-07 21:29:58 +02:00
Manuel Stahl
1f56bac356 Add basic tests for dataProvider
Change-Id: Ib399cbb4e927ab18f714371e07606df83170df52
2020-07-30 13:52:17 +02:00
Manuel Stahl
bbbca0c57c Add git context to docker, so we can derive the release version
"git describe --tags" requires the git context.

Change-Id: I2bc5dde056c2ac480513004fb99336397355af30
2020-07-29 17:49:30 +02:00
Manuel Stahl
314906657f Add support to remove user devices (#57)
Change-Id: I19176daa656b9280ccd00f1ca0095e72870ca21e
2020-07-29 17:49:30 +02:00
Manuel Stahl
78e7c5f391 Fix translation of user devices
Fixes #58.

Change-Id: Ic2f91917310fd1ba59636d06c81c338ca9dd297e
2020-07-23 09:24:18 +02:00
Michael Albert
1074178e31 Rename Admin -> Server Administrator
Change-Id: Ic11539252af553dbb7cca37996f2402669a9c0e4
2020-07-21 11:52:13 +02:00
Manuel Stahl
352ab1290a Add devices tab to UserEdit component
Allows to view user devices.
API was added by synapse v1.15.0.

Change-Id: Id0693bf6cd6f6182c657412cf8036537e2db9df7
2020-07-13 10:32:13 +02:00
Manuel Stahl
12447b7708 Increase export of users up to 10000
Change-Id: I54c7a52ae35aeb311074f0f3b103a2fdb92aaedd
2020-07-13 10:20:24 +02:00
Manuel Stahl
cd4efb7c07 yarn: Upgrade packages
- react-admin:  3.7.0

Change-Id: I01e4a2244a6ca0d6fc231fe83a94f73d48618824
2020-07-13 10:20:03 +02:00
Michael Albert
ff59ee4c2e Hide some room list information by default
Change-Id: Ic6fbf0d941d2ffcc87fb5b7793517b96792ce16d
2020-07-08 11:19:09 +02:00
Manuel Stahl
61938405e9 Add room detail view
Needs Synapse v1.14.0 or later

Change-Id: I6e3956a1e02fad5ba2f847458cd184af6aaedef0
2020-07-08 10:46:49 +02:00
14 changed files with 2277 additions and 1655 deletions

View File

@@ -1,5 +1,4 @@
# Exclude a bunch of stuff which can make the build context a larger than it needs to be # Exclude a bunch of stuff which can make the build context a larger than it needs to be
.git/
tests/ tests/
build/ build/
lib/ lib/

View File

@@ -1,5 +1,5 @@
# Builder # Builder
FROM node:10-alpine as builder FROM node:current as builder
WORKDIR /src WORKDIR /src

View File

@@ -4,7 +4,7 @@
This project is built using [react-admin](https://marmelab.com/react-admin/). This project is built using [react-admin](https://marmelab.com/react-admin/).
It needs at least Synapse v1.13.0 for all functions to work as expected! It needs at least Synapse v1.18.0 for all functions to work as expected!
## Step-By-Step install: ## Step-By-Step install:
@@ -29,3 +29,8 @@ Steps for 2):
## Screenshots ## Screenshots
![Screenshots](./screenshots.jpg) ![Screenshots](./screenshots.jpg)
## Development
- Use `yarn test` to run all style, lint and unit tests
- Use `yarn fix` to fix the coding style

View File

@@ -12,19 +12,20 @@
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.1.1", "@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^10.0.2", "@testing-library/react": "^10.0.2",
"@testing-library/user-event": "^10.0.1", "@testing-library/user-event": "^12.0.11",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-react-16": "^1.15.2",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1", "eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.2",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.0.0" "prettier": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"ra-language-german": "^2.1.2", "ra-language-german": "^2.1.2",
"react": "^16.13.1", "react": "^16.13.1",
"react-admin": "^3.4.0", "react-admin": "^3.7.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-scripts": "^3.4.1" "react-scripts": "^3.4.1"
}, },

View File

@@ -4,7 +4,7 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider"; import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider"; import dataProvider from "./synapse/dataProvider";
import { UserList, UserCreate, UserEdit } from "./components/users"; import { UserList, UserCreate, UserEdit } from "./components/users";
import { RoomList } from "./components/rooms"; import { RoomList, RoomShow } from "./components/rooms";
import LoginPage from "./components/LoginPage"; import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group"; import UserIcon from "@material-ui/icons/Group";
import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList"; import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList";
@@ -35,8 +35,10 @@ const App = () => (
edit={UserEdit} edit={UserEdit}
icon={UserIcon} icon={UserIcon}
/> />
<Resource name="rooms" list={RoomList} icon={RoomIcon} /> <Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} />
<Resource name="connections" /> <Resource name="connections" />
<Resource name="devices" />
<Resource name="room_members" />
<Resource name="servernotices" /> <Resource name="servernotices" />
</Admin> </Admin>
); );

89
src/components/devices.js Normal file
View File

@@ -0,0 +1,89 @@
import React, { Fragment, useState } from "react";
import {
Button,
useMutation,
useNotify,
Confirm,
useRefresh,
} from "react-admin";
import ActionDelete from "@material-ui/icons/Delete";
import { makeStyles } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator";
import classnames from "classnames";
const useStyles = makeStyles(
theme => ({
deleteButton: {
color: theme.palette.error.main,
"&:hover": {
backgroundColor: fade(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
},
}),
{ name: "RaDeleteDeviceButton" }
);
export const DeviceRemoveButton = props => {
const { record } = props;
const classes = useStyles(props);
const [open, setOpen] = useState(false);
const refresh = useRefresh();
const notify = useNotify();
const [removeDevice, { loading }] = useMutation();
if (!record) return null;
const handleClick = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleConfirm = () => {
removeDevice(
{
type: "delete",
resource: "devices",
payload: {
id: record.id,
user_id: record.user_id,
},
},
{
onSuccess: () => {
notify("resources.devices.action.erase.success");
refresh();
},
onFailure: () =>
notify("resources.devices.action.erase.failure", "error"),
}
);
setOpen(false);
};
return (
<Fragment>
<Button
label="ra.action.remove"
onClick={handleClick}
className={classnames("ra-delete-button", classes.deleteButton)}
>
<ActionDelete />
</Button>
<Confirm
isOpen={open}
loading={loading}
onConfirm={handleConfirm}
onClose={handleDialogClose}
title="resources.devices.action.erase.title"
content="resources.devices.action.erase.content"
translateOptions={{
id: record.id,
name: record.display_name ? record.display_name : record.id,
}}
/>
</Fragment>
);
};

View File

@@ -1,16 +1,28 @@
import React from "react"; import React from "react";
import { connect } from "react-redux";
import { import {
Datagrid,
List,
TextField,
Pagination,
BooleanField, BooleanField,
Datagrid,
Filter,
List,
Pagination,
ReferenceField,
ReferenceManyField,
SelectField,
Show,
Tab,
TabbedShowLayout,
TextField,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import get from "lodash/get"; import get from "lodash/get";
import { Tooltip, Typography } from "@material-ui/core"; import { Tooltip, Typography, Chip } from "@material-ui/core";
import HttpsIcon from "@material-ui/icons/Https"; import HttpsIcon from "@material-ui/icons/Https";
import NoEncryptionIcon from "@material-ui/icons/NoEncryption"; import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
import PageviewIcon from "@material-ui/icons/Pageview";
import UserIcon from "@material-ui/icons/Group";
import ViewListIcon from "@material-ui/icons/ViewList";
import VisibilityIcon from "@material-ui/icons/Visibility";
const RoomPagination = props => ( const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
@@ -42,27 +54,202 @@ const EncryptionField = ({ source, record = {}, emptyText }) => {
); );
}; };
export const RoomList = props => ( const RoomTitle = ({ record }) => {
<List const translate = useTranslate();
{...props} var name = "";
pagination={<RoomPagination />} if (record) {
sort={{ field: "name", order: "ASC" }} name = record.name !== "" ? record.name : record.id;
> }
<Datagrid>
<EncryptionField return (
source="is_encrypted" <span>
sortBy="encryption" {translate("resources.rooms.name", 1)} {name}
label={<HttpsIcon />} </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.members" icon={<UserIcon />}>
<ReferenceManyField
reference="room_members"
target="room_id"
addLabel={false}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) => "/users/" + id}
>
<TextField
source="id"
sortable={false}
label="resources.users.fields.id"
/>
<ReferenceField
label="resources.users.fields.displayname"
source="id"
reference="users"
sortable={false}
link=""
>
<TextField source="displayname" sortable={false} />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</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 }}
/> />
<TextField source="room_id" sortable={false} /> <Chip
<TextField source="name" /> label={translate("resources.rooms.fields.state_events")}
<TextField source="canonical_alias" /> source="state_events"
<TextField source="joined_members" /> defaultValue={false}
<TextField source="joined_local_members" /> style={{ marginBottom: 8 }}
<TextField source="state_events" /> />
<TextField source="version" /> <Chip
<BooleanField source="federatable" /> label={translate("resources.rooms.fields.version")}
<BooleanField source="public" /> source="version"
</Datagrid> defaultValue={false}
</List> 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,7 +1,8 @@
import React, { Fragment } from "react"; import React, { cloneElement, Fragment } from "react";
import Avatar from "@material-ui/core/Avatar"; 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 SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent"; import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import { import {
ArrayInput, ArrayInput,
@@ -23,6 +24,7 @@ import {
TextField, TextField,
TextInput, TextInput,
ReferenceField, ReferenceField,
ReferenceManyField,
SelectInput, SelectInput,
BulkDeleteButton, BulkDeleteButton,
DeleteButton, DeleteButton,
@@ -30,8 +32,13 @@ import {
regex, regex,
useTranslate, useTranslate,
Pagination, Pagination,
CreateButton,
ExportButton,
TopToolbar,
sanitizeListRestProps,
} from "react-admin"; } from "react-admin";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles({ const useStyles = makeStyles({
@@ -46,6 +53,45 @@ const useStyles = makeStyles({
}, },
}); });
const UserListActions = ({
currentSort,
className,
resource,
filters,
displayedFilters,
exporter, // you can hide ExportButton if exporter = (null || false)
filterValues,
permanentFilter,
hasCreate, // you can hide CreateButton if hasCreate = false
basePath,
selectedIds,
onUnselectItems,
showFilter,
maxResults,
total,
...rest
}) => (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters &&
cloneElement(filters, {
resource,
showFilter,
displayedFilters,
filterValues,
context: "button",
})}
<CreateButton basePath={basePath} />
<ExportButton
disabled={total === 0}
resource={resource}
sort={currentSort}
filter={{ ...filterValues, ...permanentFilter }}
exporter={exporter}
maxResults={maxResults}
/>
</TopToolbar>
);
const UserPagination = props => ( const UserPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
@@ -86,6 +132,7 @@ export const UserList = props => {
{...props} {...props}
filters={<UserFilter />} filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }} filterDefaultValues={{ guests: true, deactivated: false }}
actions={<UserListActions maxResults={10000} />}
bulkActionButtons={<UserBulkActionButtons />} bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />} pagination={<UserPagination />}
> >
@@ -161,10 +208,14 @@ const UserTitle = ({ record }) => {
}; };
export const UserEdit = props => { export const UserEdit = props => {
const classes = useStyles(); const classes = useStyles();
const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />}> <Edit {...props} title={<UserTitle />}>
<TabbedForm toolbar={<UserEditToolbar />}> <TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label="resources.users.name" icon={<PersonPinIcon />}> <FormTab
label={translate("resources.users.name", { smart_count: 1 })}
icon={<PersonPinIcon />}
>
<AvatarField <AvatarField
source="avatar_src" source="avatar_src"
sortable={false} sortable={false}
@@ -210,6 +261,37 @@ export const UserEdit = props => {
</SimpleFormIterator> </SimpleFormIterator>
</ArrayInput> </ArrayInput>
</FormTab> </FormTab>
<FormTab
label={translate("resources.devices.name", { smart_count: 2 })}
icon={<DevicesIcon />}
path="devices"
>
<ReferenceManyField
reference="devices"
target="user_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }}>
<TextField source="device_id" sortable={false} />
<TextField source="display_name" sortable={false} />
<TextField source="last_seen_ip" sortable={false} />
<DateField
source="last_seen_ts"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<DeviceRemoveButton />
</Datagrid>
</ReferenceManyField>
</FormTab>
<FormTab <FormTab
label="resources.connections.name" label="resources.connections.name"
icon={<SettingsInputComponentIcon />} icon={<SettingsInputComponentIcon />}

View File

@@ -15,6 +15,15 @@ export default {
invalid_user_id: invalid_user_id:
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver", "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: { resources: {
users: { users: {
@@ -28,7 +37,7 @@ export default {
id: "Benutzer-ID", id: "Benutzer-ID",
name: "Name", name: "Name",
is_guest: "Gast", is_guest: "Gast",
admin: "Admin", admin: "Server Administrator",
deactivated: "Deaktiviert", deactivated: "Deaktiviert",
guests: "Zeige Gäste", guests: "Zeige Gäste",
show_deactivated: "Zeige deaktivierte Benutzer", show_deactivated: "Zeige deaktivierte Benutzer",
@@ -42,6 +51,11 @@ export default {
address: "Adresse", address: "Adresse",
creation_ts_ms: "Zeitpunkt der Erstellung", creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen", consent_version: "Zugestimmte Geschäftsbedingungen",
// Devices:
device_id: "Geräte-ID",
display_name: "Gerätename",
last_seen_ts: "Zeitstempel",
last_seen_ip: "IP-Adresse",
}, },
helper: { helper: {
deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.", deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.",
@@ -58,12 +72,36 @@ export default {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Mitglieder", joined_members: "Mitglieder",
joined_local_members: "lokale Mitglieder", joined_local_members: "Lokale Mitglieder",
state_events: "Ereignisse", state_events: "Ereignisse",
version: "Version", version: "Version",
is_encrypted: "Verschlüsselt", is_encrypted: "Verschlüsselt",
federatable: "Fö­de­riert", encryption: "Verschlüsselungs-Algorithmus",
federatable: "Fö­de­rierbar",
public: "Öffentlich", 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: { connections: {
@@ -74,6 +112,17 @@ export default {
user_agent: "User Agent", user_agent: "User Agent",
}, },
}, },
devices: {
name: "Gerät |||| Geräte",
action: {
erase: {
title: "Entferne %{id}",
content: 'Möchten Sie das Gerät "%{name}" wirklich entfernen?',
success: "Gerät erfolgreich entfernt.",
failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
},
},
servernotices: { servernotices: {
name: "Serverbenachrichtigungen", name: "Serverbenachrichtigungen",
send: "Servernachricht versenden", send: "Servernachricht versenden",

View File

@@ -14,6 +14,14 @@ export default {
invalid_user_id: invalid_user_id:
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver", "Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver",
}, },
rooms: {
tabs: {
basic: "Basic",
members: "Members",
detail: "Details",
permission: "Permissions",
},
},
}, },
resources: { resources: {
users: { users: {
@@ -27,7 +35,7 @@ export default {
id: "User-ID", id: "User-ID",
name: "Name", name: "Name",
is_guest: "Guest", is_guest: "Guest",
admin: "Admin", admin: "Server Administrator",
deactivated: "Deactivated", deactivated: "Deactivated",
guests: "Show guests", guests: "Show guests",
show_deactivated: "Show deactivated users", show_deactivated: "Show deactivated users",
@@ -41,6 +49,11 @@ export default {
address: "Address", address: "Address",
creation_ts_ms: "Creation timestamp", creation_ts_ms: "Creation timestamp",
consent_version: "Consent version", consent_version: "Consent version",
// Devices:
device_id: "Device-ID",
display_name: "Device name",
last_seen_ts: "Timestamp",
last_seen_ip: "IP address",
}, },
helper: { helper: {
deactivate: "Deactivated users cannot be reactivated", deactivate: "Deactivated users cannot be reactivated",
@@ -61,8 +74,32 @@ export default {
state_events: "State events", state_events: "State events",
version: "Version", version: "Version",
is_encrypted: "Encrypted", is_encrypted: "Encrypted",
encryption: "Encryption",
federatable: "Federatable", federatable: "Federatable",
public: "Public", 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: { connections: {
@@ -73,6 +110,17 @@ export default {
user_agent: "User agent", user_agent: "User agent",
}, },
}, },
devices: {
name: "Device |||| Devices",
action: {
erase: {
title: "Removing %{id}",
content: 'Are you sure you want to remove the device "%{name}"?',
success: "Device successfully removed.",
failure: "An error has occurred.",
},
},
},
servernotices: { servernotices: {
name: "Server Notices", name: "Server Notices",
send: "Send server notices", send: "Send server notices",

View File

@@ -1,4 +1,6 @@
import { configure } from "enzyme"; import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16"; import Adapter from "enzyme-adapter-react-16";
import fetchMock from "jest-fetch-mock";
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });
fetchMock.enableMocks();

View File

@@ -45,8 +45,8 @@ const resourceMap = {
body: data, body: data,
method: "PUT", method: "PUT",
}), }),
delete: id => ({ delete: params => ({
endpoint: `/_synapse/admin/v1/deactivate/${id}`, endpoint: `/_synapse/admin/v1/deactivate/${params.id}`,
body: { erase: true }, body: { erase: true },
method: "POST", method: "POST",
}), }),
@@ -67,6 +67,19 @@ const resourceMap = {
return json.total_rooms; return json.total_rooms;
}, },
}, },
devices: {
map: d => ({
...d,
id: d.device_id,
}),
data: "devices",
reference: id => ({
endpoint: `/_synapse/admin/v2/users/${id}/devices`,
}),
delete: params => ({
endpoint: `/_synapse/admin/v2/users/${params.user_id}/devices/${params.id}`,
}),
},
connections: { connections: {
path: "/_synapse/admin/v1/whois", path: "/_synapse/admin/v1/whois",
map: c => ({ map: c => ({
@@ -75,6 +88,15 @@ const resourceMap = {
}), }),
data: "connections", data: "connections",
}, },
room_members: {
map: m => ({
id: m,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/rooms/${id}/members`,
}),
data: "members",
},
servernotices: { servernotices: {
map: n => ({ id: n.event_id }), map: n => ({ id: n.event_id }),
create: data => ({ create: data => ({
@@ -166,30 +188,18 @@ const dataProvider = {
}, },
getManyReference: (resource, params) => { getManyReference: (resource, params) => {
// FIXME
console.log("getManyReference " + resource); console.log("getManyReference " + resource);
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
filter: JSON.stringify({
...params.filter,
[params.target]: params.id,
}),
};
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const ref = res["reference"](params.id);
const url = `${endpoint_url}?${stringify(query)}`; const endpoint_url = homeserver + ref.endpoint;
return jsonClient(url).then(({ headers, json }) => ({ return jsonClient(endpoint_url).then(({ headers, json }) => ({
data: json, data: json[res.data].map(res.map),
total: parseInt(headers.get("content-range").split("/").pop(), 10),
})); }));
}, },
@@ -276,11 +286,11 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
if ("delete" in res) { if ("delete" in res) {
const del = res["delete"](params.id); const del = res["delete"](params);
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
return jsonClient(endpoint_url, { return jsonClient(endpoint_url, {
method: del.method, method: "method" in del ? del.method : "DELETE",
body: JSON.stringify(del.body), body: "body" in del ? JSON.stringify(del.body) : null,
}).then(({ json }) => ({ }).then(({ json }) => ({
data: json, data: json,
})); }));
@@ -305,11 +315,11 @@ const dataProvider = {
if ("delete" in res) { if ("delete" in res) {
return Promise.all( return Promise.all(
params.ids.map(id => { params.ids.map(id => {
const del = res["delete"](id); const del = res["delete"]({ ...params, id: id });
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
return jsonClient(endpoint_url, { return jsonClient(endpoint_url, {
method: del.method, method: "method" in del ? del.method : "DELETE",
body: JSON.stringify(del.body), body: "body" in del ? JSON.stringify(del.body) : null,
}); });
}) })
).then(responses => ({ ).then(responses => ({

View File

@@ -0,0 +1,78 @@
import dataProvider from "./dataProvider";
beforeEach(() => {
fetch.resetMocks();
});
describe("dataProvider", () => {
localStorage.setItem("base_url", "http://localhost");
localStorage.setItem("access_token", "access_token");
it("fetches all users", async () => {
fetch.mockResponseOnce(
JSON.stringify({
users: [
{
name: "user_id1",
password_hash: "password_hash1",
is_guest: 0,
admin: 0,
user_type: null,
deactivated: 0,
displayname: "User One",
},
{
name: "user_id2",
password_hash: "password_hash2",
is_guest: 0,
admin: 1,
user_type: null,
deactivated: 0,
displayname: "User Two",
},
],
next_token: "100",
total: 200,
})
);
const users = await dataProvider.getList("users", {
pagination: { page: 1, perPage: 5 },
sort: { field: "title", order: "ASC" },
filter: { author_id: 12 },
});
expect(users["data"][0]["id"]).toEqual("user_id1");
expect(users["total"]).toEqual(200);
expect(fetch).toHaveBeenCalledTimes(1);
});
it("fetches one user", async () => {
fetch.mockResponseOnce(
JSON.stringify({
name: "user_id1",
password: "user_password",
displayname: "User",
threepids: [
{
medium: "email",
address: "user@mail_1.com",
},
{
medium: "email",
address: "user@mail_2.com",
},
],
avatar_url: "mxc://localhost/user1",
admin: false,
deactivated: false,
})
);
const user = await dataProvider.getOne("users", { id: "user_id1" });
expect(user["data"]["id"]).toEqual("user_id1");
expect(user["data"]["displayname"]).toEqual("User");
expect(fetch).toHaveBeenCalledTimes(1);
});
});

3250
yarn.lock

File diff suppressed because it is too large Load Diff