Merge branch 'master' into user_erased_details

This commit is contained in:
Dirk Klimpel
2023-01-24 16:50:21 +01:00
committed by GitHub
21 changed files with 469 additions and 370 deletions
+10 -1
View File
@@ -7,13 +7,15 @@ import { UserList, UserCreate, UserEdit } from "./components/users";
import { RoomList, RoomShow } from "./components/rooms";
import { ReportList, ReportShow } from "./components/EventReports";
import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group";
import ConfirmationNumberIcon from "@material-ui/icons/ConfirmationNumber";
import CloudQueueIcon from "@material-ui/icons/CloudQueue";
import EqualizerIcon from "@material-ui/icons/Equalizer";
import UserIcon from "@material-ui/icons/Group";
import { UserMediaStatsList } from "./components/statistics";
import RoomIcon from "@material-ui/icons/ViewList";
import ReportIcon from "@material-ui/icons/Warning";
import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { DestinationList, DestinationShow } from "./components/destinations";
import { ImportFeature } from "./components/ImportFeature";
import {
RegistrationTokenCreate,
@@ -72,6 +74,12 @@ const App = () => (
list={RoomDirectoryList}
icon={FolderSharedIcon}
/>
<Resource
name="destinations"
list={DestinationList}
show={DestinationShow}
icon={CloudQueueIcon}
/>
<Resource
name="registration_tokens"
list={RegistrationTokenList}
@@ -88,6 +96,7 @@ const App = () => (
<Resource name="servernotices" />
<Resource name="forward_extremities" />
<Resource name="room_state" />
<Resource name="destination_rooms" />
</Admin>
);
+1 -1
View File
@@ -169,7 +169,7 @@ const LoginPage = ({ theme }) => {
: typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error"
: error.message,
"warning"
{ type: "warning" }
);
});
};
+9 -3
View File
@@ -19,6 +19,7 @@ import {
useMutation,
useNotify,
useTranslate,
useRecordContext,
useRefresh,
useUnselectAll,
} from "react-admin";
@@ -87,7 +88,9 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
refresh();
},
onFailure: error =>
notify("resources.room_directory.action.send_failure", "error"),
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
}
);
};
@@ -103,7 +106,8 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
);
};
export const RoomDirectorySaveButton = ({ record }) => {
export const RoomDirectorySaveButton = props => {
const record = useRecordContext();
const notify = useNotify();
const refresh = useRefresh();
const [create, { loading }] = useCreate("room_directory");
@@ -119,7 +123,9 @@ export const RoomDirectorySaveButton = ({ record }) => {
refresh();
},
onFailure: error =>
notify("resources.room_directory.action.send_failure", "error"),
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
}
);
};
+9 -3
View File
@@ -9,6 +9,7 @@ import {
useCreate,
useMutation,
useNotify,
useRecordContext,
useTranslate,
useUnselectAll,
} from "react-admin";
@@ -64,7 +65,8 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
);
};
export const ServerNoticeButton = ({ record }) => {
export const ServerNoticeButton = props => {
const record = useRecordContext();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [create, { loading }] = useCreate("servernotices");
@@ -81,7 +83,9 @@ export const ServerNoticeButton = ({ record }) => {
handleDialogClose();
},
onFailure: () =>
notify("resources.servernotices.action.send_failure", "error"),
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
}
);
};
@@ -127,7 +131,9 @@ export const ServerNoticeBulkButton = ({ selectedIds }) => {
handleDialogClose();
},
onFailure: error =>
notify("resources.servernotices.action.send_failure", "error"),
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
}
);
};
+185
View File
@@ -0,0 +1,185 @@
import React from "react";
import {
Button,
Datagrid,
DateField,
Filter,
List,
Pagination,
ReferenceField,
ReferenceManyField,
SearchInput,
Show,
Tab,
TabbedShowLayout,
TextField,
TopToolbar,
useRecordContext,
useDelete,
useNotify,
useRefresh,
useTranslate,
} from "react-admin";
import AutorenewIcon from "@material-ui/icons/Autorenew";
import FolderSharedIcon from "@material-ui/icons/FolderShared";
import ViewListIcon from "@material-ui/icons/ViewList";
const DestinationPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const destinationRowStyle = (record, index) => ({
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
});
const DestinationFilter = ({ ...props }) => {
return (
<Filter {...props}>
<SearchInput source="destination" alwaysOn />
</Filter>
);
};
export const DestinationReconnectButton = props => {
const record = useRecordContext();
const refresh = useRefresh();
const notify = useNotify();
const [handleReconnect, { isLoading }] = useDelete("destinations");
// Reconnect is not required if no error has occurred. (`failure_ts`)
if (!record || !record.failure_ts) return null;
const handleClick = e => {
// Prevents redirection to the detail page when clicking in the list
e.stopPropagation();
handleReconnect(
{ payload: { id: record.id } },
{
onSuccess: () => {
notify("ra.notification.updated", {
messageArgs: { smart_count: 1 },
});
refresh();
},
onFailure: () => {
notify("ra.message.error", { type: "error" });
},
}
);
};
return (
<Button
label="resources.destinations.action.reconnect"
onClick={handleClick}
disabled={isLoading}
>
<AutorenewIcon />
</Button>
);
};
const DestinationShowActions = props => (
<TopToolbar>
<DestinationReconnectButton />
</TopToolbar>
);
const DestinationTitle = props => {
const record = useRecordContext();
const translate = useTranslate();
return (
<span>
{translate("resources.destinations.name", 1)} {record.destination}
</span>
);
};
export const DestinationList = props => {
return (
<List
{...props}
filters={<DestinationFilter />}
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
bulkActionButtons={false}
>
<Datagrid
rowStyle={destinationRowStyle}
rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`}
>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
<DestinationReconnectButton />
</Datagrid>
</List>
);
};
export const DestinationShow = props => {
const translate = useTranslate();
return (
<Show
actions={<DestinationShowActions />}
title={<DestinationTitle />}
{...props}
>
<TabbedShowLayout>
<Tab label="status" icon={<ViewListIcon />}>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
</Tab>
<Tab
label={translate("resources.rooms.name", { smart_count: 2 })}
icon={<FolderSharedIcon />}
path="rooms"
>
<ReferenceManyField
reference="destination_rooms"
target="destination"
addLabel={false}
pagination={<DestinationPagination />}
perPage={50}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) => `/rooms/${id}/show`}
>
<TextField
source="room_id"
label="resources.rooms.fields.room_id"
/>
<TextField source="stream_ordering" sortable={false} />
<ReferenceField
label="resources.rooms.fields.name"
source="id"
reference="rooms"
sortable={false}
link=""
>
<TextField source="name" sortable={false} />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
);
};
+9 -14
View File
@@ -1,9 +1,10 @@
import React, { Fragment, useState } from "react";
import {
Button,
useMutation,
useDelete,
useNotify,
Confirm,
useRecordContext,
useRefresh,
} from "react-admin";
import ActionDelete from "@material-ui/icons/Delete";
@@ -28,13 +29,13 @@ const useStyles = makeStyles(
);
export const DeviceRemoveButton = props => {
const { record } = props;
const record = useRecordContext();
const classes = useStyles(props);
const [open, setOpen] = useState(false);
const refresh = useRefresh();
const notify = useNotify();
const [removeDevice, { loading }] = useMutation();
const [removeDevice, { isLoading }] = useDelete("devices");
if (!record) return null;
@@ -43,21 +44,15 @@ export const DeviceRemoveButton = props => {
const handleConfirm = () => {
removeDevice(
{
type: "delete",
resource: "devices",
payload: {
id: record.id,
user_id: record.user_id,
},
},
{ 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"),
onFailure: () => {
notify("resources.devices.action.erase.failure", { type: "error" });
},
}
);
setOpen(false);
@@ -74,7 +69,7 @@ export const DeviceRemoveButton = props => {
</Button>
<Confirm
isOpen={open}
loading={loading}
loading={isLoading}
onConfirm={handleConfirm}
onClose={handleDialogClose}
title="resources.devices.action.erase.title"
+18 -7
View File
@@ -14,6 +14,7 @@ import {
useCreate,
useDelete,
useNotify,
useRecordContext,
useRefresh,
useTranslate,
} from "react-admin";
@@ -127,7 +128,9 @@ export const DeleteMediaButton = props => {
handleDialogClose();
},
onFailure: () =>
notify("resources.delete_media.action.send_failure", "error"),
notify("resources.delete_media.action.send_failure", {
type: "error",
}),
}
);
};
@@ -152,7 +155,7 @@ export const DeleteMediaButton = props => {
};
export const ProtectMediaButton = props => {
const { record } = props;
const record = useRecordContext();
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
@@ -170,7 +173,9 @@ export const ProtectMediaButton = props => {
refresh();
},
onFailure: () =>
notify("resources.protect_media.action.send_failure", "error"),
notify("resources.protect_media.action.send_failure", {
type: "error",
}),
}
);
};
@@ -184,7 +189,9 @@ export const ProtectMediaButton = props => {
refresh();
},
onFailure: () =>
notify("resources.protect_media.action.send_failure", "error"),
notify("resources.protect_media.action.send_failure", {
type: "error",
}),
}
);
};
@@ -244,7 +251,7 @@ export const ProtectMediaButton = props => {
};
export const QuarantineMediaButton = props => {
const { record } = props;
const record = useRecordContext();
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
@@ -262,7 +269,9 @@ export const QuarantineMediaButton = props => {
refresh();
},
onFailure: () =>
notify("resources.quarantine_media.action.send_failure", "error"),
notify("resources.quarantine_media.action.send_failure", {
type: "error",
}),
}
);
};
@@ -276,7 +285,9 @@ export const QuarantineMediaButton = props => {
refresh();
},
onFailure: () =>
notify("resources.quarantine_media.action.send_failure", "error"),
notify("resources.quarantine_media.action.send_failure", {
type: "error",
}),
}
);
};
+3 -2
View File
@@ -87,7 +87,8 @@ const EncryptionField = ({ source, record = {}, emptyText }) => {
);
};
const RoomTitle = ({ record }) => {
const RoomTitle = props => {
const record = useRecordContext();
const translate = useTranslate();
var name = "";
if (record) {
@@ -354,7 +355,7 @@ const RoomFilter = ({ ...props }) => {
const RoomNameField = props => {
const { source } = props;
const record = useRecordContext(props);
const record = useRecordContext();
return (
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
);
+8 -2
View File
@@ -39,6 +39,7 @@ import {
maxLength,
regex,
required,
useRecordContext,
useTranslate,
Pagination,
CreateButton,
@@ -330,7 +331,8 @@ export const UserCreate = props => (
</Create>
);
const UserTitle = ({ record }) => {
const UserTitle = props => {
const record = useRecordContext();
const translate = useTranslate();
return (
<span>
@@ -359,7 +361,11 @@ export const UserEdit = props => {
/>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<PasswordInput
source="password"
autoComplete="new-password"
helperText="resources.users.helper.password"
/>
<SelectInput
source="user_type"
choices={choices_type}
+14
View File
@@ -125,6 +125,8 @@ const de = {
user_type: "Benutzertyp",
},
helper: {
password:
"Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
deactivate:
"Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
erase: "DSGVO konformes Löschen der Benutzerdaten",
@@ -354,6 +356,18 @@ const de = {
send_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
},
destinations: {
name: "Föderation",
fields: {
destination: "Ziel",
failure_ts: "Fehlerzeitpunkt",
retry_last_ts: "Letzter Wiederholungsversuch",
retry_interval: "Wiederholungsintervall",
last_successful_stream_ordering: "letzte erfogreicher Stream",
stream_ordering: "Stream",
},
action: { reconnect: "Neu verbinden" },
},
registration_tokens: {
name: "Registrierungstoken",
fields: {
+13
View File
@@ -124,6 +124,7 @@ const en = {
user_type: "User type",
},
helper: {
password: "Changing password will log user out of all sessions.",
deactivate: "You must provide a password to re-activate an account.",
erase: "Mark the user as GDPR-erased",
},
@@ -352,6 +353,18 @@ const en = {
send_failure: "An error has occurred.",
},
},
destinations: {
name: "Federation",
fields: {
destination: "Destination",
failure_ts: "Failure timestamp",
retry_last_ts: "Last retry timestamp",
retry_interval: "Retry interval",
last_successful_stream_ordering: "Last successful stream",
stream_ordering: "Stream",
},
action: { reconnect: "Reconnect" },
},
},
registration_tokens: {
name: "Registration tokens",
+69 -20
View File
@@ -41,14 +41,16 @@ const resourceMap = {
data: "users",
total: json => json.total,
create: data => ({
endpoint: `/_synapse/admin/v2/users/@${data.id}:${localStorage.getItem(
"home_server"
)}`,
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(
data.id
)}:${localStorage.getItem("home_server")}`,
body: data,
method: "PUT",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/deactivate/${params.id}`,
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
params.id
)}`,
body: { erase: true },
method: "POST",
}),
@@ -92,10 +94,12 @@ const resourceMap = {
return json.total;
},
reference: id => ({
endpoint: `/_synapse/admin/v2/users/${id}/devices`,
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
}),
delete: params => ({
endpoint: `/_synapse/admin/v2/users/${params.user_id}/devices/${params.id}`,
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
params.user_id
)}/devices/${params.id}`,
}),
},
connections: {
@@ -137,7 +141,7 @@ const resourceMap = {
id: p.pushkey,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${id}/pushers`,
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
}),
data: "pushers",
total: json => {
@@ -149,7 +153,9 @@ const resourceMap = {
id: jr,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${id}/joined_rooms`,
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
id
)}/joined_rooms`,
}),
data: "joined_rooms",
total: json => {
@@ -162,7 +168,7 @@ const resourceMap = {
id: um.media_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${id}/media`,
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
}),
data: "media",
total: json => {
@@ -275,6 +281,34 @@ const resourceMap = {
method: "PUT",
}),
},
destinations: {
path: "/_synapse/admin/v1/federation/destinations",
map: dst => ({
...dst,
id: dst.destination,
}),
data: "destinations",
total: json => {
return json.total;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
method: "POST",
}),
},
destination_rooms: {
map: dstroom => ({
...dstroom,
id: dstroom.room_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
}),
data: "rooms",
total: json => {
return json.total;
},
},
registration_tokens: {
path: "/_synapse/admin/v1/registration_tokens",
map: rt => ({
@@ -316,8 +350,15 @@ function getSearchOrder(order) {
const dataProvider = {
getList: (resource, params) => {
console.log("getList " + resource);
const { user_id, name, guests, deactivated, search_term, valid } =
params.filter;
const {
user_id,
name,
guests,
deactivated,
search_term,
destination,
valid,
} = params.filter;
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const from = (page - 1) * perPage;
@@ -327,6 +368,7 @@ const dataProvider = {
user_id: user_id,
search_term: search_term,
name: name,
destination: destination,
guests: guests,
deactivated: deactivated,
valid: valid,
@@ -355,9 +397,11 @@ const dataProvider = {
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.id}`).then(({ json }) => ({
data: res.map(json),
}));
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then(
({ json }) => ({
data: res.map(json),
})
);
},
getMany: (resource, params) => {
@@ -369,7 +413,9 @@ const dataProvider = {
const endpoint_url = homeserver + res.path;
return Promise.all(
params.ids.map(id => jsonClient(`${endpoint_url}/${id}`))
params.ids.map(id =>
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
)
).then(responses => ({
data: responses.map(({ json }) => res.map(json)),
total: responses.length,
@@ -410,7 +456,7 @@ const dataProvider = {
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.data.id}`, {
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, {
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
}).then(({ json }) => ({
@@ -427,10 +473,13 @@ const dataProvider = {
const endpoint_url = homeserver + res.path;
return Promise.all(
params.ids.map(id => jsonClient(`${endpoint_url}/${id}`), {
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
})
params.ids.map(
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
{
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
}
)
).then(responses => ({
data: responses.map(({ json }) => json),
}));