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="rooms" list={RoomList} icon={RoomIcon} />
|
||||||
<Resource name="connections" />
|
<Resource name="connections" />
|
||||||
|
<Resource name="devices" />
|
||||||
<Resource name="servernotices" />
|
<Resource name="servernotices" />
|
||||||
</Admin>
|
</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 React, { Fragment } from "react";
|
||||||
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,
|
||||||
@ -32,6 +33,7 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
||||||
|
import { RemoveDeviceButton } from "./devices";
|
||||||
|
|
||||||
const UserPagination = props => (
|
const UserPagination = props => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
@ -148,10 +150,15 @@ const UserTitle = ({ record }) => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const UserEdit = props => (
|
export const UserEdit = props => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
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 />}
|
||||||
|
>
|
||||||
<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" />
|
||||||
@ -192,6 +199,40 @@ export const UserEdit = props => (
|
|||||||
</SimpleFormIterator>
|
</SimpleFormIterator>
|
||||||
</ArrayInput>
|
</ArrayInput>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
<FormTab
|
||||||
|
label={translate("resources.devices.name", { smart_count: 2 })}
|
||||||
|
icon={<DevicesIcon />}
|
||||||
|
path="devices"
|
||||||
|
>
|
||||||
|
<ReferenceField
|
||||||
|
reference="devices"
|
||||||
|
source="id"
|
||||||
|
addLabel={false}
|
||||||
|
link={false}
|
||||||
|
>
|
||||||
|
<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
|
<FormTab
|
||||||
label="resources.connections.name"
|
label="resources.connections.name"
|
||||||
icon={<SettingsInputComponentIcon />}
|
icon={<SettingsInputComponentIcon />}
|
||||||
@ -233,4 +274,5 @@ export const UserEdit = props => (
|
|||||||
</FormTab>
|
</FormTab>
|
||||||
</TabbedForm>
|
</TabbedForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -73,6 +73,22 @@ export default {
|
|||||||
user_agent: "User Agent",
|
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: {
|
servernotices: {
|
||||||
name: "Serverbenachrichtigungen",
|
name: "Serverbenachrichtigungen",
|
||||||
send: "Servernachricht versenden",
|
send: "Servernachricht versenden",
|
||||||
|
@ -72,6 +72,22 @@ export default {
|
|||||||
user_agent: "User agent",
|
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: {
|
servernotices: {
|
||||||
name: "Server Notices",
|
name: "Server Notices",
|
||||||
send: "Send server notices",
|
send: "Send server notices",
|
||||||
|
@ -32,6 +32,9 @@ const resourceMap = {
|
|||||||
? parseInt(json.next_token, 10) + perPage
|
? parseInt(json.next_token, 10) + perPage
|
||||||
: from + json.users.length;
|
: from + json.users.length;
|
||||||
},
|
},
|
||||||
|
getMany: id => ({
|
||||||
|
endpoint: `/_synapse/admin/v2/users/${id}`,
|
||||||
|
}),
|
||||||
create: data => ({
|
create: data => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/${data.id}`,
|
endpoint: `/_synapse/admin/v2/users/${data.id}`,
|
||||||
body: data,
|
body: data,
|
||||||
@ -59,13 +62,26 @@ const resourceMap = {
|
|||||||
return json.total_rooms;
|
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: {
|
connections: {
|
||||||
path: "/_synapse/admin/v1/whois",
|
|
||||||
map: c => ({
|
map: c => ({
|
||||||
...c,
|
...c,
|
||||||
id: c.user_id,
|
id: c.user_id,
|
||||||
}),
|
}),
|
||||||
data: "connections",
|
data: "connections",
|
||||||
|
getMany: id => ({
|
||||||
|
endpoint: `/_synapse/admin/v1/whois/${id}`,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
servernotices: {
|
servernotices: {
|
||||||
map: n => ({ id: n.event_id }),
|
map: n => ({ id: n.event_id }),
|
||||||
@ -148,10 +164,14 @@ const dataProvider = {
|
|||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
if (!("getMany" in res)) return Promise.reject();
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
|
||||||
return Promise.all(
|
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 => ({
|
).then(responses => ({
|
||||||
data: responses.map(({ json }) => res.map(json)),
|
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;
|
export default dataProvider;
|
||||||
|
Loading…
Reference in New Issue
Block a user