Merge tag '0.8.1' into amp.chat

Change-Id: Ice2d3474ef9284bcfea81a4e0043d798edcfaa03
This commit is contained in:
Manuel Stahl 2021-05-25 16:00:00 +01:00
commit 364234f4f4
18 changed files with 3334 additions and 2919 deletions

5
.env Normal file
View File

@ -0,0 +1,5 @@
# This setting allows to fix the homeserver.
# If you set this setting, the user will not be able to select
# the server and have to use synapse-admin with this server.
#REACT_APP_SERVER=https://yourmatrixserver.example.com

View File

@ -1,5 +1,5 @@
language: node_js
node_js:
- 13
- lts/*
cache: yarn

View File

@ -4,7 +4,7 @@
This project is built using [react-admin](https://marmelab.com/react-admin/).
It needs at least Synapse v1.27.0 for all functions to work as expected!
It needs at least Synapse v1.32.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/version_api.rst).
@ -33,6 +33,11 @@ Steps for 1):
- download dependencies: `yarn install`
- start web server: `yarn start`
You can fix the homeserver, so that the user can no longer define it himself.
Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver.example.com yarn start`)
or by editing it in the [.env](.env) file. See also the
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
Steps for 2):
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the (docker-compose.yml)[docker-compose.yml]: `docker-compose up -d`

View File

@ -11,16 +11,14 @@
},
"devDependencies": {
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^10.0.2",
"@testing-library/user-event": "^12.0.11",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.8",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.1.2",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.0.0",
"ra-test": "^3.14.0"
"prettier": "^2.2.0",
"ra-test": "^3.15.0"
},
"dependencies": {
"@progress/kendo-drawing": "^1.6.0",
@ -30,11 +28,11 @@
"prop-types": "^15.7.2",
"qrcode.react": "^1.0.0",
"ra-language-chinese": "^2.0.10",
"ra-language-german": "^2.1.2",
"ra-language-german": "^3.13.4",
"react": "^17.0.0",
"react-admin": "^3.14.0",
"react-dom": "^16.14.0",
"react-scripts": "^3.4.4"
"react-admin": "^3.15.0",
"react-dom": "^17.0.2",
"react-scripts": "^4.0.0"
},
"scripts": {
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",

View File

@ -6,6 +6,7 @@ import dataProvider from "./synapse/dataProvider";
import { UserList, UserCreate, UserEdit } from "./components/users";
import { RoomList, RoomCreate, RoomShow, RoomEdit } from "./components/rooms";
import { ReportList, ReportShow } from "./components/EventReports";
import ImportFeature from "./components/ImportFeature";
import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group";
import EqualizerIcon from "@material-ui/icons/Equalizer";
@ -81,6 +82,7 @@ const App = () => (
<Resource name="joined_rooms" />
<Resource name="pushers" />
<Resource name="servernotices" />
<Resource name="forward_extremities" />
<Resource name="room_state" />
</Admin>
);

View File

@ -1,14 +1,9 @@
import React from "react";
import { TestContext } from "ra-test";
import { shallow } from "enzyme";
import { render } from "@testing-library/react";
import App from "./App";
describe("App", () => {
it("renders", () => {
shallow(
<TestContext>
<App />
</TestContext>
);
render(<App />);
});
});

View File

@ -82,6 +82,7 @@ const LoginPage = ({ theme }) => {
const setLocale = useSetLocale();
const translate = useTranslate();
const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER;
const renderInput = ({
meta: { touched, error } = {},
@ -111,7 +112,9 @@ const LoginPage = ({ theme }) => {
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})?$/)
!values.base_url.match(
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/
)
) {
errors.base_url = translate("synapseadmin.auth.url_error");
}
@ -147,7 +150,7 @@ const LoginPage = ({ theme }) => {
const [serverVersion, setServerVersion] = useState("");
const handleUsernameChange = _ => {
if (formData.base_url) return;
if (formData.base_url || cfg_base_url) return;
// check if username is a full qualified userId then set base_url accordially
const home_server = extractHomeServer(formData.username);
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
@ -199,6 +202,7 @@ const LoginPage = ({ theme }) => {
label={translate("ra.auth.username")}
disabled={loading}
onBlur={handleUsernameChange}
resettable
fullWidth
/>
</div>
@ -209,6 +213,7 @@ const LoginPage = ({ theme }) => {
label={translate("ra.auth.password")}
type="password"
disabled={loading}
resettable
fullWidth
/>
</div>
@ -217,7 +222,8 @@ const LoginPage = ({ theme }) => {
name="base_url"
component={renderInput}
label={translate("synapseadmin.auth.base_url")}
disabled={loading}
disabled={cfg_base_url || loading}
resettable
fullWidth
/>
</div>
@ -228,7 +234,7 @@ const LoginPage = ({ theme }) => {
return (
<Form
initialValues={{ base_url: base_url }}
initialValues={{ base_url: cfg_base_url || base_url }}
onSubmit={handleSubmit}
validate={validate}
render={({ handleSubmit }) => (

View File

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

View File

@ -171,7 +171,7 @@ const RoomDirectoryFilter = ({ ...props }) => {
);
};
export const FilterableRoomDirectoryList = ({ ...props }) => {
export const FilterableRoomDirectoryList = ({ dispatch, ...props }) => {
const classes = useStyles();
const translate = useTranslate();
const filter = props.roomDirectoryFilters;
@ -242,7 +242,7 @@ export const FilterableRoomDirectoryList = ({ ...props }) => {
function mapStateToProps(state) {
return {
roomDirectoryFilters:
roomdirectoryfilters:
state.admin.resources.room_directory.list.params.displayedFilters,
};
}

View File

@ -16,6 +16,7 @@ import {
Filter,
FormTab,
List,
NumberField,
Pagination,
ReferenceArrayInput,
ReferenceField,
@ -33,10 +34,13 @@ import {
Toolbar,
TopToolbar,
useDataProvider,
useRecordContext,
useRefresh,
useTranslate,
} from "react-admin";
import get from "lodash/get";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import {
Tooltip,
Typography,
@ -47,6 +51,7 @@ import {
Select,
MenuItem,
} from "@material-ui/core";
import FastForwardIcon from "@material-ui/icons/FastForward";
import HttpsIcon from "@material-ui/icons/Https";
import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
import PageviewIcon from "@material-ui/icons/Pageview";
@ -62,6 +67,13 @@ import {
RoomDirectorySaveButton,
} from "./RoomDirectory";
const useStyles = makeStyles(theme => ({
helper_forward_extremities: {
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
margin: "0.5em",
},
}));
const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
@ -481,6 +493,7 @@ const RoomShowActions = ({ basePath, data, resource }) => {
};
export const RoomShow = props => {
const classes = useStyles({ props });
const translate = useTranslate();
return (
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
@ -592,6 +605,7 @@ export const RoomShow = props => {
]}
/>
</Tab>
<Tab
label={translate("resources.room_state.name", { smart_count: 2 })}
icon={<EventIcon />}
@ -628,6 +642,40 @@ export const RoomShow = props => {
</Datagrid>
</ReferenceManyField>
</Tab>
<Tab
label="resources.forward_extremities.name"
icon={<FastForwardIcon />}
path="forward_extremities"
>
<div className={classes.helper_forward_extremities}>
{translate("resources.rooms.helper.forward_extremities")}
</div>
<ReferenceManyField
reference="forward_extremities"
target="room_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }}>
<TextField source="id" sortable={false} />
<DateField
source="received_ts"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<NumberField source="depth" sortable={false} />
<TextField source="state_group" sortable={false} />
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
);
@ -679,8 +727,22 @@ const RoomFilter = ({ ...props }) => {
);
};
const FilterableRoomList = ({ ...props }) => {
const filter = props.roomFilters;
const RoomNameField = props => {
const { source } = props;
const record = useRecordContext(props);
return (
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
);
};
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;
@ -701,7 +763,7 @@ const FilterableRoomList = ({ ...props }) => {
sortBy="encryption"
label={<HttpsIcon />}
/>
<TextField source="name" />
<RoomNameField source="name" />
<TextField source="joined_members" />
{localMembersFilter && <TextField source="joined_local_members" />}
{stateEventsFilter && <TextField source="state_events" />}

View File

@ -158,6 +158,7 @@ export const UserList = props => {
{...props}
filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />}
@ -165,13 +166,13 @@ export const UserList = props => {
<Datagrid rowClick="edit">
<AvatarField
source="avatar_src"
sortable={false}
className={classes.small}
sortBy="avatar_url"
/>
<TextField source="id" sortable={false} />
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<SelectField
source="user_type"
choices={[
@ -180,7 +181,7 @@ export const UserList = props => {
{ id: "limited", name: "resources.users.type.limited" },
]}
/>
<BooleanField source="deactivated" sortable={false} />
<BooleanField source="deactivated" />
</Datagrid>
</List>
);
@ -480,6 +481,7 @@ export const UserEdit = props => {
addLabel={false}
pagination={<UserPagination />}
perPage={50}
sort={{ field: "created_ts", order: "DESC" }}
>
<Datagrid style={{ width: "100%" }}>
<DateField
@ -493,7 +495,6 @@ export const UserEdit = props => {
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<DateField
source="last_access_ts"
@ -506,14 +507,13 @@ export const UserEdit = props => {
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<TextField source="media_id" sortable={false} />
<NumberField source="media_length" sortable={false} />
<TextField source="media_type" sortable={false} />
<TextField source="upload_name" sortable={false} />
<TextField source="quarantined_by" sortable={false} />
<BooleanField source="safe_from_quarantine" sortable={false} />
<TextField source="media_id" />
<NumberField source="media_length" />
<TextField source="media_type" />
<TextField source="upload_name" />
<TextField source="quarantined_by" />
<BooleanField source="safe_from_quarantine" />
<DeleteButton mutationMode="pessimistic" redirect={false} />
</Datagrid>
</ReferenceManyField>

View File

@ -1,6 +1,6 @@
import germanMessages from "ra-language-german";
export default {
const de = {
...germanMessages,
synapseadmin: {
auth: {
@ -186,6 +186,10 @@ export default {
topic: "Thema",
avatar: "Avatar",
},
helper: {
forward_extremities:
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.",
},
enums: {
join_rules: {
public: "Öffentlich",
@ -329,6 +333,15 @@ export default {
media_length: "Größe der Dateien",
},
},
forward_extremities: {
name: "Vorderextremitäten",
fields: {
id: "Event-ID",
received_ts: "Zeitstempel",
depth: "Tiefe",
state_group: "Zustandsgruppe",
},
},
room_state: {
name: "Zustandsereignisse",
fields: {
@ -383,5 +396,10 @@ export default {
empty: "Keine Einträge vorhanden",
invite: "",
},
navigation: {
...germanMessages.ra.navigation,
skip_nav: "Zum Inhalt springen",
},
},
};
export default de;

View File

@ -1,6 +1,6 @@
import englishMessages from "ra-language-english";
export default {
const en = {
...englishMessages,
synapseadmin: {
auth: {
@ -184,6 +184,10 @@ export default {
topic: "Topic",
avatar: "Avatar",
},
helper: {
forward_extremities:
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.",
},
enums: {
join_rules: {
public: "Public",
@ -262,7 +266,7 @@ export default {
name: "Media",
fields: {
media_id: "Media ID",
media_length: "Lenght",
media_length: "File Size (in Bytes)",
media_type: "Type",
upload_name: "File name",
quarantined_by: "Quarantined by",
@ -325,6 +329,15 @@ export default {
media_length: "Media length",
},
},
forward_extremities: {
name: "Forward Extremities",
fields: {
id: "Event ID",
received_ts: "Timestamp",
depth: "Depth",
state_group: "State group",
},
},
room_state: {
name: "State events",
fields: {
@ -353,3 +366,4 @@ export default {
},
},
};
export default en;

View File

@ -1,6 +1,6 @@
import chineseMessages from "ra-language-chinese";
export default {
const zh = {
...chineseMessages,
synapseadmin: {
auth: {
@ -288,3 +288,4 @@ export default {
},
},
};
export default zh;

View File

@ -1,6 +1,3 @@
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import fetchMock from "jest-fetch-mock";
configure({ adapter: new Adapter() });
fetchMock.enableMocks();

View File

@ -3,6 +3,9 @@ import { fetchUtils } from "react-admin";
const authProvider = {
// called when the user attempts to log in
login: ({ base_url, username, password }) => {
// force homeserver for protection in case the form is manipulated
base_url = process.env.REACT_APP_SERVER || base_url;
console.log("login ");
const options = {
method: "POST",
@ -10,6 +13,7 @@ const authProvider = {
type: "m.login.password",
user: username,
password: password,
device_id: localStorage.getItem("device_id"),
initial_device_display_name: "Synapse Admin",
}),
};
@ -17,6 +21,7 @@ const authProvider = {
// use the base_url from login instead of the well_known entry from the
// server, since the admin might want to access the admin API via some
// private address
base_url = base_url.replace(/\/+$/g, "");
localStorage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url);
@ -48,7 +53,6 @@ const authProvider = {
if (typeof access_token === "string") {
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
localStorage.removeItem("access_token");
localStorage.removeItem("device_id");
});
}
return Promise.resolve();

View File

@ -248,6 +248,22 @@ const resourceMap = {
return json.total;
},
},
forward_extremities: {
map: fe => ({
...fe,
id: fe.event_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
}),
data: "results",
total: json => {
return json.count;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
}),
},
room_directory: {
path: "/_matrix/client/r0/publicRooms",
map: rd => ({
@ -354,10 +370,13 @@ const dataProvider = {
getManyReference: (resource, params) => {
console.log("getManyReference " + resource);
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const from = (page - 1) * perPage;
const query = {
from: from,
limit: perPage,
order_by: field,
dir: getSearchOrder(order),
};
const homeserver = localStorage.getItem("base_url");

6021
yarn.lock

File diff suppressed because it is too large Load Diff