739 lines
26 KiB
TypeScript
739 lines
26 KiB
TypeScript
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
||
import DevicesIcon from "@mui/icons-material/Devices";
|
||
import DocumentScannerIcon from "@mui/icons-material/DocumentScanner";
|
||
import GetAppIcon from "@mui/icons-material/GetApp";
|
||
import UserIcon from "@mui/icons-material/Group";
|
||
import LockClockIcon from "@mui/icons-material/LockClock";
|
||
import NotificationsIcon from "@mui/icons-material/Notifications";
|
||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
||
import ScienceIcon from "@mui/icons-material/Science";
|
||
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||
import { Alert, Typography } from "@mui/material";
|
||
import { useTheme } from "@mui/material/styles";
|
||
import { useEffect, useState } from "react";
|
||
import {
|
||
ArrayInput,
|
||
ArrayField,
|
||
Button,
|
||
Datagrid,
|
||
DatagridConfigurable,
|
||
DateField,
|
||
Create,
|
||
CreateProps,
|
||
Edit,
|
||
EditProps,
|
||
List,
|
||
ListProps,
|
||
SimpleForm,
|
||
SimpleFormIterator,
|
||
TabbedForm,
|
||
FormTab,
|
||
BooleanField,
|
||
BooleanInput,
|
||
PasswordInput,
|
||
TextField,
|
||
TextInput,
|
||
ReferenceField,
|
||
ReferenceManyField,
|
||
ResourceProps,
|
||
SearchInput,
|
||
SelectInput,
|
||
DeleteButton,
|
||
maxLength,
|
||
regex,
|
||
required,
|
||
useRecordContext,
|
||
useTranslate,
|
||
Pagination,
|
||
SaveButton,
|
||
CreateButton,
|
||
ExportButton,
|
||
BulkDeleteButton,
|
||
TopToolbar,
|
||
Toolbar,
|
||
NumberField,
|
||
useListContext,
|
||
useNotify,
|
||
Identifier,
|
||
ToolbarClasses,
|
||
ImageInput,
|
||
ImageField,
|
||
FunctionField,
|
||
useDataProvider,
|
||
Confirm,
|
||
useCreate,
|
||
useRedirect,
|
||
} from "react-admin";
|
||
import { useFormContext } from "react-hook-form";
|
||
import { Link } from "react-router-dom";
|
||
|
||
import { MakeAdminBtn } from "./rooms";
|
||
import AvatarField from "../components/AvatarField";
|
||
import DeleteUserButton from "../components/DeleteUserButton";
|
||
import DeviceRemoveButton from "../components/DeviceRemoveButton";
|
||
import ExperimentalFeaturesList from "../components/ExperimentalFeatures";
|
||
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
|
||
import UserAccountData from "../components/UserAccountData";
|
||
import UserRateLimits from "../components/UserRateLimits";
|
||
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
|
||
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
|
||
import { DATE_FORMAT } from "../utils/date";
|
||
import decodeURLComponent from "../utils/decodeURLComponent";
|
||
import { isASManaged } from "../utils/mxid";
|
||
import { generateRandomPassword } from "../utils/password";
|
||
|
||
const choices_medium = [
|
||
{ id: "email", name: "resources.users.email" },
|
||
{ id: "msisdn", name: "resources.users.msisdn" },
|
||
];
|
||
|
||
const choices_type = [
|
||
{ id: "bot", name: "bot" },
|
||
{ id: "support", name: "support" },
|
||
];
|
||
|
||
const UserListActions = () => {
|
||
const { isLoading, total } = useListContext();
|
||
return (
|
||
<TopToolbar>
|
||
<CreateButton />
|
||
<ExportButton disabled={isLoading || total === 0} maxResults={10000} />
|
||
<Button component={Link} to="/import_users" label="CSV Import">
|
||
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
|
||
</Button>
|
||
</TopToolbar>
|
||
);
|
||
};
|
||
|
||
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||
|
||
const userFilters = [
|
||
<SearchInput source="name" alwaysOn />,
|
||
<BooleanInput source="guests" alwaysOn />,
|
||
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||
// waiting for https://github.com/element-hq/synapse/issues/18016
|
||
// <BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />,
|
||
];
|
||
|
||
const UserPreventSelfDelete: React.FC<{
|
||
children: React.ReactNode;
|
||
ownUserIsSelected: boolean;
|
||
asManagedUserIsSelected: boolean;
|
||
}> = props => {
|
||
const ownUserIsSelected = props.ownUserIsSelected;
|
||
const asManagedUserIsSelected = props.asManagedUserIsSelected;
|
||
const notify = useNotify();
|
||
const translate = useTranslate();
|
||
|
||
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||
if (ownUserIsSelected) {
|
||
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>);
|
||
ev.stopPropagation();
|
||
} else if (asManagedUserIsSelected) {
|
||
notify(<Alert severity="error">{translate("resources.users.helper.modify_managed_user_error")}</Alert>);
|
||
ev.stopPropagation();
|
||
}
|
||
};
|
||
|
||
return <div onClickCapture={handleDeleteClick}>{props.children}</div>;
|
||
};
|
||
|
||
const UserBulkActionButtons = () => {
|
||
const record = useListContext();
|
||
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
|
||
const [asManagedUserIsSelected, setAsManagedUserIsSelected] = useState(false);
|
||
const selectedIds = record.selectedIds;
|
||
const ownUserId = localStorage.getItem("user_id");
|
||
|
||
useEffect(() => {
|
||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||
setAsManagedUserIsSelected(selectedIds.some(id => isASManaged(id)));
|
||
}, [selectedIds]);
|
||
|
||
return (
|
||
<>
|
||
<ServerNoticeBulkButton />
|
||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||
<DeleteUserButton
|
||
selectedIds={selectedIds}
|
||
confirmTitle="resources.users.helper.erase"
|
||
confirmContent="resources.users.helper.erase_text"
|
||
/>
|
||
</UserPreventSelfDelete>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export const UserList = (props: ListProps) => (
|
||
<List
|
||
{...props}
|
||
filters={userFilters}
|
||
filterDefaultValues={{ guests: false, deactivated: false, locked: false, suspended: false }}
|
||
sort={{ field: "name", order: "ASC" }}
|
||
actions={<UserListActions />}
|
||
pagination={<UserPagination />}
|
||
perPage={50}
|
||
>
|
||
<DatagridConfigurable
|
||
rowClick={(id: Identifier, resource: string) => `/${resource}/${encodeURIComponent(id)}`}
|
||
bulkActionButtons={<UserBulkActionButtons />}
|
||
>
|
||
<AvatarField
|
||
source="avatar_src"
|
||
sx={{ height: "40px", width: "40px" }}
|
||
sortBy="avatar_url"
|
||
label="resources.users.fields.avatar"
|
||
/>
|
||
<TextField
|
||
source="id"
|
||
sx={{
|
||
wordBreak: "break-word",
|
||
overflowWrap: "break-word",
|
||
}}
|
||
sortBy="name"
|
||
label="resources.users.fields.id"
|
||
/>
|
||
<TextField
|
||
source="displayname"
|
||
sx={{
|
||
wordBreak: "break-word",
|
||
overflowWrap: "break-word",
|
||
}}
|
||
label="resources.users.fields.displayname"
|
||
/>
|
||
<BooleanField source="is_guest" label="resources.users.fields.is_guest" />
|
||
<BooleanField source="admin" label="resources.users.fields.admin" />
|
||
<BooleanField source="deactivated" label="resources.users.fields.deactivated" />
|
||
<BooleanField source="locked" label="resources.users.fields.locked" />
|
||
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
|
||
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
||
</DatagridConfigurable>
|
||
</List>
|
||
);
|
||
|
||
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||
// here only local part of user_id
|
||
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||
// storage.getItem("home_server").length is not valid here
|
||
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-+/]+$/, "synapseadmin.users.invalid_user_id")];
|
||
|
||
const validateAddress = [required(), maxLength(255)];
|
||
|
||
const UserEditActions = () => {
|
||
const record = useRecordContext();
|
||
const ownUserId = localStorage.getItem("user_id");
|
||
let ownUserIsSelected = false;
|
||
let asManagedUserIsSelected = false;
|
||
if (record && record.id) {
|
||
ownUserIsSelected = record.id === ownUserId;
|
||
asManagedUserIsSelected = isASManaged(record.id);
|
||
}
|
||
|
||
return (
|
||
<TopToolbar>
|
||
{!record?.deactivated && <ServerNoticeButton />}
|
||
{record && record.id && (
|
||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||
<DeleteUserButton
|
||
selectedIds={[record?.id]}
|
||
confirmTitle="resources.users.helper.erase"
|
||
confirmContent="resources.users.helper.erase_text"
|
||
/>
|
||
</UserPreventSelfDelete>
|
||
)}
|
||
</TopToolbar>
|
||
);
|
||
};
|
||
|
||
export const UserCreate = (props: CreateProps) => {
|
||
const dataProvider = useDataProvider();
|
||
const translate = useTranslate();
|
||
const redirect = useRedirect();
|
||
const notify = useNotify();
|
||
const theme = useTheme();
|
||
|
||
const [open, setOpen] = useState(false);
|
||
const [userIsAvailable, setUserIsAvailable] = useState<boolean | undefined>();
|
||
const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(
|
||
<Typography component="span"></Typography>
|
||
);
|
||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||
const [create] = useCreate();
|
||
|
||
const checkAvailability = async (event: React.FocusEvent<HTMLInputElement>) => {
|
||
const username = event.target.value;
|
||
const result: UsernameAvailabilityResult = await dataProvider.checkUsernameAvailability(username);
|
||
setUserIsAvailable(!!result?.available);
|
||
if (result?.available) {
|
||
setUserAvailabilityEl(
|
||
<Typography component="span" variant="body2" sx={{ color: theme.palette.success.main }}>
|
||
✔️ {translate("resources.users.helper.username_available")}
|
||
</Typography>
|
||
);
|
||
} else {
|
||
setUserAvailabilityEl(
|
||
<Typography component="span" variant="body2" sx={{ color: theme.palette.warning.main }}>
|
||
⚠️ {result?.error || "unknown error"}
|
||
</Typography>
|
||
);
|
||
}
|
||
};
|
||
|
||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||
const postSave = (data: Record<string, any>) => {
|
||
setFormData(data);
|
||
if (!userIsAvailable) {
|
||
setOpen(true);
|
||
return;
|
||
}
|
||
|
||
create(
|
||
"users",
|
||
{ data: data },
|
||
{
|
||
onSuccess: (resource: User) => {
|
||
notify("ra.notification.created", { messageArgs: { smart_count: 1 } });
|
||
redirect(() => {
|
||
return `users/${encodeURIComponent(resource.id as string)}`;
|
||
});
|
||
},
|
||
}
|
||
);
|
||
};
|
||
|
||
const handleConfirm = () => {
|
||
setOpen(false);
|
||
updateUser();
|
||
};
|
||
|
||
const handleDialogClose = () => {
|
||
setOpen(false);
|
||
};
|
||
|
||
const updateUser = () => {
|
||
create(
|
||
"users",
|
||
{ data: formData },
|
||
{
|
||
onSuccess: (resource: User) => {
|
||
notify("ra.notification.updated", { messageArgs: { smart_count: 1 } });
|
||
redirect(() => {
|
||
return `users/${encodeURIComponent(resource.id as string)}`;
|
||
});
|
||
},
|
||
}
|
||
);
|
||
};
|
||
|
||
return (
|
||
<Create {...props}>
|
||
<SimpleForm onSubmit={postSave}>
|
||
<TextInput
|
||
source="id"
|
||
autoComplete="off"
|
||
validate={validateUser}
|
||
onBlur={checkAvailability}
|
||
helperText={userAvailabilityEl}
|
||
/>
|
||
<TextInput source="displayname" validate={maxLength(256)} />
|
||
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||
<BooleanInput source="admin" />
|
||
<ArrayInput source="threepids">
|
||
<SimpleFormIterator disableReordering>
|
||
<SelectInput source="medium" choices={choices_medium} validate={required()} />
|
||
<TextInput source="address" validate={validateAddress} />
|
||
</SimpleFormIterator>
|
||
</ArrayInput>
|
||
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
|
||
<SimpleFormIterator disableReordering>
|
||
<TextInput source="auth_provider" validate={required()} />
|
||
<TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
|
||
</SimpleFormIterator>
|
||
</ArrayInput>
|
||
</SimpleForm>
|
||
<Confirm
|
||
isOpen={open}
|
||
title="resources.users.action.overwrite_title"
|
||
content="resources.users.action.overwrite_content"
|
||
onConfirm={handleConfirm}
|
||
onClose={handleDialogClose}
|
||
confirm="resources.users.action.overwrite_confirm"
|
||
cancel="resources.users.action.overwrite_cancel"
|
||
/>
|
||
</Create>
|
||
);
|
||
};
|
||
|
||
const UserTitle = () => {
|
||
const record = useRecordContext();
|
||
const translate = useTranslate();
|
||
if (!record) {
|
||
return null;
|
||
}
|
||
|
||
const username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : "";
|
||
return (
|
||
<span>
|
||
{translate("resources.users.name", {
|
||
smart_count: 1,
|
||
})}{" "}
|
||
{username}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const UserEditToolbar = () => {
|
||
const record = useRecordContext();
|
||
const ownUserId = localStorage.getItem("user_id");
|
||
let ownUserIsSelected = false;
|
||
let asManagedUserIsSelected = false;
|
||
if (record && record.id) {
|
||
ownUserIsSelected = record.id === ownUserId;
|
||
asManagedUserIsSelected = isASManaged(record.id);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div className={ToolbarClasses.defaultToolbar}>
|
||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||
<SaveButton />
|
||
<UserPreventSelfDelete
|
||
ownUserIsSelected={ownUserIsSelected}
|
||
asManagedUserIsSelected={asManagedUserIsSelected}
|
||
>
|
||
<DeleteButton />
|
||
</UserPreventSelfDelete>
|
||
</Toolbar>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const UserBooleanInput = props => {
|
||
const record = useRecordContext();
|
||
const ownUserId = localStorage.getItem("user_id");
|
||
let ownUserIsSelected = false;
|
||
let asManagedUserIsSelected = false;
|
||
if (record) {
|
||
ownUserIsSelected = record.id === ownUserId;
|
||
asManagedUserIsSelected = isASManaged(record.id);
|
||
}
|
||
|
||
return (
|
||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||
<BooleanInput disabled={ownUserIsSelected || asManagedUserIsSelected} {...props} />
|
||
</UserPreventSelfDelete>
|
||
);
|
||
};
|
||
|
||
const UserPasswordInput = props => {
|
||
const record = useRecordContext();
|
||
let asManagedUserIsSelected = false;
|
||
const translate = useTranslate();
|
||
|
||
// Get form context to update field value
|
||
const form = useFormContext();
|
||
if (record) {
|
||
asManagedUserIsSelected = isASManaged(record.id);
|
||
}
|
||
|
||
const generatePassword = () => {
|
||
const password = generateRandomPassword();
|
||
form.setValue("password", password, { shouldDirty: true });
|
||
};
|
||
|
||
// Get the current deactivated state and the original value
|
||
const deactivated = form.watch("deactivated");
|
||
const deactivatedFromRecord = record?.deactivated;
|
||
|
||
// Custom validation for reactivation case
|
||
const validatePasswordOnReactivation = value => {
|
||
if (deactivatedFromRecord === true && deactivated === false && !value) {
|
||
return translate("resources.users.helper.password_required_for_reactivation");
|
||
}
|
||
return undefined;
|
||
};
|
||
|
||
let passwordHelperText = "resources.users.helper.create_password";
|
||
|
||
if (asManagedUserIsSelected) {
|
||
passwordHelperText = "resources.users.helper.modify_managed_user_error";
|
||
} else if (deactivatedFromRecord === true && deactivated === false) {
|
||
passwordHelperText = "resources.users.helper.password_required_for_reactivation";
|
||
} else if (record) {
|
||
passwordHelperText = "resources.users.helper.password";
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<PasswordInput
|
||
{...props}
|
||
validate={validatePasswordOnReactivation}
|
||
helperText={passwordHelperText}
|
||
disabled={asManagedUserIsSelected}
|
||
/>
|
||
<Button
|
||
variant="outlined"
|
||
label="resources.users.action.generate_password"
|
||
onClick={generatePassword}
|
||
sx={{ marginBottom: "10px" }}
|
||
disabled={asManagedUserIsSelected}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const ErasedBooleanInput = props => {
|
||
const record = useRecordContext();
|
||
const form = useFormContext();
|
||
const deactivated = form.watch("deactivated");
|
||
const erased = form.watch("erased");
|
||
|
||
const erasedFromRecord = record?.erased;
|
||
const deactivatedFromRecord = record?.deactivated;
|
||
|
||
useEffect(() => {
|
||
// If the user was erased and deactivated, by unchecking Erased, we want to also uncheck Deactivated
|
||
if (erasedFromRecord === true && erased === false) {
|
||
form.setValue("deactivated", false);
|
||
}
|
||
}, [deactivatedFromRecord, erased, erasedFromRecord]);
|
||
|
||
return <UserBooleanInput disabled={!deactivated} {...props} />;
|
||
};
|
||
|
||
export const UserEdit = (props: EditProps) => {
|
||
const translate = useTranslate();
|
||
const theme = useTheme();
|
||
|
||
return (
|
||
<Edit
|
||
{...props}
|
||
title={<UserTitle />}
|
||
actions={<UserEditActions />}
|
||
mutationMode="pessimistic"
|
||
queryOptions={{
|
||
meta: {
|
||
include: ["features"], // Tell your dataProvider to include features
|
||
},
|
||
}}
|
||
>
|
||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||
<AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} />
|
||
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
||
<ImageInput
|
||
source="avatar_file"
|
||
label="resources.users.fields.avatar"
|
||
accept={{ "image/*": [".png", ".jpg"] }}
|
||
>
|
||
<ImageField
|
||
source="src"
|
||
title="Avatar"
|
||
sx={{
|
||
"& img": {
|
||
width: "120px !important",
|
||
height: "120px !important",
|
||
objectFit: "cover !important",
|
||
borderRadius: "50% !important",
|
||
},
|
||
}}
|
||
/>
|
||
</ImageInput>
|
||
<TextInput source="id" readOnly />
|
||
<TextInput source="displayname" />
|
||
<UserPasswordInput
|
||
source="password"
|
||
autoComplete="new-password"
|
||
helperText="resources.users.helper.password"
|
||
/>
|
||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||
<BooleanInput source="admin" helperText="resources.users.helper.admin" />
|
||
<UserBooleanInput source="suspended" helperText="resources.users.helper.suspend" />
|
||
<UserBooleanInput
|
||
sx={{ color: theme.palette.warning.main }}
|
||
source="locked"
|
||
helperText="resources.users.helper.lock"
|
||
/>
|
||
<UserBooleanInput
|
||
sx={{ color: theme.palette.error.main }}
|
||
source="deactivated"
|
||
helperText="resources.users.helper.deactivate"
|
||
/>
|
||
<ErasedBooleanInput
|
||
sx={{ color: theme.palette.error.main, marginLeft: "25px" }}
|
||
source="erased"
|
||
helperText="resources.users.helper.erase"
|
||
/>
|
||
<DateField sx={{ marginTop: "20px" }} source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||
<TextField source="consent_version" />
|
||
</FormTab>
|
||
|
||
<FormTab label="resources.users.threepid" icon={<ContactMailIcon />} path="threepid">
|
||
<ArrayInput source="threepids">
|
||
<SimpleFormIterator disableReordering>
|
||
<SelectInput source="medium" choices={choices_medium} />
|
||
<TextInput source="address" />
|
||
</SimpleFormIterator>
|
||
</ArrayInput>
|
||
</FormTab>
|
||
|
||
<FormTab label="synapseadmin.users.tabs.sso" icon={<AssignmentIndIcon />} path="sso">
|
||
<ArrayInput source="external_ids" label={false}>
|
||
<SimpleFormIterator disableReordering>
|
||
<TextInput source="auth_provider" validate={required()} />
|
||
<TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
|
||
</SimpleFormIterator>
|
||
</ArrayInput>
|
||
</FormTab>
|
||
|
||
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
|
||
<ReferenceManyField reference="devices" target="user_id" label={false}>
|
||
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
|
||
<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={DATE_FORMAT} sortable={false} />
|
||
<DeviceRemoveButton />
|
||
</Datagrid>
|
||
</ReferenceManyField>
|
||
</FormTab>
|
||
|
||
<FormTab label="resources.connections.name" icon={<SettingsInputComponentIcon />} path="connections">
|
||
<ReferenceField reference="connections" source="id" label={false} link={false}>
|
||
<ArrayField source="devices[].sessions[0].connections" label="resources.connections.name">
|
||
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
|
||
<TextField source="ip" sortable={false} />
|
||
<DateField source="last_seen" showTime options={DATE_FORMAT} sortable={false} />
|
||
<TextField source="user_agent" sortable={false} style={{ width: "100%" }} />
|
||
</Datagrid>
|
||
</ArrayField>
|
||
</ReferenceField>
|
||
</FormTab>
|
||
|
||
<FormTab
|
||
label={translate("resources.users_media.name", { smart_count: 2 })}
|
||
icon={<PermMediaIcon />}
|
||
path="media"
|
||
>
|
||
<ReferenceManyField
|
||
reference="users_media"
|
||
target="user_id"
|
||
label={false}
|
||
pagination={<UserPagination />}
|
||
perPage={10}
|
||
sort={{ field: "created_ts", order: "DESC" }}
|
||
>
|
||
<Datagrid sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton />}>
|
||
<MediaIDField source="media_id" />
|
||
<DateField source="created_ts" showTime options={DATE_FORMAT} />
|
||
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
||
<NumberField source="media_length" />
|
||
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
|
||
<FunctionField
|
||
source="upload_name"
|
||
render={record => (record.upload_name ? decodeURLComponent(record.upload_name) : "")}
|
||
/>
|
||
<TextField source="quarantined_by" />
|
||
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
|
||
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
|
||
<DeleteButton mutationMode="pessimistic" redirect={false} />
|
||
</Datagrid>
|
||
</ReferenceManyField>
|
||
</FormTab>
|
||
|
||
<FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms">
|
||
<ReferenceManyField
|
||
reference="joined_rooms"
|
||
target="user_id"
|
||
label={false}
|
||
perPage={10}
|
||
pagination={<Pagination />}
|
||
>
|
||
<Datagrid sx={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}>
|
||
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
|
||
<AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} />
|
||
</ReferenceField>
|
||
<TextField source="id" label="resources.rooms.fields.room_id" sortable={false} />
|
||
<ReferenceField
|
||
reference="rooms"
|
||
source="id"
|
||
label="resources.rooms.fields.name"
|
||
link={false}
|
||
sortable={false}
|
||
>
|
||
<TextField
|
||
source="name"
|
||
sx={{
|
||
wordBreak: "break-word",
|
||
overflowWrap: "break-word",
|
||
}}
|
||
/>
|
||
</ReferenceField>
|
||
<ReferenceField
|
||
reference="rooms"
|
||
source="id"
|
||
label="resources.rooms.fields.joined_members"
|
||
link={false}
|
||
sortable={false}
|
||
>
|
||
<TextField source="joined_members" sortable={false} />
|
||
</ReferenceField>
|
||
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
|
||
<MakeAdminBtn />
|
||
</ReferenceField>
|
||
</Datagrid>
|
||
</ReferenceManyField>
|
||
</FormTab>
|
||
|
||
<FormTab
|
||
label={translate("resources.pushers.name", { smart_count: 2 })}
|
||
icon={<NotificationsIcon />}
|
||
path="pushers"
|
||
>
|
||
<ReferenceManyField reference="pushers" target="user_id" label={false}>
|
||
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
|
||
<TextField source="kind" sortable={false} />
|
||
<TextField source="app_display_name" sortable={false} />
|
||
<TextField source="app_id" sortable={false} />
|
||
<TextField source="data.url" sortable={false} />
|
||
<TextField source="device_display_name" sortable={false} />
|
||
<TextField source="lang" sortable={false} />
|
||
<TextField source="profile_tag" sortable={false} />
|
||
<TextField source="pushkey" sortable={false} />
|
||
</Datagrid>
|
||
</ReferenceManyField>
|
||
</FormTab>
|
||
|
||
<FormTab label="synapseadmin.users.tabs.experimental" icon={<ScienceIcon />} path="experimental">
|
||
<ExperimentalFeaturesList />
|
||
</FormTab>
|
||
|
||
<FormTab label="synapseadmin.users.tabs.limits" icon={<LockClockIcon />} path="limits">
|
||
<UserRateLimits />
|
||
</FormTab>
|
||
|
||
<FormTab label="synapseadmin.users.tabs.account_data" icon={<DocumentScannerIcon />} path="accountdata">
|
||
<UserAccountData />
|
||
</FormTab>
|
||
</TabbedForm>
|
||
</Edit>
|
||
);
|
||
};
|
||
|
||
const resource: ResourceProps = {
|
||
name: "users",
|
||
icon: UserIcon,
|
||
list: UserList,
|
||
edit: UserEdit,
|
||
create: UserCreate,
|
||
};
|
||
|
||
export default resource;
|