Merge branch 'master' into locked_status

This commit is contained in:
Dirk Klimpel 2024-02-07 11:51:54 +01:00 committed by GitHub
commit feadb7b601
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 3347 additions and 4365 deletions

View File

@ -12,7 +12,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"
- name: Install dependencies - name: Install dependencies

View File

@ -19,11 +19,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -43,7 +43,7 @@ jobs:
esac esac
echo "::set-output name=tag::$tag" echo "::set-output name=tag::$tag"
- name: Build and Push Tag - name: Build and Push Tag
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true

View File

@ -11,7 +11,7 @@ jobs:
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"
- name: Install and Build 🔧 - name: Install and Build 🔧

View File

@ -14,7 +14,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"
- run: yarn install - run: yarn install

View File

@ -19,11 +19,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -43,7 +43,7 @@ jobs:
esac esac
echo "::set-output name=tag::$tag" echo "::set-output name=tag::$tag"
- name: Build and Push Tag - name: Build and Push Tag
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: false push: false

View File

@ -13,10 +13,10 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
### Supported Synapse ### Supported Synapse
It needs at least [Synapse](https://github.com/matrix-org/synapse) v1.93.0 for all functions to work as expected! It needs at least [Synapse](https://github.com/element-hq/synapse) v1.93.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`. You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html). See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
After entering the URL on the login page of synapse-admin the server version appears below the input field. After entering the URL on the login page of synapse-admin the server version appears below the input field.
@ -27,7 +27,7 @@ You need access to the following endpoints:
- `/_matrix` - `/_matrix`
- `/_synapse/admin` - `/_synapse/admin`
See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints) See also [Synapse administration endpoints](https://element-hq.github.io/synapse/latest/reverse_proxy.html#synapse-administration-endpoints)
### Use without install ### Use without install

View File

@ -1,6 +1,6 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.8.5", "version": "0.9.0",
"description": "Admin GUI for the Matrix.org server Synapse", "description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH", "author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -13,27 +13,25 @@
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"eslint": "^8.48.0", "eslint": "^8.55.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^2.2.0", "prettier": "^2.2.0"
"ra-test": "^3.19.12"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@mui/icons-material": "^5.14.19",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.14.8",
"@mui/material": "^5.14.8", "@mui/material": "^5.14.8",
"@mui/styles": "5.14.10",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"ra-language-chinese": "^2.0.10", "ra-language-chinese": "^2.0.10",
"ra-language-french": "^4.13.3", "ra-language-french": "^4.16.2",
"ra-language-german": "^3.13.4", "ra-language-german": "^3.13.4",
"ra-language-italian": "^3.13.1", "ra-language-italian": "^3.13.1",
"react": "^17.0.0", "react": "^17.0.0",
"react-admin": "^3.19.12", "react-admin": "^4.16.9",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "^5.0.1" "react-scripts": "^5.0.1"
}, },

View File

@ -1,2 +1,3 @@
# https://www.robotstxt.org/robotstxt.html # https://www.robotstxt.org/robotstxt.html
User-agent: * User-agent: *
Disallow: /

View File

@ -1,28 +1,22 @@
import React from "react"; 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 polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider"; import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider"; import dataProvider from "./synapse/dataProvider";
import { UserList, UserCreate, UserEdit } from "./components/users"; import users from "./components/users";
import { RoomList, RoomShow } from "./components/rooms"; import rooms from "./components/rooms";
import { ReportList, ReportShow } from "./components/EventReports"; import userMediaStats from "./components/statistics";
import reports from "./components/EventReports";
import roomDirectory from "./components/RoomDirectory";
import destinations from "./components/destinations";
import registrationToken from "./components/RegistrationTokens";
import LoginPage from "./components/LoginPage"; import LoginPage from "./components/LoginPage";
import ConfirmationNumberIcon from "@mui/icons-material/ConfirmationNumber";
import CloudQueueIcon from "@mui/icons-material/CloudQueue";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import UserIcon from "@mui/icons-material/Group";
import { UserMediaStatsList } from "./components/statistics";
import RoomIcon from "@mui/icons-material/ViewList";
import ReportIcon from "@mui/icons-material/Warning";
import FolderSharedIcon from "@mui/icons-material/FolderShared";
import { DestinationList, DestinationShow } from "./components/destinations";
import { ImportFeature } from "./components/ImportFeature"; import { ImportFeature } from "./components/ImportFeature";
import {
RegistrationTokenCreate,
RegistrationTokenEdit,
RegistrationTokenList,
} from "./components/RegistrationTokens";
import { RoomDirectoryList } from "./components/RoomDirectory";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import germanMessages from "./i18n/de"; import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en"; import englishMessages from "./i18n/en";
@ -50,47 +44,17 @@ const App = () => (
authProvider={authProvider} authProvider={authProvider}
dataProvider={dataProvider} dataProvider={dataProvider}
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
customRoutes={[
<Route key="userImport" path="/import_users" component={ImportFeature} />,
]}
> >
<Resource <CustomRoutes>
name="users" <Route path="/import_users" element={<ImportFeature />} />
list={UserList} </CustomRoutes>
create={UserCreate} <Resource {...users} />
edit={UserEdit} <Resource {...rooms} />
icon={UserIcon} <Resource {...userMediaStats} />
/> <Resource {...reports} />
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} /> <Resource {...roomDirectory} />
<Resource <Resource {...destinations} />
name="user_media_statistics" <Resource {...registrationToken} />
list={UserMediaStatsList}
icon={EqualizerIcon}
/>
<Resource
name="reports"
list={ReportList}
show={ReportShow}
icon={ReportIcon}
/>
<Resource
name="room_directory"
list={RoomDirectoryList}
icon={FolderSharedIcon}
/>
<Resource
name="destinations"
list={DestinationList}
show={DestinationShow}
icon={CloudQueueIcon}
/>
<Resource
name="registration_tokens"
list={RegistrationTokenList}
create={RegistrationTokenCreate}
edit={RegistrationTokenEdit}
icon={ConfirmationNumberIcon}
/>
<Resource name="connections" /> <Resource name="connections" />
<Resource name="devices" /> <Resource name="devices" />
<Resource name="room_members" /> <Resource name="room_members" />

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

@ -13,6 +13,7 @@ import {
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import PageviewIcon from "@mui/icons-material/Pageview"; import PageviewIcon from "@mui/icons-material/Pageview";
import ReportIcon from "@mui/icons-material/Warning";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
const date_format = { const date_format = {
@ -24,8 +25,8 @@ const date_format = {
second: "2-digit", second: "2-digit",
}; };
const ReportPagination = props => ( const ReportPagination = () => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
export const ReportShow = props => { export const ReportShow = props => {
@ -90,7 +91,7 @@ export const ReportShow = props => {
<TextField source="event_json.content.algorithm" /> <TextField source="event_json.content.algorithm" />
<TextField <TextField
source="event_json.content.device_id" source="event_json.content.device_id"
label="resources.users.fields.device_id" label="resources.devices.fields.device_id"
/> />
</Tab> </Tab>
</TabbedShowLayout> </TabbedShowLayout>
@ -98,15 +99,13 @@ export const ReportShow = props => {
); );
}; };
export const ReportList = ({ ...props }) => { export const ReportList = props => (
return (
<List <List
{...props} {...props}
pagination={<ReportPagination />} pagination={<ReportPagination />}
sort={{ field: "received_ts", order: "DESC" }} sort={{ field: "received_ts", order: "DESC" }}
bulkActionButtons={false}
> >
<Datagrid rowClick="show"> <Datagrid rowClick="show" bulkActionButtons={false}>
<TextField source="id" sortable={false} /> <TextField source="id" sortable={false} />
<DateField <DateField
source="received_ts" source="received_ts"
@ -119,5 +118,13 @@ export const ReportList = ({ ...props }) => {
<TextField sortable={false} source="score" /> <TextField sortable={false} source="score" />
</Datagrid> </Datagrid>
</List> </List>
); );
const resource = {
name: "reports",
icon: ReportIcon,
list: ReportList,
show: ReportShow,
}; };
export default resource;

View File

@ -1,12 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { import { useDataProvider, useNotify, Title } from "react-admin";
Button as ReactAdminButton,
useDataProvider,
useNotify,
Title,
} from "react-admin";
import { parse as parseCsv, unparse as unparseCsv } from "papaparse"; import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
import GetAppIcon from "@mui/icons-material/GetApp";
import { import {
Button, Button,
Card, Card,
@ -23,19 +17,6 @@ import { generateRandomUser } from "./users";
const LOGGING = true; const LOGGING = true;
export const ImportButton = ({ label, variant = "text" }) => {
return (
<ReactAdminButton
color="primary"
component="span"
variant={variant}
label={label}
>
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
</ReactAdminButton>
);
};
const expectedFields = ["id", "displayname"].sort(); const expectedFields = ["id", "displayname"].sort();
const optionalFields = [ const optionalFields = [
"user_type", "user_type",
@ -51,7 +32,7 @@ function TranslatableOption({ value, text }) {
return <option value={value}>{translate(text)}</option>; return <option value={value}>{translate(text)}</option>;
} }
const FilePicker = props => { const FilePicker = () => {
const [values, setValues] = useState(null); const [values, setValues] = useState(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
@ -210,7 +191,7 @@ const FilePicker = props => {
return true; return true;
}; };
const runImport = async e => { const runImport = async _e => {
if (progress !== null) { if (progress !== null) {
notify("import_users.errors.already_in_progress"); notify("import_users.errors.already_in_progress");
return; return;
@ -326,7 +307,7 @@ const FilePicker = props => {
let retries = 0; let retries = 0;
const submitRecord = recordData => { const submitRecord = recordData => {
return dataProvider.getOne("users", { id: recordData.id }).then( return dataProvider.getOne("users", { id: recordData.id }).then(
async alreadyExists => { async _alreadyExists => {
if (LOGGING) console.log("already existed"); if (LOGGING) console.log("already existed");
if (useridMode === "update" || conflictMode === "skip") { if (useridMode === "update" || conflictMode === "skip") {
@ -351,7 +332,7 @@ const FilePicker = props => {
} }
} }
}, },
async okToSubmit => { async _okToSubmit => {
if (LOGGING) if (LOGGING)
console.log( console.log(
"OK to create record " + "OK to create record " +

View File

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

View File

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

View File

@ -1,39 +0,0 @@
// in src/Menu.js
import * as React from "react";
import { useSelector } from "react-redux";
import { useMediaQuery } from "@mui/material";
import { MenuItemLink, getResources } from "react-admin";
import DefaultIcon from "@mui/icons-material/ViewList";
import LabelIcon from "@mui/icons-material/Label";
const Menu = ({ onMenuClick, logout }) => {
const isXSmall = useMediaQuery(theme => theme.breakpoints.down("xs"));
const open = useSelector(state => state.admin.ui.sidebarOpen);
const resources = useSelector(getResources);
return (
<div>
{resources.map(resource => (
<MenuItemLink
key={resource.name}
to={`/${resource.name}`}
primaryText={
(resource.options && resource.options.label) || resource.name
}
leftIcon={resource.icon ? <resource.icon /> : <DefaultIcon />}
onClick={onMenuClick}
sidebarIsOpen={open}
/>
))}
<MenuItemLink
to="/custom-route"
primaryText="Miscellaneous"
leftIcon={<LabelIcon />}
onClick={onMenuClick}
sidebarIsOpen={open}
/>
{isXSmall && logout}
</div>
);
};
export default Menu;

View File

@ -6,18 +6,19 @@ import {
DateField, DateField,
DateTimeInput, DateTimeInput,
Edit, Edit,
Filter,
List, List,
maxValue, maxValue,
number, number,
NumberField, NumberField,
NumberInput, NumberInput,
regex, regex,
SaveButton,
SimpleForm, SimpleForm,
TextInput, TextInput,
TextField, TextField,
Toolbar, Toolbar,
} from "react-admin"; } from "react-admin";
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
const date_format = { const date_format = {
year: "numeric", year: "numeric",
@ -53,17 +54,12 @@ const dateFormatter = v => {
return `${year}-${month}-${day}T${hour}:${minute}`; return `${year}-${month}-${day}T${hour}:${minute}`;
}; };
const RegistrationTokenFilter = props => ( const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
<Filter {...props}>
<BooleanInput source="valid" alwaysOn />
</Filter>
);
export const RegistrationTokenList = props => { export const RegistrationTokenList = props => (
return (
<List <List
{...props} {...props}
filters={<RegistrationTokenFilter />} filters={registrationTokenFilters}
filterDefaultValues={{ valid: true }} filterDefaultValues={{ valid: true }}
pagination={false} pagination={false}
perPage={500} perPage={500}
@ -81,12 +77,18 @@ export const RegistrationTokenList = props => {
/> />
</Datagrid> </Datagrid>
</List> </List>
); );
};
export const RegistrationTokenCreate = props => ( export const RegistrationTokenCreate = props => (
<Create {...props}> <Create {...props} redirect="list">
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}> <SimpleForm
toolbar={
<Toolbar>
{/* It is possible to create tokens per default without input. */}
<SaveButton alwaysEnable />
</Toolbar>
}
>
<TextInput <TextInput
source="token" source="token"
autoComplete="off" autoComplete="off"
@ -109,8 +111,7 @@ export const RegistrationTokenCreate = props => (
</Create> </Create>
); );
export const RegistrationTokenEdit = props => { export const RegistrationTokenEdit = props => (
return (
<Edit {...props}> <Edit {...props}>
<SimpleForm> <SimpleForm>
<TextInput source="token" disabled /> <TextInput source="token" disabled />
@ -128,5 +129,14 @@ export const RegistrationTokenEdit = props => {
/> />
</SimpleForm> </SimpleForm>
</Edit> </Edit>
); );
const resource = {
name: "users",
icon: RegistrationTokenIcon,
list: RegistrationTokenList,
edit: RegistrationTokenEdit,
create: RegistrationTokenCreate,
}; };
export default resource;

View File

@ -1,40 +1,35 @@
import React, { Fragment } from "react"; import React from "react";
import { Avatar, Chip } from "@mui/material";
import { connect } from "react-redux";
import FolderSharedIcon from "@mui/icons-material/FolderShared";
import { makeStyles } from "@material-ui/core/styles";
import { import {
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
Button, Button,
Datagrid, DatagridConfigurable,
ExportButton,
DeleteButton, DeleteButton,
Filter,
List, List,
NumberField, NumberField,
Pagination, Pagination,
SelectColumnsButton,
TextField, TextField,
TopToolbar,
useCreate, useCreate,
useMutation, useDataProvider,
useListContext,
useNotify, useNotify,
useTranslate, useTranslate,
useRecordContext, useRecordContext,
useRefresh, useRefresh,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "react-query";
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
import AvatarField from "./AvatarField";
const useStyles = makeStyles({ const RoomDirectoryPagination = () => (
small: { <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
height: "40px",
width: "40px",
},
});
const RoomDirectoryPagination = props => (
<Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} />
); );
export const RoomDirectoryDeleteButton = props => { export const RoomDirectoryUnpublishButton = props => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
@ -50,12 +45,12 @@ export const RoomDirectoryDeleteButton = props => {
smart_count: 1, smart_count: 1,
})} })}
resource="room_directory" resource="room_directory"
icon={<FolderSharedIcon />} icon={<RoomDirectoryIcon />}
/> />
); );
}; };
export const RoomDirectoryBulkDeleteButton = props => ( export const RoomDirectoryBulkUnpublishButton = props => (
<BulkDeleteButton <BulkDeleteButton
{...props} {...props}
label="resources.room_directory.action.erase" label="resources.room_directory.action.erase"
@ -63,65 +58,63 @@ export const RoomDirectoryBulkDeleteButton = props => (
confirmTitle="resources.room_directory.action.title" confirmTitle="resources.room_directory.action.title"
confirmContent="resources.room_directory.action.content" confirmContent="resources.room_directory.action.content"
resource="room_directory" resource="room_directory"
icon={<FolderSharedIcon />} icon={<RoomDirectoryIcon />}
/> />
); );
export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => { export const RoomDirectoryBulkPublishButton = props => {
const { selectedIds } = useListContext();
const notify = useNotify(); const notify = useNotify();
const refresh = useRefresh(); const refresh = useRefresh();
const unselectAll = useUnselectAll(); const unselectAllRooms = useUnselectAll("rooms");
const [createMany, { loading }] = useMutation(); const dataProvider = useDataProvider();
const { mutate, isLoading } = useMutation(
const handleSend = values => { () =>
createMany( dataProvider.createMany("room_directory", {
ids: selectedIds,
data: {},
}),
{ {
type: "createMany", onSuccess: () => {
resource: "room_directory",
payload: { ids: selectedIds, data: {} },
},
{
onSuccess: ({ data }) => {
notify("resources.room_directory.action.send_success"); notify("resources.room_directory.action.send_success");
unselectAll("rooms"); unselectAllRooms();
refresh(); refresh();
}, },
onFailure: error => onError: () =>
notify("resources.room_directory.action.send_failure", { notify("resources.room_directory.action.send_failure", {
type: "error", type: "error",
}), }),
} }
); );
};
return ( return (
<Button <Button
{...props}
label="resources.room_directory.action.create" label="resources.room_directory.action.create"
onClick={handleSend} onClick={mutate}
disabled={loading} disabled={isLoading}
> >
<FolderSharedIcon /> <RoomDirectoryIcon />
</Button> </Button>
); );
}; };
export const RoomDirectorySaveButton = props => { export const RoomDirectoryPublishButton = props => {
const record = useRecordContext(); const record = useRecordContext();
const notify = useNotify(); const notify = useNotify();
const refresh = useRefresh(); const refresh = useRefresh();
const [create, { loading }] = useCreate("room_directory"); const [create, { isLoading }] = useCreate();
const handleSend = values => { const handleSend = () => {
create( create(
"room_directory",
{ data: { id: record.id } },
{ {
payload: { data: { id: record.id } }, onSuccess: () => {
},
{
onSuccess: ({ data }) => {
notify("resources.room_directory.action.send_success"); notify("resources.room_directory.action.send_success");
refresh(); refresh();
}, },
onFailure: error => onError: () =>
notify("resources.room_directory.action.send_failure", { notify("resources.room_directory.action.send_failure", {
type: "error", type: "error",
}), }),
@ -131,75 +124,38 @@ export const RoomDirectorySaveButton = props => {
return ( return (
<Button <Button
{...props}
label="resources.room_directory.action.create" label="resources.room_directory.action.create"
onClick={handleSend} onClick={handleSend}
disabled={loading} disabled={isLoading}
> >
<FolderSharedIcon /> <RoomDirectoryIcon />
</Button> </Button>
); );
}; };
const RoomDirectoryBulkActionButtons = props => ( const RoomDirectoryListActions = () => (
<Fragment> <TopToolbar>
<RoomDirectoryBulkDeleteButton {...props} /> <SelectColumnsButton />
</Fragment> <ExportButton />
</TopToolbar>
); );
const AvatarField = ({ source, className, record = {} }) => ( export const RoomDirectoryList = () => (
<Avatar src={record[source]} className={className} />
);
const RoomDirectoryFilter = ({ ...props }) => {
const translate = useTranslate();
return (
<Filter {...props}>
<Chip
label={translate("resources.rooms.fields.room_id")}
source="room_id"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.topic")}
source="topic"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.canonical_alias")}
source="canonical_alias"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
</Filter>
);
};
export const FilterableRoomDirectoryList = ({
roomDirectoryFilters,
dispatch,
...props
}) => {
const classes = useStyles();
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 <List
{...props}
pagination={<RoomDirectoryPagination />} pagination={<RoomDirectoryPagination />}
bulkActionButtons={<RoomDirectoryBulkActionButtons />}
filters={<RoomDirectoryFilter />}
perPage={100} perPage={100}
actions={<RoomDirectoryListActions />}
>
<DatagridConfigurable
rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
omit={["room_id", "canonical_alias", "topic"]}
> >
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}>
<AvatarField <AvatarField
source="avatar_src" source="avatar_src"
sortable={false} sortable={false}
className={classes.small} sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar" label="resources.rooms.fields.avatar"
/> />
<TextField <TextField
@ -207,27 +163,21 @@ export const FilterableRoomDirectoryList = ({
sortable={false} sortable={false}
label="resources.rooms.fields.name" label="resources.rooms.fields.name"
/> />
{roomIdFilter && (
<TextField <TextField
source="room_id" source="room_id"
sortable={false} sortable={false}
label="resources.rooms.fields.room_id" label="resources.rooms.fields.room_id"
/> />
)}
{canonicalAliasFilter && (
<TextField <TextField
source="canonical_alias" source="canonical_alias"
sortable={false} sortable={false}
label="resources.rooms.fields.canonical_alias" label="resources.rooms.fields.canonical_alias"
/> />
)}
{topicFilter && (
<TextField <TextField
source="topic" source="topic"
sortable={false} sortable={false}
label="resources.rooms.fields.topic" label="resources.rooms.fields.topic"
/> />
)}
<NumberField <NumberField
source="num_joined_members" source="num_joined_members"
sortable={false} sortable={false}
@ -243,18 +193,14 @@ export const FilterableRoomDirectoryList = ({
sortable={false} sortable={false}
label="resources.room_directory.fields.guest_can_join" label="resources.room_directory.fields.guest_can_join"
/> />
</Datagrid> </DatagridConfigurable>
</List> </List>
); );
const resource = {
name: "room_directory",
icon: RoomDirectoryIcon,
list: RoomDirectoryList,
}; };
function mapStateToProps(state) { export default resource;
return {
roomDirectoryFilters:
state.admin.resources.room_directory.list.params.displayedFilters,
};
}
export const RoomDirectoryList = connect(mapStateToProps)(
FilterableRoomDirectoryList
);

View File

@ -1,4 +1,4 @@
import React, { Fragment, useState } from "react"; import React, { useState } from "react";
import { import {
Button, Button,
SaveButton, SaveButton,
@ -7,12 +7,14 @@ import {
Toolbar, Toolbar,
required, required,
useCreate, useCreate,
useMutation, useDataProvider,
useListContext,
useNotify, useNotify,
useRecordContext, useRecordContext,
useTranslate, useTranslate,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "react-query";
import MessageIcon from "@mui/icons-material/Message"; import MessageIcon from "@mui/icons-material/Message";
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@mui/icons-material/Cancel";
import { import {
@ -22,7 +24,7 @@ import {
DialogTitle, DialogTitle,
} from "@mui/material"; } from "@mui/material";
const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => { const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
const translate = useTranslate(); const translate = useTranslate();
const ServerNoticeToolbar = props => ( const ServerNoticeToolbar = props => (
@ -46,12 +48,7 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
<DialogContentText> <DialogContentText>
{translate("resources.servernotices.helper.send")} {translate("resources.servernotices.helper.send")}
</DialogContentText> </DialogContentText>
<SimpleForm <SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
toolbar={<ServerNoticeToolbar />}
submitOnEnter={false}
redirect={false}
save={onSend}
>
<TextInput <TextInput
source="body" source="body"
label="resources.servernotices.fields.body" label="resources.servernotices.fields.body"
@ -67,24 +64,25 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
); );
}; };
export const ServerNoticeButton = props => { export const ServerNoticeButton = () => {
const record = useRecordContext(); const record = useRecordContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const notify = useNotify(); const notify = useNotify();
const [create, { loading }] = useCreate("servernotices"); const [create, { isloading }] = useCreate();
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
const handleSend = values => { const handleSend = values => {
create( create(
{ payload: { data: { id: record.id, ...values } } }, "servernotices",
{ data: { id: record.id, ...values } },
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.servernotices.action.send_success"); notify("resources.servernotices.action.send_success");
handleDialogClose(); handleDialogClose();
}, },
onFailure: () => onError: () =>
notify("resources.servernotices.action.send_failure", { notify("resources.servernotices.action.send_failure", {
type: "error", type: "error",
}), }),
@ -93,67 +91,65 @@ export const ServerNoticeButton = props => {
}; };
return ( return (
<Fragment> <>
<Button <Button
label="resources.servernotices.send" label="resources.servernotices.send"
onClick={handleDialogOpen} onClick={handleDialogOpen}
disabled={loading} disabled={isloading}
> >
<MessageIcon /> <MessageIcon />
</Button> </Button>
<ServerNoticeDialog <ServerNoticeDialog
open={open} open={open}
onClose={handleDialogClose} onClose={handleDialogClose}
onSend={handleSend} onSubmit={handleSend}
/> />
</Fragment> </>
); );
}; };
export const ServerNoticeBulkButton = ({ selectedIds }) => { export const ServerNoticeBulkButton = () => {
const { selectedIds } = useListContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const notify = useNotify(); const notify = useNotify();
const unselectAll = useUnselectAll(); const unselectAllUsers = useUnselectAll("users");
const [createMany, { loading }] = useMutation(); const dataProvider = useDataProvider();
const handleDialogOpen = () => setOpen(true); const { mutate: sendNotices, isLoading } = useMutation(
const handleDialogClose = () => setOpen(false); data =>
dataProvider.createMany("servernotices", {
const handleSend = values => { ids: selectedIds,
createMany( data: data,
}),
{ {
type: "createMany", onSuccess: () => {
resource: "servernotices",
payload: { ids: selectedIds, data: values },
},
{
onSuccess: ({ data }) => {
notify("resources.servernotices.action.send_success"); notify("resources.servernotices.action.send_success");
unselectAll("users"); unselectAllUsers();
handleDialogClose(); closeDialog();
}, },
onFailure: error => onError: () =>
notify("resources.servernotices.action.send_failure", { notify("resources.servernotices.action.send_failure", {
type: "error", type: "error",
}), }),
} }
); );
};
return ( return (
<Fragment> <>
<Button <Button
label="resources.servernotices.send" label="resources.servernotices.send"
onClick={handleDialogOpen} onClick={openDialog}
disabled={loading} disabled={isLoading}
> >
<MessageIcon /> <MessageIcon />
</Button> </Button>
<ServerNoticeDialog <ServerNoticeDialog
open={open} open={open}
onClose={handleDialogClose} onClose={closeDialog}
onSend={handleSend} onSubmit={sendNotices}
/> />
</Fragment> </>
); );
}; };

View File

@ -3,7 +3,6 @@ import {
Button, Button,
Datagrid, Datagrid,
DateField, DateField,
Filter,
List, List,
Pagination, Pagination,
ReferenceField, ReferenceField,
@ -21,11 +20,12 @@ import {
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import AutorenewIcon from "@mui/icons-material/Autorenew"; import AutorenewIcon from "@mui/icons-material/Autorenew";
import DestinationsIcon from "@mui/icons-material/CloudQueue";
import FolderSharedIcon from "@mui/icons-material/FolderShared"; import FolderSharedIcon from "@mui/icons-material/FolderShared";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
const DestinationPagination = props => ( const DestinationPagination = () => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
const date_format = { const date_format = {
@ -37,23 +37,17 @@ const date_format = {
second: "2-digit", second: "2-digit",
}; };
const destinationRowStyle = (record, index) => ({ const destinationRowSx = (record, _index) => ({
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white", backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
}); });
const DestinationFilter = ({ ...props }) => { const destinationFilters = [<SearchInput source="destination" alwaysOn />];
return (
<Filter {...props}>
<SearchInput source="destination" alwaysOn />
</Filter>
);
};
export const DestinationReconnectButton = props => { export const DestinationReconnectButton = () => {
const record = useRecordContext(); const record = useRecordContext();
const refresh = useRefresh(); const refresh = useRefresh();
const notify = useNotify(); const notify = useNotify();
const [handleReconnect, { isLoading }] = useDelete("destinations"); const [handleReconnect, { isLoading }] = useDelete();
// Reconnect is not required if no error has occurred. (`failure_ts`) // Reconnect is not required if no error has occurred. (`failure_ts`)
if (!record || !record.failure_ts) return null; if (!record || !record.failure_ts) return null;
@ -63,7 +57,8 @@ export const DestinationReconnectButton = props => {
e.stopPropagation(); e.stopPropagation();
handleReconnect( handleReconnect(
{ payload: { id: record.id } }, "destinations",
{ id: record.id },
{ {
onSuccess: () => { onSuccess: () => {
notify("ra.notification.updated", { notify("ra.notification.updated", {
@ -71,7 +66,7 @@ export const DestinationReconnectButton = props => {
}); });
refresh(); refresh();
}, },
onFailure: () => { onError: () => {
notify("ra.message.error", { type: "error" }); notify("ra.message.error", { type: "error" });
}, },
} }
@ -89,13 +84,13 @@ export const DestinationReconnectButton = props => {
); );
}; };
const DestinationShowActions = props => ( const DestinationShowActions = () => (
<TopToolbar> <TopToolbar>
<DestinationReconnectButton /> <DestinationReconnectButton />
</TopToolbar> </TopToolbar>
); );
const DestinationTitle = props => { const DestinationTitle = () => {
const record = useRecordContext(); const record = useRecordContext();
const translate = useTranslate(); const translate = useTranslate();
return ( return (
@ -109,14 +104,14 @@ export const DestinationList = props => {
return ( return (
<List <List
{...props} {...props}
filters={<DestinationFilter />} filters={destinationFilters}
pagination={<DestinationPagination />} pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }} sort={{ field: "destination", order: "ASC" }}
bulkActionButtons={false}
> >
<Datagrid <Datagrid
rowStyle={destinationRowStyle} rowSx={destinationRowSx}
rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`} rowClick={(id, _resource, _record) => `${id}/show/rooms`}
bulkActionButtons={false}
> >
<TextField source="destination" /> <TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} /> <DateField source="failure_ts" showTime options={date_format} />
@ -160,7 +155,7 @@ export const DestinationShow = props => {
> >
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, basePath, record) => `/rooms/${id}/show`} rowClick={(id, resource, record) => `/rooms/${id}/show`}
> >
<TextField <TextField
source="room_id" source="room_id"
@ -183,3 +178,12 @@ export const DestinationShow = props => {
</Show> </Show>
); );
}; };
const resource = {
name: "destinations",
icon: DestinationsIcon,
list: DestinationList,
show: DestinationShow,
};
export default resource;

View File

@ -1,4 +1,4 @@
import React, { Fragment, useState } from "react"; import React, { useState } from "react";
import { import {
Button, Button,
useDelete, useDelete,
@ -8,34 +8,16 @@ import {
useRefresh, useRefresh,
} from "react-admin"; } from "react-admin";
import ActionDelete from "@mui/icons-material/Delete"; import ActionDelete from "@mui/icons-material/Delete";
import { makeStyles } from "@material-ui/core/styles"; import { alpha, useTheme } from "@mui/material/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" }
);
export const DeviceRemoveButton = props => { export const DeviceRemoveButton = props => {
const theme = useTheme();
const record = useRecordContext(); const record = useRecordContext();
const classes = useStyles(props);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const refresh = useRefresh(); const refresh = useRefresh();
const notify = useNotify(); const notify = useNotify();
const [removeDevice, { isLoading }] = useDelete("devices"); const [removeDevice, { isLoading }] = useDelete();
if (!record) return null; if (!record) return null;
@ -44,13 +26,15 @@ export const DeviceRemoveButton = props => {
const handleConfirm = () => { const handleConfirm = () => {
removeDevice( removeDevice(
{ payload: { id: record.id, user_id: record.user_id } }, "devices",
// needs previousData for user_id
{ id: record.id, previousData: record },
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.devices.action.erase.success"); notify("resources.devices.action.erase.success");
refresh(); refresh();
}, },
onFailure: () => { onError: () => {
notify("resources.devices.action.erase.failure", { type: "error" }); notify("resources.devices.action.erase.failure", { type: "error" });
}, },
} }
@ -59,11 +43,21 @@ export const DeviceRemoveButton = props => {
}; };
return ( return (
<Fragment> <>
<Button <Button
{...props}
label="ra.action.remove" label="ra.action.remove"
onClick={handleClick} 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 /> <ActionDelete />
</Button> </Button>
@ -79,6 +73,6 @@ export const DeviceRemoveButton = props => {
name: record.display_name ? record.display_name : record.id, name: record.display_name ? record.display_name : record.id,
}} }}
/> />
</Fragment> </>
); );
}; };

View File

@ -1,7 +1,4 @@
import React, { Fragment, useState } from "react"; import React, { useState } from "react";
import classnames from "classnames";
import { alpha } from "@mui/material/styles";
import { makeStyles } from "@material-ui/core/styles";
import { import {
BooleanInput, BooleanInput,
Button, Button,
@ -30,24 +27,9 @@ import {
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@mui/icons-material/Cancel";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen"; import LockOpenIcon from "@mui/icons-material/LockOpen";
import { alpha, useTheme } from "@mui/material/styles";
const useStyles = makeStyles( const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
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" }
);
const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
const translate = useTranslate(); const translate = useTranslate();
const dateParser = v => { const dateParser = v => {
@ -56,8 +38,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
return d.getTime(); return d.getTime();
}; };
const DeleteMediaToolbar = props => { const DeleteMediaToolbar = props => (
return (
<Toolbar {...props}> <Toolbar {...props}>
<SaveButton <SaveButton
label="resources.delete_media.action.send" label="resources.delete_media.action.send"
@ -68,7 +49,6 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
</Button> </Button>
</Toolbar> </Toolbar>
); );
};
return ( return (
<Dialog open={open} onClose={onClose} loading={loading}> <Dialog open={open} onClose={onClose} loading={loading}>
@ -79,12 +59,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
<DialogContentText> <DialogContentText>
{translate("resources.delete_media.helper.send")} {translate("resources.delete_media.helper.send")}
</DialogContentText> </DialogContentText>
<SimpleForm <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
toolbar={<DeleteMediaToolbar />}
submitOnEnter={false}
redirect={false}
save={onSend}
>
<DateTimeInput <DateTimeInput
fullWidth fullWidth
source="before_ts" source="before_ts"
@ -113,23 +88,25 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
}; };
export const DeleteMediaButton = props => { export const DeleteMediaButton = props => {
const classes = useStyles(props); const theme = useTheme();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const notify = useNotify(); const notify = useNotify();
const [deleteOne, { loading }] = useDelete("delete_media"); const [deleteOne, { isLoading }] = useDelete();
const handleDialogOpen = () => setOpen(true); const openDialog = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const closeDialog = () => setOpen(false);
const handleSend = values => { const deleteMedia = values => {
deleteOne( deleteOne(
{ payload: { ...values } }, "delete_media",
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
{ meta: values },
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.delete_media.action.send_success"); notify("resources.delete_media.action.send_success");
handleDialogClose(); closeDialog();
}, },
onFailure: () => onError: () =>
notify("resources.delete_media.action.send_failure", { notify("resources.delete_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -138,43 +115,54 @@ export const DeleteMediaButton = props => {
}; };
return ( return (
<Fragment> <>
<Button <Button
{...props}
label="resources.delete_media.action.send" label="resources.delete_media.action.send"
onClick={handleDialogOpen} onClick={openDialog}
disabled={loading} disabled={isLoading}
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",
},
},
}}
> >
<DeleteSweepIcon /> <DeleteSweepIcon />
</Button> </Button>
<DeleteMediaDialog <DeleteMediaDialog
open={open} open={open}
onClose={handleDialogClose} onClose={closeDialog}
onSend={handleSend} onSubmit={deleteMedia}
/> />
</Fragment> </>
); );
}; };
export const ProtectMediaButton = props => { export const ProtectMediaButton = () => {
const record = useRecordContext(); const record = useRecordContext();
const translate = useTranslate(); const translate = useTranslate();
const refresh = useRefresh(); const refresh = useRefresh();
const notify = useNotify(); const notify = useNotify();
const [create, { loading }] = useCreate("protect_media"); const [create, { isLoading }] = useCreate();
const [deleteOne] = useDelete("protect_media"); const [deleteOne] = useDelete();
if (!record) return null; if (!record) return null;
const handleProtect = () => { const handleProtect = () => {
create( create(
{ payload: { data: record } }, "protect_media",
{ data: record },
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.protect_media.action.send_success"); notify("resources.protect_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.protect_media.action.send_failure", { notify("resources.protect_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -184,13 +172,14 @@ export const ProtectMediaButton = props => {
const handleUnprotect = () => { const handleUnprotect = () => {
deleteOne( deleteOne(
{ payload: { ...record } }, "protect_media",
{ id: record.id },
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.protect_media.action.send_success"); notify("resources.protect_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.protect_media.action.send_failure", { notify("resources.protect_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -203,7 +192,7 @@ export const ProtectMediaButton = props => {
Wrapping Tooltip with <div> Wrapping Tooltip with <div>
https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735 https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
*/ */
<Fragment> <>
{record.quarantined_by && ( {record.quarantined_by && (
<Tooltip <Tooltip
title={translate("resources.protect_media.action.none", { title={translate("resources.protect_media.action.none", {
@ -229,7 +218,7 @@ export const ProtectMediaButton = props => {
arrow arrow
> >
<div> <div>
<Button onClick={handleUnprotect} disabled={loading}> <Button onClick={handleUnprotect} disabled={isLoading}>
<LockIcon /> <LockIcon />
</Button> </Button>
</div> </div>
@ -242,13 +231,13 @@ export const ProtectMediaButton = props => {
})} })}
> >
<div> <div>
<Button onClick={handleProtect} disabled={loading}> <Button onClick={handleProtect} disabled={isLoading}>
<LockOpenIcon /> <LockOpenIcon />
</Button> </Button>
</div> </div>
</Tooltip> </Tooltip>
)} )}
</Fragment> </>
); );
}; };
@ -257,20 +246,21 @@ export const QuarantineMediaButton = props => {
const translate = useTranslate(); const translate = useTranslate();
const refresh = useRefresh(); const refresh = useRefresh();
const notify = useNotify(); const notify = useNotify();
const [create, { loading }] = useCreate("quarantine_media"); const [create, { isLoading }] = useCreate();
const [deleteOne] = useDelete("quarantine_media"); const [deleteOne] = useDelete();
if (!record) return null; if (!record) return null;
const handleQuarantaine = () => { const handleQuarantaine = () => {
create( create(
{ payload: { data: record } }, "quarantine_media",
{ data: record },
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.quarantine_media.action.send_success"); notify("resources.quarantine_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.quarantine_media.action.send_failure", { notify("resources.quarantine_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -280,13 +270,14 @@ export const QuarantineMediaButton = props => {
const handleRemoveQuarantaine = () => { const handleRemoveQuarantaine = () => {
deleteOne( deleteOne(
{ payload: { ...record } }, "quarantine_media",
{ id: record.id, previousData: record },
{ {
onSuccess: () => { onSuccess: () => {
notify("resources.quarantine_media.action.send_success"); notify("resources.quarantine_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.quarantine_media.action.send_failure", { notify("resources.quarantine_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -295,7 +286,7 @@ export const QuarantineMediaButton = props => {
}; };
return ( return (
<Fragment> <>
{record.safe_from_quarantine && ( {record.safe_from_quarantine && (
<Tooltip <Tooltip
title={translate("resources.quarantine_media.action.none", { title={translate("resources.quarantine_media.action.none", {
@ -303,7 +294,7 @@ export const QuarantineMediaButton = props => {
})} })}
> >
<div> <div>
<Button disabled={true}> <Button {...props} disabled={true}>
<ClearIcon /> <ClearIcon />
</Button> </Button>
</div> </div>
@ -316,7 +307,11 @@ export const QuarantineMediaButton = props => {
})} })}
> >
<div> <div>
<Button onClick={handleRemoveQuarantaine} disabled={loading}> <Button
{...props}
onClick={handleRemoveQuarantaine}
disabled={isLoading}
>
<BlockIcon color="error" /> <BlockIcon color="error" />
</Button> </Button>
</div> </div>
@ -329,12 +324,12 @@ export const QuarantineMediaButton = props => {
})} })}
> >
<div> <div>
<Button onClick={handleQuarantaine} disabled={loading}> <Button onClick={handleQuarantaine} disabled={isLoading}>
<BlockIcon /> <BlockIcon />
</Button> </Button>
</div> </div>
</Tooltip> </Tooltip>
)} )}
</Fragment> </>
); );
}; };

View File

@ -1,18 +1,20 @@
import React, { Fragment } from "react"; import React from "react";
import { connect } from "react-redux";
import { import {
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
DateField, DateField,
Datagrid, Datagrid,
DatagridConfigurable,
DeleteButton, DeleteButton,
Filter, ExportButton,
FunctionField,
List, List,
NumberField, NumberField,
Pagination, Pagination,
ReferenceField, ReferenceField,
ReferenceManyField, ReferenceManyField,
SearchInput, SearchInput,
SelectColumnsButton,
SelectField, SelectField,
Show, Show,
Tab, Tab,
@ -22,10 +24,8 @@ import {
useRecordContext, useRecordContext,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import get from "lodash/get"; import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types"; import Box from "@mui/material/Box";
import { makeStyles } from "@material-ui/core/styles";
import { Tooltip, Typography, Chip } from "@mui/material";
import FastForwardIcon from "@mui/icons-material/FastForward"; import FastForwardIcon from "@mui/icons-material/FastForward";
import HttpsIcon from "@mui/icons-material/Https"; import HttpsIcon from "@mui/icons-material/Https";
import NoEncryptionIcon from "@mui/icons-material/NoEncryption"; import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
@ -34,11 +34,12 @@ import UserIcon from "@mui/icons-material/Group";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import EventIcon from "@mui/icons-material/Event"; import EventIcon from "@mui/icons-material/Event";
import RoomIcon from "@mui/icons-material/ViewList";
import { import {
RoomDirectoryBulkDeleteButton, RoomDirectoryBulkUnpublishButton,
RoomDirectoryBulkSaveButton, RoomDirectoryBulkPublishButton,
RoomDirectoryDeleteButton, RoomDirectoryUnpublishButton,
RoomDirectorySaveButton, RoomDirectoryPublishButton,
} from "./RoomDirectory"; } from "./RoomDirectory";
const date_format = { const date_format = {
@ -50,44 +51,11 @@ const date_format = {
second: "2-digit", second: "2-digit",
}; };
const useStyles = makeStyles(theme => ({ const RoomPagination = () => (
helper_forward_extremities: { <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
margin: "0.5em",
},
}));
const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
const EncryptionField = ({ source, record = {}, emptyText }) => { const RoomTitle = () => {
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 record = useRecordContext();
const translate = useTranslate(); const translate = useTranslate();
var name = ""; var name = "";
@ -102,24 +70,18 @@ const RoomTitle = props => {
); );
}; };
const RoomShowActions = ({ basePath, data, resource }) => { const RoomShowActions = () => {
const record = useRecordContext();
var roomDirectoryStatus = ""; var roomDirectoryStatus = "";
if (data) { if (record) {
roomDirectoryStatus = data.public; roomDirectoryStatus = record.public;
} }
return ( return (
<TopToolbar> <TopToolbar>
{roomDirectoryStatus === false && ( {roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
<RoomDirectorySaveButton record={data} /> {roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
)}
{roomDirectoryStatus === true && (
<RoomDirectoryDeleteButton record={data} />
)}
<DeleteButton <DeleteButton
basePath={basePath}
record={data}
resource={resource}
mutationMode="pessimistic" mutationMode="pessimistic"
confirmTitle="resources.rooms.action.erase.title" confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content" confirmContent="resources.rooms.action.erase.content"
@ -129,7 +91,6 @@ const RoomShowActions = ({ basePath, data, resource }) => {
}; };
export const RoomShow = props => { export const RoomShow = props => {
const classes = useStyles({ props });
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}> <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
@ -171,7 +132,7 @@ export const RoomShow = props => {
> >
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, basePath, record) => "/users/" + id} rowClick={(id, resource, record) => "/users/" + id}
> >
<TextField <TextField
source="id" source="id"
@ -281,9 +242,14 @@ export const RoomShow = props => {
icon={<FastForwardIcon />} icon={<FastForwardIcon />}
path="forward_extremities" path="forward_extremities"
> >
<div className={classes.helper_forward_extremities}> <Box
sx={{
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
margin: "0.5em",
}}
>
{translate("resources.rooms.helper.forward_extremities")} {translate("resources.rooms.helper.forward_extremities")}
</div> </Box>
<ReferenceManyField <ReferenceManyField
reference="forward_extremities" reference="forward_extremities"
target="room_id" target="room_id"
@ -307,104 +273,81 @@ export const RoomShow = props => {
); );
}; };
const RoomBulkActionButtons = props => ( const RoomBulkActionButtons = () => (
<Fragment> <>
<RoomDirectoryBulkSaveButton {...props} /> <RoomDirectoryBulkPublishButton />
<RoomDirectoryBulkDeleteButton {...props} /> <RoomDirectoryBulkUnpublishButton />
<BulkDeleteButton <BulkDeleteButton
{...props}
confirmTitle="resources.rooms.action.erase.title" confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content" confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic" mutationMode="pessimistic"
/> />
</Fragment> </>
); );
const RoomFilter = ({ ...props }) => { const roomFilters = [<SearchInput source="search_term" alwaysOn />];
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}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.state_events")}
source="state_events"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.version")}
source="version"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.federatable")}
source="federatable"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
</Filter>
);
};
const RoomNameField = props => { const RoomListActions = () => (
const { source } = props; <TopToolbar>
const record = useRecordContext(); <SelectColumnsButton />
return ( <ExportButton />
<span>{record[source] || record["canonical_alias"] || record["id"]}</span> </TopToolbar>
); );
};
RoomNameField.propTypes = { export const RoomList = props => {
label: PropTypes.string, const theme = useTheme();
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;
return ( return (
<List <List
{...props} {...props}
pagination={<RoomPagination />} pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }} sort={{ field: "name", order: "ASC" }}
filters={<RoomFilter />} filters={roomFilters}
bulkActionButtons={<RoomBulkActionButtons />} actions={<RoomListActions />}
> >
<Datagrid rowClick="show"> <DatagridConfigurable
<EncryptionField rowClick="show"
bulkActionButtons={<RoomBulkActionButtons />}
omit={[
"joined_local_members",
"state_events",
"version",
"federatable",
]}
>
<BooleanField
source="is_encrypted" source="is_encrypted"
sortBy="encryption" sortBy="encryption"
TrueIcon={HttpsIcon}
FalseIcon={NoEncryptionIcon}
label={<HttpsIcon />} 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" /> <TextField source="joined_members" />
{localMembersFilter && <TextField source="joined_local_members" />} <TextField source="joined_local_members" />
{stateEventsFilter && <TextField source="state_events" />} <TextField source="state_events" />
{versionFilter && <TextField source="version" />} <TextField source="version" />
{federateableFilter && <BooleanField source="federatable" />} <BooleanField source="federatable" />
<BooleanField source="public" /> <BooleanField source="public" />
</Datagrid> </DatagridConfigurable>
</List> </List>
); );
}; };
function mapStateToProps(state) { const resource = {
return { name: "rooms",
roomFilters: state.admin.resources.rooms.list.params.displayedFilters, icon: RoomIcon,
}; list: RoomList,
} show: RoomShow,
};
export const RoomList = connect(mapStateToProps)(FilterableRoomList); export default resource;

View File

@ -3,7 +3,6 @@ import { cloneElement } from "react";
import { import {
Datagrid, Datagrid,
ExportButton, ExportButton,
Filter,
List, List,
NumberField, NumberField,
Pagination, Pagination,
@ -13,18 +12,13 @@ import {
TopToolbar, TopToolbar,
useListContext, useListContext,
} from "react-admin"; } from "react-admin";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import { DeleteMediaButton } from "./media"; import { DeleteMediaButton } from "./media";
const ListActions = props => { const ListActions = props => {
const { className, exporter, filters, maxResults, ...rest } = props; const { className, exporter, filters, maxResults, ...rest } = props;
const { const { sort, resource, displayedFilters, filterValues, showFilter, total } =
currentSort, useListContext();
resource,
displayedFilters,
filterValues,
showFilter,
total,
} = useListContext();
return ( return (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}> <TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters && {filters &&
@ -39,7 +33,7 @@ const ListActions = props => {
<ExportButton <ExportButton
disabled={total === 0} disabled={total === 0}
resource={resource} resource={resource}
sort={currentSort} sort={sort}
filterValues={filterValues} filterValues={filterValues}
maxResults={maxResults} maxResults={maxResults}
/> />
@ -47,27 +41,24 @@ const ListActions = props => {
); );
}; };
const UserMediaStatsPagination = props => ( const UserMediaStatsPagination = () => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
const UserMediaStatsFilter = props => ( const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
<Filter {...props}>
<SearchInput source="search_term" alwaysOn />
</Filter>
);
export const UserMediaStatsList = props => { export const UserMediaStatsList = props => (
return (
<List <List
{...props} {...props}
actions={<ListActions />} actions={<ListActions />}
filters={<UserMediaStatsFilter />} filters={userMediaStatsFilters}
pagination={<UserMediaStatsPagination />} pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }} sort={{ field: "media_length", order: "DESC" }}
>
<Datagrid
rowClick={(id, resource, record) => "/users/" + id + "/media"}
bulkActionButtons={false} bulkActionButtons={false}
> >
<Datagrid rowClick={(id, basePath, record) => "/users/" + id + "/media"}>
<TextField source="user_id" label="resources.users.fields.id" /> <TextField source="user_id" label="resources.users.fields.id" />
<TextField <TextField
source="displayname" source="displayname"
@ -77,5 +68,12 @@ export const UserMediaStatsList = props => {
<NumberField source="media_length" /> <NumberField source="media_length" />
</Datagrid> </Datagrid>
</List> </List>
); );
const resource = {
name: "user_media_statistics",
icon: EqualizerIcon,
list: UserMediaStatsList,
}; };
export default resource;

View File

@ -1,5 +1,4 @@
import React, { cloneElement, Fragment } from "react"; import React, { cloneElement } from "react";
import Avatar from "@mui/material/Avatar";
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd"; import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import ContactMailIcon from "@mui/icons-material/ContactMail"; import ContactMailIcon from "@mui/icons-material/ContactMail";
import DevicesIcon from "@mui/icons-material/Devices"; import DevicesIcon from "@mui/icons-material/Devices";
@ -8,6 +7,7 @@ import NotificationsIcon from "@mui/icons-material/Notifications";
import PermMediaIcon from "@mui/icons-material/PermMedia"; import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonPinIcon from "@mui/icons-material/PersonPin"; import PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import UserIcon from "@mui/icons-material/Group";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import { import {
ArrayInput, ArrayInput,
@ -18,7 +18,6 @@ import {
Create, Create,
Edit, Edit,
List, List,
Filter,
Toolbar, Toolbar,
SimpleForm, SimpleForm,
SimpleFormIterator, SimpleFormIterator,
@ -49,28 +48,10 @@ import {
NumberField, NumberField,
} from "react-admin"; } from "react-admin";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import AvatarField from "./AvatarField";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices"; import { DeviceRemoveButton } from "./devices";
import { ProtectMediaButton, QuarantineMediaButton } from "./media"; import { ProtectMediaButton, QuarantineMediaButton } from "./media";
import { makeStyles } from "@material-ui/core/styles";
const redirect = () => {
return {
pathname: "/import_users",
};
};
const useStyles = makeStyles({
small: {
height: "40px",
width: "40px",
},
large: {
height: "120px",
width: "120px",
float: "right",
},
});
const choices_medium = [ const choices_medium = [
{ id: "email", name: "resources.users.email" }, { id: "email", name: "resources.users.email" },
@ -92,7 +73,7 @@ const date_format = {
}; };
const UserListActions = ({ const UserListActions = ({
currentSort, sort,
className, className,
resource, resource,
filters, filters,
@ -101,7 +82,6 @@ const UserListActions = ({
filterValues, filterValues,
permanentFilter, permanentFilter,
hasCreate, // you can hide CreateButton if hasCreate = false hasCreate, // you can hide CreateButton if hasCreate = false
basePath,
selectedIds, selectedIds,
onUnselectItems, onUnselectItems,
showFilter, showFilter,
@ -119,18 +99,18 @@ const UserListActions = ({
filterValues, filterValues,
context: "button", context: "button",
})} })}
<CreateButton basePath={basePath} /> <CreateButton />
<ExportButton <ExportButton
disabled={total === 0} disabled={total === 0}
resource={resource} resource={resource}
sort={currentSort} sort={sort}
filter={{ ...filterValues, ...permanentFilter }} filter={{ ...filterValues, ...permanentFilter }}
exporter={exporter} exporter={exporter}
maxResults={maxResults} maxResults={maxResults}
/> />
{/* Add your custom actions */} {/* Add your custom actions */}
<Button component={Link} to={redirect} label="CSV Import"> <Button component={Link} to="/import_users" label="CSV Import">
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} /> <GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
</Button> </Button>
</TopToolbar> </TopToolbar>
); );
@ -141,54 +121,44 @@ UserListActions.defaultProps = {
onUnselectItems: () => null, onUnselectItems: () => null,
}; };
const UserPagination = props => ( const UserPagination = () => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
const UserFilter = props => ( const userFilters = [
<Filter {...props}> <SearchInput source="name" alwaysOn />,
<SearchInput source="name" alwaysOn /> <BooleanInput source="guests" alwaysOn />,
<BooleanInput source="guests" alwaysOn />
<BooleanInput <BooleanInput
label="resources.users.fields.show_deactivated" label="resources.users.fields.show_deactivated"
source="deactivated" source="deactivated"
alwaysOn alwaysOn
/> />,
</Filter> ];
);
const UserBulkActionButtons = props => ( const UserBulkActionButtons = () => (
<Fragment> <>
<ServerNoticeBulkButton {...props} /> <ServerNoticeBulkButton />
<BulkDeleteButton <BulkDeleteButton
{...props}
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase" confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic" mutationMode="pessimistic"
/> />
</Fragment> </>
); );
const AvatarField = ({ source, className, record = {} }) => ( export const UserList = props => (
<Avatar src={record[source]} className={className} />
);
export const UserList = props => {
const classes = useStyles();
return (
<List <List
{...props} {...props}
filters={<UserFilter />} filters={userFilters}
filterDefaultValues={{ guests: true, deactivated: false }} filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }} sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />} actions={<UserListActions maxResults={10000} />}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />} pagination={<UserPagination />}
> >
<Datagrid rowClick="edit"> <Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField <AvatarField
source="avatar_src" source="avatar_src"
className={classes.small} sx={{ height: "40px", width: "40px" }}
sortBy="avatar_url" sortBy="avatar_url"
/> />
<TextField source="id" sortBy="name" /> <TextField source="id" sortBy="name" />
@ -205,8 +175,7 @@ export const UserList = props => {
/> />
</Datagrid> </Datagrid>
</List> </List>
); );
};
// https://matrix.org/docs/spec/appendices#user-identifiers // https://matrix.org/docs/spec/appendices#user-identifiers
// here only local part of user_id // here only local part of user_id
@ -263,7 +232,7 @@ export function generateRandomUser() {
const UserEditToolbar = props => ( const UserEditToolbar = props => (
<Toolbar {...props}> <Toolbar {...props}>
<SaveButton submitOnEnter={true} disabled={props.pristine} /> <SaveButton disabled={props.pristine} />
</Toolbar> </Toolbar>
); );
@ -303,7 +272,6 @@ export const UserCreate = props => (
source="user_type" source="user_type"
choices={choices_type} choices={choices_type}
translateChoice={false} translateChoice={false}
allowEmpty={true}
resettable resettable
/> />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
@ -331,7 +299,7 @@ export const UserCreate = props => (
</Create> </Create>
); );
const UserTitle = props => { const UserTitle = () => {
const record = useRecordContext(); const record = useRecordContext();
const translate = useTranslate(); const translate = useTranslate();
return ( return (
@ -345,7 +313,6 @@ const UserTitle = props => {
}; };
export const UserEdit = props => { export const UserEdit = props => {
const classes = useStyles();
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}> <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
@ -357,7 +324,7 @@ export const UserEdit = props => {
<AvatarField <AvatarField
source="avatar_src" source="avatar_src"
sortable={false} sortable={false}
className={classes.large} sx={{ height: "120px", width: "120px", float: "right" }}
/> />
<TextInput source="id" disabled /> <TextInput source="id" disabled />
<TextInput source="displayname" /> <TextInput source="displayname" />
@ -370,7 +337,6 @@ export const UserEdit = props => {
source="user_type" source="user_type"
choices={choices_type} choices={choices_type}
translateChoice={false} translateChoice={false}
allowEmpty={true}
resettable resettable
/> />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
@ -515,7 +481,7 @@ export const UserEdit = props => {
> >
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, basePath, record) => "/rooms/" + id + "/show"} rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
> >
<TextField <TextField
source="id" source="id"
@ -561,3 +527,13 @@ export const UserEdit = props => {
</Edit> </Edit>
); );
}; };
const resource = {
name: "users",
icon: UserIcon,
list: UserList,
edit: UserEdit,
create: UserCreate,
};
export default resource;

View File

@ -98,7 +98,7 @@ const resourceMap = {
}), }),
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent( endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
params.user_id params.previousData.user_id
)}/devices/${params.id}`, )}/devices/${params.id}`,
}), }),
}, },
@ -184,9 +184,9 @@ const resourceMap = {
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem( endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
"home_server" "home_server"
)}/delete?before_ts=${params.before_ts}&size_gt=${ )}/delete?before_ts=${params.meta.before_ts}&size_gt=${
params.size_gt params.meta.size_gt
}&keep_profiles=${params.keep_profiles}`, }&keep_profiles=${params.meta.keep_profiles}`,
method: "POST", method: "POST",
}), }),
}, },
@ -197,7 +197,7 @@ const resourceMap = {
method: "POST", method: "POST",
}), }),
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v1/media/unprotect/${params.media_id}`, endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
method: "POST", method: "POST",
}), }),
}, },
@ -212,7 +212,7 @@ const resourceMap = {
delete: params => ({ delete: params => ({
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem( endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
"home_server" "home_server"
)}/${params.media_id}`, )}/${params.id}`,
method: "POST", method: "POST",
}), }),
}, },
@ -546,7 +546,7 @@ const dataProvider = {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.id}`, { return jsonClient(`${endpoint_url}/${params.id}`, {
method: "DELETE", method: "DELETE",
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(params.previousData, filterNullValues),
}).then(({ json }) => ({ }).then(({ json }) => ({
data: json, data: json,
})); }));

6004
yarn.lock

File diff suppressed because it is too large Load Diff