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:
parent
8282a3caf8
commit
f92173b1cb
@ -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
96
src/components/devices.js
Normal 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" }
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user