Add management of devices to EditUser

Allows to view user devices and logout individual devices from
those. Add a new tab to UserEdit (detail view).
This commit is contained in:
dklimpel 2020-07-06 22:15:25 +02:00
parent 8282a3caf8
commit f92173b1cb
6 changed files with 296 additions and 87 deletions

View File

@ -37,6 +37,7 @@ const App = () => (
/>
<Resource name="rooms" list={RoomList} icon={RoomIcon} />
<Resource name="connections" />
<Resource name="devices" />
<Resource name="servernotices" />
</Admin>
);

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

@ -0,0 +1,96 @@
import React, { Fragment, useState } from "react";
import {
Button,
useMutation,
useNotify,
useTranslate,
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";
export const RemoveDeviceButton = props => {
const { record } = props;
const classes = useStyles(props);
const [open, setOpen] = useState(false);
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [removeDevice, { loading }] = useMutation();
const handleSend = values => {
removeDevice(
{
type: "removeDevice",
resource: "devices",
payload: {
user_id: record.user_id,
device_id: record.device_id,
},
},
{
onSuccess: () => {
notify("resources.devices.action.remove_success");
refresh();
},
onFailure: () =>
notify("resources.devices.action.remove_failure", "error"),
}
);
};
const handleClick = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleConfirm = () => {
handleSend();
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}
title="resources.devices.action.remove_title"
content="resources.devices.action.remove_content"
onConfirm={handleConfirm}
onClose={handleDialogClose}
translateOptions={{
name: translate(`resources.devices.name`, {
smart_count: 1,
}),
display_name: translate(`resources.devices.fields.display_name`),
id: record.device_id,
displayname: record.display_name,
}}
/>
</Fragment>
);
};
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" }
);

View File

@ -1,6 +1,7 @@
import React, { Fragment } from "react";
import PersonPinIcon from "@material-ui/icons/PersonPin";
import ContactMailIcon from "@material-ui/icons/ContactMail";
import DevicesIcon from "@material-ui/icons/Devices";
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import {
ArrayInput,
@ -32,6 +33,7 @@ import {
Pagination,
} from "react-admin";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { RemoveDeviceButton } from "./devices";
const UserPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
@ -148,89 +150,129 @@ const UserTitle = ({ record }) => {
</span>
);
};
export const UserEdit = props => (
<Edit {...props} title={<UserTitle />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label="resources.users.name" icon={<PersonPinIcon />}>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<BooleanInput source="admin" />
<BooleanInput
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
source="medium"
choices={[
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
]}
/>
<TextInput source="address" />
</SimpleFormIterator>
</ArrayInput>
</FormTab>
<FormTab
label="resources.connections.name"
icon={<SettingsInputComponentIcon />}
path="connections"
>
<ReferenceField
reference="connections"
source="id"
addLabel={false}
link={false}
export const UserEdit = props => {
const translate = useTranslate();
return (
<Edit {...props} title={<UserTitle />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab
label={translate("resources.users.name", { smart_count: 1 })}
icon={<PersonPinIcon />}
>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<BooleanInput source="admin" />
<BooleanInput
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
source="medium"
choices={[
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
]}
/>
<TextInput source="address" />
</SimpleFormIterator>
</ArrayInput>
</FormTab>
<FormTab
label={translate("resources.devices.name", { smart_count: 2 })}
icon={<DevicesIcon />}
path="devices"
>
<ReferenceField
reference="devices"
source="id"
addLabel={false}
link={false}
>
<Datagrid style={{ width: "100%" }}>
<TextField source="ip" sortable={false} />
<DateField
source="last_seen"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<TextField
source="user_agent"
sortable={false}
style={{ width: "100%" }}
/>
</Datagrid>
</ArrayField>
</ReferenceField>
</FormTab>
</TabbedForm>
</Edit>
);
<ArrayField source="devices" label="resources.devices.name">
<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}
/>
<RemoveDeviceButton />
</Datagrid>
</ArrayField>
</ReferenceField>
</FormTab>
<FormTab
label="resources.connections.name"
icon={<SettingsInputComponentIcon />}
path="connections"
>
<ReferenceField
reference="connections"
source="id"
addLabel={false}
link={false}
>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
>
<Datagrid style={{ width: "100%" }}>
<TextField source="ip" sortable={false} />
<DateField
source="last_seen"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<TextField
source="user_agent"
sortable={false}
style={{ width: "100%" }}
/>
</Datagrid>
</ArrayField>
</ReferenceField>
</FormTab>
</TabbedForm>
</Edit>
);
};

View File

@ -73,6 +73,22 @@ export default {
user_agent: "User Agent",
},
},
devices: {
name: "Gerät |||| Geräte",
fields: {
device_id: "Geräte-ID",
display_name: "Anzeigename",
last_seen_ts: "Zeitstempel",
last_seen_ip: "IP-Adresse",
},
action: {
remove_title: "Entferne %{name}: %{id}",
remove_content:
"Möchten Sie dieses %{name} wirklich entfernen? %{display_name}: %{displayname}",
remove_success: "Gerät erfolgreich entfernt.",
remove_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
},
servernotices: {
name: "Serverbenachrichtigungen",
send: "Servernachricht versenden",

View File

@ -72,6 +72,22 @@ export default {
user_agent: "User agent",
},
},
devices: {
name: "Device |||| Devices",
fields: {
device_id: "Device-ID",
display_name: "Displayname",
last_seen_ts: "Timestamp",
last_seen_ip: "IP address",
},
action: {
remove_title: "Remove %{name} #%{id}",
remove_content:
"Are you sure you want to remove this %{name}? %{display_name}: %{displayname}",
remove_success: "Device successfully removed.",
remove_failure: "An error has occurred.",
},
},
servernotices: {
name: "Server Notices",
send: "Send server notices",

View File

@ -32,6 +32,9 @@ const resourceMap = {
? parseInt(json.next_token, 10) + perPage
: from + json.users.length;
},
getMany: id => ({
endpoint: `/_synapse/admin/v2/users/${id}`,
}),
create: data => ({
endpoint: `/_synapse/admin/v2/users/${data.id}`,
body: data,
@ -59,13 +62,26 @@ const resourceMap = {
return json.total_rooms;
},
},
devices: {
path: "/_synapse/admin/v2/users",
map: d => ({
...d,
id: d.devices[0].user_id,
}),
data: "devices",
getMany: id => ({
endpoint: `/_synapse/admin/v2/users/${id}/devices`,
}),
},
connections: {
path: "/_synapse/admin/v1/whois",
map: c => ({
...c,
id: c.user_id,
}),
data: "connections",
getMany: id => ({
endpoint: `/_synapse/admin/v1/whois/${id}`,
}),
},
servernotices: {
map: n => ({ id: n.event_id }),
@ -148,10 +164,14 @@ const dataProvider = {
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource];
if (!("getMany" in res)) return Promise.reject();
const endpoint_url = homeserver + res.path;
return Promise.all(
params.ids.map(id => jsonClient(`${endpoint_url}/${id}`))
params.ids.map(id => {
const getMany = res["getMany"](id);
const endpoint_url = homeserver + getMany.endpoint;
return jsonClient(endpoint_url);
})
).then(responses => ({
data: responses.map(({ json }) => res.map(json)),
}));
@ -321,6 +341,24 @@ const dataProvider = {
}));
}
},
removeDevice: (resource, params) => {
console.log("removeDevice " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
return jsonClient(
`${endpoint_url}/${params.user_id}/devices/${params.device_id}`,
{
method: "DELETE",
}
).then(({ json }) => ({
data: json,
}));
},
};
export default dataProvider;