Upgrade to React-Admin 4 (#332)

Change-Id: Ia03486edfd934438580e614af754a0966f6fd6e3
This commit is contained in:
dklimpel 2023-02-04 16:57:37 +01:00 committed by Manuel Stahl
parent 9f03ec9b0f
commit b70ee7c55d
14 changed files with 601 additions and 1066 deletions

View File

@ -18,12 +18,9 @@
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.2.0",
"ra-test": "^3.19.12"
"prettier": "^2.2.0"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.8",
"@mui/styles": "5.14.10",
@ -34,7 +31,7 @@
"ra-language-german": "^3.13.4",
"ra-language-italian": "^3.13.1",
"react": "^17.0.0",
"react-admin": "^3.19.12",
"react-admin": "^4.16.9",
"react-dom": "^17.0.2",
"react-scripts": "^5.0.1"
},

View File

@ -1,5 +1,10 @@
import React from "react";
import { Admin, Resource, resolveBrowserLocale } from "react-admin";
import {
Admin,
CustomRoutes,
Resource,
resolveBrowserLocale,
} from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
@ -50,10 +55,10 @@ const App = () => (
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
customRoutes={[
<Route key="userImport" path="/import_users" component={ImportFeature} />,
]}
>
<CustomRoutes>
<Route path="/import_users" element={<ImportFeature />} />
</CustomRoutes>
<Resource
name="users"
list={UserList}

View File

@ -0,0 +1,12 @@
import React from "react";
import get from "lodash/get";
import { Avatar } from "@mui/material";
import { useRecordContext } from "react-admin";
const AvatarField = ({ source, ...rest }) => {
const record = useRecordContext(rest);
const src = get(record, source)?.toString();
return <Avatar src={src} {...rest} />;
};
export default AvatarField;

View File

@ -1,19 +1,21 @@
import React, { useState, useEffect } from "react";
import {
fetchUtils,
Form,
FormDataConsumer,
Notification,
required,
useLogin,
useNotify,
useLocale,
useSetLocale,
useLocaleState,
useTranslate,
PasswordInput,
TextInput,
} from "react-admin";
import { Form, useForm } from "react-final-form";
import { useFormContext } from "react-hook-form";
import {
Avatar,
Box,
Button,
Card,
CardActions,
@ -21,66 +23,64 @@ import {
MenuItem,
Select,
TextField,
Typography,
} from "@mui/material";
import { makeStyles } from "@material-ui/core/styles";
import { styled } from "@mui/material/styles";
import LockIcon from "@mui/icons-material/Lock";
const useStyles = makeStyles(theme => ({
main: {
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 1em)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: "#f9f9f9",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
},
card: {
const FormBox = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 1em)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: "#f9f9f9",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
[`& .card`]: {
minWidth: "30em",
marginTop: "6em",
marginBottom: "6em",
},
avatar: {
[`& .avatar`]: {
margin: "1em",
display: "flex",
justifyContent: "center",
},
icon: {
backgroundColor: theme.palette.secondary.main,
[`& .icon`]: {
backgroundColor: theme.palette.grey[500],
},
hint: {
[`& .hint`]: {
marginTop: "1em",
display: "flex",
justifyContent: "center",
color: theme.palette.grey[500],
color: theme.palette.grey[600],
},
form: {
[`& .form`]: {
padding: "0 1em 1em 1em",
},
input: {
[`& .input`]: {
marginTop: "1em",
},
actions: {
[`& .actions`]: {
padding: "0 1em 1em 1em",
},
serverVersion: {
color: "#9e9e9e",
[`& .serverVersion`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginBottom: "1em",
marginLeft: "0.5em",
},
}));
const LoginPage = ({ theme }) => {
const classes = useStyles({ theme });
const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true);
var locale = useLocale();
const setLocale = useSetLocale();
const [locale, setLocale] = useLocaleState();
const translate = useTranslate();
const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER;
@ -135,28 +135,16 @@ const LoginPage = ({ theme }) => {
/>
);
const validate = values => {
const errors = {};
if (!values.username) {
errors.username = translate("ra.validation.required");
}
if (!values.password) {
errors.password = translate("ra.validation.required");
}
if (!values.base_url) {
errors.base_url = translate("ra.validation.required");
const validateBaseUrl = value => {
if (!value.match(/^(http|https):\/\//)) {
return translate("synapseadmin.auth.protocol_error");
} else if (
!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)
) {
return translate("synapseadmin.auth.url_error");
} else {
if (!values.base_url.match(/^(http|https):\/\//)) {
errors.base_url = translate("synapseadmin.auth.protocol_error");
} else if (
!values.base_url.match(
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/
)
) {
errors.base_url = translate("synapseadmin.auth.url_error");
}
return undefined;
}
return errors;
};
const handleSubmit = auth => {
@ -191,7 +179,7 @@ const LoginPage = ({ theme }) => {
};
const UserData = ({ formData }) => {
const form = useForm();
const form = useFormContext();
const [serverVersion, setServerVersion] = useState("");
const handleUsernameChange = _ => {
@ -204,11 +192,11 @@ const LoginPage = ({ theme }) => {
fetchUtils
.fetchJson(wellKnownUrl, { method: "GET" })
.then(({ json }) => {
form.change("base_url", json["m.homeserver"].base_url);
form.setValue("base_url", json["m.homeserver"].base_url);
})
.catch(_ => {
// if there is no .well-known entry, try the home server name
form.change("base_url", `https://${home_server}`);
form.setValue("base_url", `https://${home_server}`);
});
}
};
@ -265,8 +253,8 @@ const LoginPage = ({ theme }) => {
);
return (
<div>
<div className={classes.input}>
<>
<Box>
<TextInput
autoFocus
name="username"
@ -276,9 +264,11 @@ const LoginPage = ({ theme }) => {
onBlur={handleUsernameChange}
resettable
fullWidth
className="input"
validate={required()}
/>
</div>
<div className={classes.input}>
</Box>
<Box>
<PasswordInput
name="password"
component={renderInput}
@ -287,9 +277,11 @@ const LoginPage = ({ theme }) => {
disabled={loading || !supportPassAuth}
resettable
fullWidth
className="input"
validate={required()}
/>
</div>
<div className={classes.input}>
</Box>
<Box>
<TextInput
name="base_url"
component={renderInput}
@ -297,79 +289,75 @@ const LoginPage = ({ theme }) => {
disabled={cfg_base_url || loading}
resettable
fullWidth
className="input"
validate={[required(), validateBaseUrl]}
/>
</div>
<div className={classes.serverVersion}>{serverVersion}</div>
</div>
</Box>
<Typography className="serverVersion">{serverVersion}</Typography>
</>
);
};
return (
<Form
initialValues={{ base_url: cfg_base_url || base_url }}
defaultValues={{ base_url: cfg_base_url || base_url }}
onSubmit={handleSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} noValidate>
<div className={classes.main}>
<Card className={classes.card}>
<div className={classes.avatar}>
<Avatar className={classes.icon}>
<LockIcon />
</Avatar>
</div>
<div className={classes.hint}>
{translate("synapseadmin.auth.welcome")}
</div>
<div className={classes.form}>
<div className={classes.input}>
<Select
value={locale}
onChange={e => {
setLocale(e.target.value);
}}
fullWidth
disabled={loading}
>
<MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem>
<MenuItem value="zh">简体中文</MenuItem>
</Select>
</div>
<FormDataConsumer>
{formDataProps => <UserData {...formDataProps} />}
</FormDataConsumer>
</div>
<CardActions className={classes.actions}>
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading || !supportPassAuth}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("ra.auth.sign_in")}
</Button>
<Button
variant="contained"
color="secondary"
onClick={handleSSO}
disabled={loading || ssoBaseUrl === ""}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("synapseadmin.auth.sso_sign_in")}
</Button>
</CardActions>
</Card>
<Notification />
</div>
</form>
)}
/>
mode="onTouched"
>
<FormBox>
<Card className="card">
<Box className="avatar">
<Avatar className="icon">
<LockIcon />
</Avatar>
</Box>
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
<Box className="form">
<Select
value={locale}
onChange={e => {
setLocale(e.target.value);
}}
fullWidth
disabled={loading}
className="input"
>
<MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem>
<MenuItem value="zh">简体中文</MenuItem>
</Select>
<FormDataConsumer>
{formDataProps => <UserData {...formDataProps} />}
</FormDataConsumer>
<CardActions className="actions">
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading || !supportPassAuth}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("ra.auth.sign_in")}
</Button>
<Button
variant="contained"
color="secondary"
onClick={handleSSO}
disabled={loading || ssoBaseUrl === ""}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("synapseadmin.auth.sso_sign_in")}
</Button>
</CardActions>
</Box>
</Card>
</FormBox>
<Notification />
</Form>
);
};

View File

@ -1,14 +1,14 @@
import React from "react";
import { render } from "@testing-library/react";
import { TestContext } from "ra-test";
import { AdminContext } from "react-admin";
import LoginPage from "./LoginPage";
describe("LoginForm", () => {
it("renders", () => {
render(
<TestContext>
<AdminContext>
<LoginPage />
</TestContext>
</AdminContext>
);
});
});

View File

@ -1,26 +1,28 @@
import React, { Fragment } from "react";
import { Avatar, Chip } from "@mui/material";
import { connect } from "react-redux";
import FolderSharedIcon from "@mui/icons-material/FolderShared";
import {
BooleanField,
BulkDeleteButton,
Button,
Datagrid,
DatagridConfigurable,
ExportButton,
DeleteButton,
Filter,
List,
NumberField,
Pagination,
SelectColumnsButton,
TextField,
TopToolbar,
useCreate,
useMutation,
useListContext,
useNotify,
useTranslate,
useRecordContext,
useRefresh,
useUnselectAll,
} from "react-admin";
import { useMutation } from "react-query";
import AvatarField from "./AvatarField";
const RoomDirectoryPagination = props => (
<Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} />
@ -59,26 +61,23 @@ export const RoomDirectoryBulkDeleteButton = props => (
/>
);
export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
export const RoomDirectoryBulkSaveButton = () => {
const { selectedIds } = useListContext();
const notify = useNotify();
const refresh = useRefresh();
const unselectAll = useUnselectAll();
const [createMany, { loading }] = useMutation();
const { createMany, isloading } = useMutation();
const handleSend = values => {
createMany(
["room_directory", "createMany", { ids: selectedIds, data: {} }],
{
type: "createMany",
resource: "room_directory",
payload: { ids: selectedIds, data: {} },
},
{
onSuccess: ({ data }) => {
onSuccess: data => {
notify("resources.room_directory.action.send_success");
unselectAll("rooms");
refresh();
},
onFailure: error =>
onError: error =>
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
@ -90,30 +89,29 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
<Button
label="resources.room_directory.action.create"
onClick={handleSend}
disabled={loading}
disabled={isloading}
>
<FolderSharedIcon />
</Button>
);
};
export const RoomDirectorySaveButton = props => {
export const RoomDirectorySaveButton = () => {
const record = useRecordContext();
const notify = useNotify();
const refresh = useRefresh();
const [create, { loading }] = useCreate("room_directory");
const [create, { isloading }] = useCreate();
const handleSend = values => {
create(
"room_directory",
{ data: { id: record.id } },
{
payload: { data: { id: record.id } },
},
{
onSuccess: ({ data }) => {
onSuccess: data => {
notify("resources.room_directory.action.send_success");
refresh();
},
onFailure: error =>
onError: error =>
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
@ -125,127 +123,78 @@ export const RoomDirectorySaveButton = props => {
<Button
label="resources.room_directory.action.create"
onClick={handleSend}
disabled={loading}
disabled={isloading}
>
<FolderSharedIcon />
</Button>
);
};
const RoomDirectoryBulkActionButtons = props => (
const RoomDirectoryBulkActionButtons = () => (
<Fragment>
<RoomDirectoryBulkDeleteButton {...props} />
<RoomDirectoryBulkDeleteButton />
</Fragment>
);
const AvatarField = ({ source, className, record = {} }) => (
<Avatar src={record[source]} className={className} />
const RoomDirectoryListActions = () => (
<TopToolbar>
<SelectColumnsButton />
<ExportButton />
</TopToolbar>
);
const RoomDirectoryFilter = ({ ...props }) => {
const translate = useTranslate();
return (
<Filter {...props}>
<Chip
label={translate("resources.rooms.fields.room_id")}
source="room_id"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
<Chip
label={translate("resources.rooms.fields.topic")}
source="topic"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
<Chip
label={translate("resources.rooms.fields.canonical_alias")}
source="canonical_alias"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
</Filter>
);
};
export const FilterableRoomDirectoryList = ({
roomDirectoryFilters,
dispatch,
...props
}) => {
const filter = roomDirectoryFilters;
const roomIdFilter = filter && filter.room_id ? true : false;
const topicFilter = filter && filter.topic ? true : false;
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
return (
<List
{...props}
pagination={<RoomDirectoryPagination />}
export const RoomDirectoryList = () => (
<List
pagination={<RoomDirectoryPagination />}
perPage={100}
actions={<RoomDirectoryListActions />}
>
<DatagridConfigurable
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
bulkActionButtons={<RoomDirectoryBulkActionButtons />}
filters={<RoomDirectoryFilter />}
perPage={100}
omit={["room_id", "canonical_alias", "topic"]}
>
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}>
<AvatarField
source="avatar_src"
sortable={false}
sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar"
/>
<TextField
source="name"
sortable={false}
label="resources.rooms.fields.name"
/>
{roomIdFilter && (
<TextField
source="room_id"
sortable={false}
label="resources.rooms.fields.room_id"
/>
)}
{canonicalAliasFilter && (
<TextField
source="canonical_alias"
sortable={false}
label="resources.rooms.fields.canonical_alias"
/>
)}
{topicFilter && (
<TextField
source="topic"
sortable={false}
label="resources.rooms.fields.topic"
/>
)}
<NumberField
source="num_joined_members"
sortable={false}
label="resources.rooms.fields.joined_members"
/>
<BooleanField
source="world_readable"
sortable={false}
label="resources.room_directory.fields.world_readable"
/>
<BooleanField
source="guest_can_join"
sortable={false}
label="resources.room_directory.fields.guest_can_join"
/>
</Datagrid>
</List>
);
};
function mapStateToProps(state) {
return {
roomDirectoryFilters:
state.admin.resources.room_directory.list.params.displayedFilters,
};
}
export const RoomDirectoryList = connect(mapStateToProps)(
FilterableRoomDirectoryList
<AvatarField
source="avatar_src"
sortable={false}
sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar"
/>
<TextField
source="name"
sortable={false}
label="resources.rooms.fields.name"
/>
<TextField
source="room_id"
sortable={false}
label="resources.rooms.fields.room_id"
/>
<TextField
source="canonical_alias"
sortable={false}
label="resources.rooms.fields.canonical_alias"
/>
<TextField
source="topic"
sortable={false}
label="resources.rooms.fields.topic"
/>
<NumberField
source="num_joined_members"
sortable={false}
label="resources.rooms.fields.joined_members"
/>
<BooleanField
source="world_readable"
sortable={false}
label="resources.room_directory.fields.world_readable"
/>
<BooleanField
source="guest_can_join"
sortable={false}
label="resources.room_directory.fields.guest_can_join"
/>
</DatagridConfigurable>
</List>
);

View File

@ -7,12 +7,13 @@ import {
Toolbar,
required,
useCreate,
useMutation,
useListContext,
useNotify,
useRecordContext,
useTranslate,
useUnselectAll,
} from "react-admin";
import { useMutation } from "react-query";
import MessageIcon from "@mui/icons-material/Message";
import IconCancel from "@mui/icons-material/Cancel";
import {
@ -48,7 +49,6 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
</DialogContentText>
<SimpleForm
toolbar={<ServerNoticeToolbar />}
submitOnEnter={false}
redirect={false}
save={onSend}
>
@ -67,11 +67,11 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
);
};
export const ServerNoticeButton = props => {
export const ServerNoticeButton = () => {
const record = useRecordContext();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [create, { loading }] = useCreate("servernotices");
const [create, { isloading }] = useCreate("servernotices");
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
@ -84,7 +84,7 @@ export const ServerNoticeButton = props => {
notify("resources.servernotices.action.send_success");
handleDialogClose();
},
onFailure: () =>
onError: () =>
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
@ -97,7 +97,7 @@ export const ServerNoticeButton = props => {
<Button
label="resources.servernotices.send"
onClick={handleDialogOpen}
disabled={loading}
disabled={isloading}
>
<MessageIcon />
</Button>
@ -110,29 +110,26 @@ export const ServerNoticeButton = props => {
);
};
export const ServerNoticeBulkButton = ({ selectedIds }) => {
export const ServerNoticeBulkButton = () => {
const { selectedIds } = useListContext();
const [open, setOpen] = useState(false);
const notify = useNotify();
const unselectAll = useUnselectAll();
const [createMany, { loading }] = useMutation();
const { createMany, isloading } = useMutation();
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleSend = values => {
createMany(
["servernotices", "createMany", { ids: selectedIds, data: values }],
{
type: "createMany",
resource: "servernotices",
payload: { ids: selectedIds, data: values },
},
{
onSuccess: ({ data }) => {
onSuccess: data => {
notify("resources.servernotices.action.send_success");
unselectAll("users");
handleDialogClose();
},
onFailure: error =>
onError: error =>
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
@ -145,7 +142,7 @@ export const ServerNoticeBulkButton = ({ selectedIds }) => {
<Button
label="resources.servernotices.send"
onClick={handleDialogOpen}
disabled={loading}
disabled={isloading}
>
<MessageIcon />
</Button>

View File

@ -37,11 +37,11 @@ const date_format = {
second: "2-digit",
};
const destinationRowStyle = (record, index) => ({
const destinationRowSx = (record, _index) => ({
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
});
const DestinationFilter = ({ ...props }) => {
const DestinationFilter = props => {
return (
<Filter {...props}>
<SearchInput source="destination" alwaysOn />
@ -71,7 +71,7 @@ export const DestinationReconnectButton = props => {
});
refresh();
},
onFailure: () => {
onError: () => {
notify("ra.message.error", { type: "error" });
},
}
@ -112,11 +112,11 @@ export const DestinationList = props => {
filters={<DestinationFilter />}
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
bulkActionButtons={false}
>
<Datagrid
rowStyle={destinationRowStyle}
rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`}
rowSx={destinationRowSx}
rowClick={(id, _resource, _record) => `${id}/show/rooms`}
bulkActionButtons={false}
>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
@ -160,7 +160,7 @@ export const DestinationShow = props => {
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) => `/rooms/${id}/show`}
rowClick={(id, resource, record) => `/rooms/${id}/show`}
>
<TextField
source="room_id"

View File

@ -8,29 +8,11 @@ import {
useRefresh,
} from "react-admin";
import ActionDelete from "@mui/icons-material/Delete";
import { makeStyles } from "@material-ui/core/styles";
import { alpha } from "@mui/material/styles";
import classnames from "classnames";
const useStyles = makeStyles(
theme => ({
deleteButton: {
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
},
}),
{ name: "RaDeleteDeviceButton" }
);
import { alpha, useTheme } from "@mui/material/styles";
export const DeviceRemoveButton = props => {
const theme = useTheme();
const record = useRecordContext();
const classes = useStyles(props);
const [open, setOpen] = useState(false);
const refresh = useRefresh();
const notify = useNotify();
@ -63,7 +45,16 @@ export const DeviceRemoveButton = props => {
<Button
label="ra.action.remove"
onClick={handleClick}
className={classnames("ra-delete-button", classes.deleteButton)}
sx={{
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
}}
>
<ActionDelete />
</Button>

View File

@ -1,7 +1,4 @@
import React, { Fragment, useState } from "react";
import classnames from "classnames";
import { alpha } from "@mui/material/styles";
import { makeStyles } from "@material-ui/core/styles";
import {
BooleanInput,
Button,
@ -30,22 +27,7 @@ import {
import IconCancel from "@mui/icons-material/Cancel";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
const useStyles = makeStyles(
theme => ({
deleteButton: {
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
},
}),
{ name: "RaDeleteDeviceButton" }
);
import { alpha, useTheme } from "@mui/material/styles";
const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
const translate = useTranslate();
@ -81,7 +63,6 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
</DialogContentText>
<SimpleForm
toolbar={<DeleteMediaToolbar />}
submitOnEnter={false}
redirect={false}
save={onSend}
>
@ -113,10 +94,10 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
};
export const DeleteMediaButton = props => {
const classes = useStyles(props);
const theme = useTheme();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [deleteOne, { loading }] = useDelete("delete_media");
const [deleteOne, { isLoading }] = useDelete("delete_media");
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
@ -129,7 +110,7 @@ export const DeleteMediaButton = props => {
notify("resources.delete_media.action.send_success");
handleDialogClose();
},
onFailure: () =>
onError: () =>
notify("resources.delete_media.action.send_failure", {
type: "error",
}),
@ -142,8 +123,17 @@ export const DeleteMediaButton = props => {
<Button
label="resources.delete_media.action.send"
onClick={handleDialogOpen}
disabled={loading}
className={classnames("ra-delete-button", classes.deleteButton)}
disabled={isLoading}
sx={{
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
}}
>
<DeleteSweepIcon />
</Button>
@ -174,7 +164,7 @@ export const ProtectMediaButton = props => {
notify("resources.protect_media.action.send_success");
refresh();
},
onFailure: () =>
onError: () =>
notify("resources.protect_media.action.send_failure", {
type: "error",
}),
@ -190,7 +180,7 @@ export const ProtectMediaButton = props => {
notify("resources.protect_media.action.send_success");
refresh();
},
onFailure: () =>
onError: () =>
notify("resources.protect_media.action.send_failure", {
type: "error",
}),
@ -270,7 +260,7 @@ export const QuarantineMediaButton = props => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onFailure: () =>
onError: () =>
notify("resources.quarantine_media.action.send_failure", {
type: "error",
}),
@ -286,7 +276,7 @@ export const QuarantineMediaButton = props => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onFailure: () =>
onError: () =>
notify("resources.quarantine_media.action.send_failure", {
type: "error",
}),

View File

@ -1,18 +1,21 @@
import React, { Fragment } from "react";
import { connect } from "react-redux";
import {
BooleanField,
BulkDeleteButton,
DateField,
Datagrid,
DatagridConfigurable,
DeleteButton,
ExportButton,
Filter,
FunctionField,
List,
NumberField,
Pagination,
ReferenceField,
ReferenceManyField,
SearchInput,
SelectColumnsButton,
SelectField,
Show,
Tab,
@ -22,9 +25,7 @@ import {
useRecordContext,
useTranslate,
} from "react-admin";
import get from "lodash/get";
import PropTypes from "prop-types";
import { Tooltip, Typography, Chip } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import FastForwardIcon from "@mui/icons-material/FastForward";
import HttpsIcon from "@mui/icons-material/Https";
@ -54,32 +55,6 @@ const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const EncryptionField = ({ source, record = {}, emptyText }) => {
const translate = useTranslate();
const value = get(record, source);
let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true";
if (value === false || value === true) {
return (
<Typography component="span" variant="body2">
<Tooltip title={translate(ariaLabel, { _: ariaLabel })}>
{value === true ? (
<HttpsIcon data-testid="true" htmlColor="limegreen" />
) : (
<NoEncryptionIcon data-testid="false" color="error" />
)}
</Tooltip>
</Typography>
);
}
return (
<Typography component="span" variant="body2">
{emptyText}
</Typography>
);
};
const RoomTitle = props => {
const record = useRecordContext();
const translate = useTranslate();
@ -95,7 +70,7 @@ const RoomTitle = props => {
);
};
const RoomShowActions = ({ basePath, data, resource }) => {
const RoomShowActions = ({ data, resource }) => {
var roomDirectoryStatus = "";
if (data) {
roomDirectoryStatus = data.public;
@ -110,7 +85,6 @@ const RoomShowActions = ({ basePath, data, resource }) => {
<RoomDirectoryDeleteButton record={data} />
)}
<DeleteButton
basePath={basePath}
record={data}
resource={resource}
mutationMode="pessimistic"
@ -163,7 +137,7 @@ export const RoomShow = props => {
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) => "/users/" + id}
rowClick={(id, resource, record) => "/users/" + id}
>
<TextField
source="id"
@ -304,12 +278,11 @@ export const RoomShow = props => {
);
};
const RoomBulkActionButtons = props => (
const RoomBulkActionButtons = () => (
<Fragment>
<RoomDirectoryBulkSaveButton {...props} />
<RoomDirectoryBulkDeleteButton {...props} />
<RoomDirectoryBulkSaveButton />
<RoomDirectoryBulkDeleteButton />
<BulkDeleteButton
{...props}
confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic"
@ -317,91 +290,63 @@ const RoomBulkActionButtons = props => (
</Fragment>
);
const RoomFilter = ({ ...props }) => {
const translate = useTranslate();
return (
<Filter {...props}>
<SearchInput source="search_term" alwaysOn />
<Chip
label={translate("resources.rooms.fields.joined_local_members")}
source="joined_local_members"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
<Chip
label={translate("resources.rooms.fields.state_events")}
source="state_events"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
<Chip
label={translate("resources.rooms.fields.version")}
source="version"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
<Chip
label={translate("resources.rooms.fields.federatable")}
source="federatable"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
</Filter>
);
};
const RoomFilter = props => (
<Filter {...props}>
<SearchInput source="search_term" alwaysOn />
</Filter>
);
const RoomNameField = props => {
const { source } = props;
const record = useRecordContext();
return (
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
);
};
const RoomListActions = () => (
<TopToolbar>
<SelectColumnsButton />
<ExportButton />
</TopToolbar>
);
RoomNameField.propTypes = {
label: PropTypes.string,
record: PropTypes.object,
source: PropTypes.string.isRequired,
};
const FilterableRoomList = ({ roomFilters, dispatch, ...props }) => {
const filter = roomFilters;
const localMembersFilter =
filter && filter.joined_local_members ? true : false;
const stateEventsFilter = filter && filter.state_events ? true : false;
const versionFilter = filter && filter.version ? true : false;
const federateableFilter = filter && filter.federatable ? true : false;
export const RoomList = () => {
const theme = useTheme();
return (
<List
{...props}
pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }}
filters={<RoomFilter />}
bulkActionButtons={<RoomBulkActionButtons />}
actions={<RoomListActions />}
>
<Datagrid rowClick="show">
<EncryptionField
<DatagridConfigurable
rowClick="show"
bulkActionButtons={<RoomBulkActionButtons />}
omit={[
"joined_local_members",
"state_events",
"version",
"federatable",
]}
>
<BooleanField
source="is_encrypted"
sortBy="encryption"
TrueIcon={HttpsIcon}
FalseIcon={NoEncryptionIcon}
label={<HttpsIcon />}
sx={{
[`& [data-testid="true"]`]: { color: theme.palette.success.main },
[`& [data-testid="false"]`]: { color: theme.palette.error.main },
}}
/>
<FunctionField
source="name"
render={record =>
record["name"] || record["canonical_alias"] || record["id"]
}
/>
<RoomNameField source="name" />
<TextField source="joined_members" />
{localMembersFilter && <TextField source="joined_local_members" />}
{stateEventsFilter && <TextField source="state_events" />}
{versionFilter && <TextField source="version" />}
{federateableFilter && <BooleanField source="federatable" />}
<TextField source="joined_local_members" />
<TextField source="state_events" />
<TextField source="version" />
<BooleanField source="federatable" />
<BooleanField source="public" />
</Datagrid>
</DatagridConfigurable>
</List>
);
};
function mapStateToProps(state) {
return {
roomFilters: state.admin.resources.rooms.list.params.displayedFilters,
};
}
export const RoomList = connect(mapStateToProps)(FilterableRoomList);

View File

@ -65,9 +65,11 @@ export const UserMediaStatsList = props => {
filters={<UserMediaStatsFilter />}
pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }}
bulkActionButtons={false}
>
<Datagrid rowClick={(id, basePath, record) => "/users/" + id + "/media"}>
<Datagrid
rowClick={(id, resource, record) => "/users/" + id + "/media"}
bulkActionButtons={false}
>
<TextField source="user_id" label="resources.users.fields.id" />
<TextField
source="displayname"

View File

@ -1,5 +1,4 @@
import React, { cloneElement, Fragment } from "react";
import Avatar from "@mui/material/Avatar";
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import ContactMailIcon from "@mui/icons-material/ContactMail";
import DevicesIcon from "@mui/icons-material/Devices";
@ -49,16 +48,11 @@ import {
NumberField,
} from "react-admin";
import { Link } from "react-router-dom";
import AvatarField from "./AvatarField";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices";
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
const redirect = () => {
return {
pathname: "/import_users",
};
};
const choices_medium = [
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
@ -88,7 +82,6 @@ const UserListActions = ({
filterValues,
permanentFilter,
hasCreate, // you can hide CreateButton if hasCreate = false
basePath,
selectedIds,
onUnselectItems,
showFilter,
@ -106,7 +99,7 @@ const UserListActions = ({
filterValues,
context: "button",
})}
<CreateButton basePath={basePath} />
<CreateButton />
<ExportButton
disabled={total === 0}
resource={resource}
@ -116,7 +109,7 @@ const UserListActions = ({
maxResults={maxResults}
/>
{/* Add your custom actions */}
<Button component={Link} to={redirect} label="CSV Import">
<Button component={Link} to="/import_users" label="CSV Import">
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
</Button>
</TopToolbar>
@ -156,10 +149,6 @@ const UserBulkActionButtons = props => (
</Fragment>
);
const AvatarField = ({ source, record = {}, sx }) => (
<Avatar src={record[source]} sx={sx} />
);
export const UserList = props => {
return (
<List
@ -168,10 +157,9 @@ export const UserList = props => {
filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit">
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField
source="avatar_src"
sx={{ height: "40px", width: "40px" }}
@ -248,7 +236,7 @@ export function generateRandomUser() {
const UserEditToolbar = props => (
<Toolbar {...props}>
<SaveButton submitOnEnter={true} disabled={props.pristine} />
<SaveButton disabled={props.pristine} />
</Toolbar>
);
@ -288,7 +276,6 @@ export const UserCreate = props => (
source="user_type"
choices={choices_type}
translateChoice={false}
allowEmpty={true}
resettable
/>
<BooleanInput source="admin" />
@ -354,7 +341,6 @@ export const UserEdit = props => {
source="user_type"
choices={choices_type}
translateChoice={false}
allowEmpty={true}
resettable
/>
<BooleanInput source="admin" />
@ -498,7 +484,7 @@ export const UserEdit = props => {
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
>
<TextField
source="id"

863
yarn.lock

File diff suppressed because it is too large Load Diff