Compare commits
25 Commits
amp.chat
...
AMP/2021.07
| Author | SHA1 | Date | |
|---|---|---|---|
| af51cca461 | |||
| ff0201273a | |||
| e50c95b4be | |||
| 9f16e5c6ba | |||
| 509a45cba4 | |||
| 5c6e5a9641 | |||
| e3d5d51342 | |||
| 364234f4f4 | |||
| 1fe0b2f330 | |||
| 1aaa137afe | |||
| 985673b161 | |||
| d72357f64f | |||
| e19c34324b | |||
| 3ea1f51eb5 | |||
| 229518e456 | |||
| 5a5a7143af | |||
| dda8ba5e85 | |||
| 5208198b76 | |||
| c8082a7198 | |||
| 10831796e3 | |||
| 5ee5288edf | |||
| a5528d9fe7 | |||
| e2fd934851 | |||
| 0bc1ce3226 | |||
| 41ce58bac8 |
@@ -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
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 13
|
- lts/*
|
||||||
|
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
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.34.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://github.com/matrix-org/synapse/blob/develop/docs/admin_api/version_api.rst).
|
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`
|
- download dependencies: `yarn install`
|
||||||
- start web server: `yarn start`
|
- 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):
|
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`
|
- 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`
|
||||||
|
|||||||
+11
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "AMP/2021.05",
|
"version": "AMP/2021.07",
|
||||||
"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",
|
||||||
@@ -11,16 +11,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.1.1",
|
"@testing-library/jest-dom": "^5.1.1",
|
||||||
"@testing-library/react": "^10.0.2",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@testing-library/user-event": "^12.0.11",
|
"@testing-library/user-event": "^13.1.8",
|
||||||
"enzyme": "^3.11.0",
|
"eslint": "^7.25.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.2",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint": "^6.8.0",
|
|
||||||
"eslint-config-prettier": "^6.10.1",
|
|
||||||
"eslint-plugin-prettier": "^3.1.2",
|
"eslint-plugin-prettier": "^3.1.2",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^2.0.0",
|
"prettier": "^2.2.0",
|
||||||
"ra-test": "^3.14.0"
|
"ra-test": "^3.15.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@progress/kendo-drawing": "^1.6.0",
|
"@progress/kendo-drawing": "^1.6.0",
|
||||||
@@ -30,11 +28,11 @@
|
|||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"qrcode.react": "^1.0.0",
|
"qrcode.react": "^1.0.0",
|
||||||
"ra-language-chinese": "^2.0.10",
|
"ra-language-chinese": "^2.0.10",
|
||||||
"ra-language-german": "^2.1.2",
|
"ra-language-german": "^3.13.4",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-admin": "^3.14.0",
|
"react-admin": "^3.15.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-scripts": "^3.4.4"
|
"react-scripts": "^4.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@ import dataProvider from "./synapse/dataProvider";
|
|||||||
import { UserList, UserCreate, UserEdit } from "./components/users";
|
import { UserList, UserCreate, UserEdit } from "./components/users";
|
||||||
import { RoomList, RoomCreate, RoomShow, RoomEdit } from "./components/rooms";
|
import { RoomList, RoomCreate, RoomShow, RoomEdit } from "./components/rooms";
|
||||||
import { ReportList, ReportShow } from "./components/EventReports";
|
import { ReportList, ReportShow } from "./components/EventReports";
|
||||||
|
import ImportFeature from "./components/ImportFeature";
|
||||||
import LoginPage from "./components/LoginPage";
|
import LoginPage from "./components/LoginPage";
|
||||||
import UserIcon from "@material-ui/icons/Group";
|
import UserIcon from "@material-ui/icons/Group";
|
||||||
import EqualizerIcon from "@material-ui/icons/Equalizer";
|
import EqualizerIcon from "@material-ui/icons/Equalizer";
|
||||||
@@ -13,7 +14,6 @@ import { UserMediaStatsList } from "./components/statistics";
|
|||||||
import RoomIcon from "@material-ui/icons/ViewList";
|
import RoomIcon from "@material-ui/icons/ViewList";
|
||||||
import ReportIcon from "@material-ui/icons/Warning";
|
import ReportIcon from "@material-ui/icons/Warning";
|
||||||
import FolderSharedIcon from "@material-ui/icons/FolderShared";
|
import FolderSharedIcon from "@material-ui/icons/FolderShared";
|
||||||
import { ImportFeature } from "./components/ImportFeature";
|
|
||||||
import { RoomDirectoryList } from "./components/RoomDirectory";
|
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";
|
||||||
@@ -40,6 +40,7 @@ const App = () => (
|
|||||||
dataProvider={dataProvider}
|
dataProvider={dataProvider}
|
||||||
i18nProvider={i18nProvider}
|
i18nProvider={i18nProvider}
|
||||||
customRoutes={[
|
customRoutes={[
|
||||||
|
<Route key="csvImport" path="/importcsv" component={ImportFeature} />,
|
||||||
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
|
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -81,6 +82,7 @@ const App = () => (
|
|||||||
<Resource name="joined_rooms" />
|
<Resource name="joined_rooms" />
|
||||||
<Resource name="pushers" />
|
<Resource name="pushers" />
|
||||||
<Resource name="servernotices" />
|
<Resource name="servernotices" />
|
||||||
|
<Resource name="forward_extremities" />
|
||||||
<Resource name="room_state" />
|
<Resource name="room_state" />
|
||||||
</Admin>
|
</Admin>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-7
@@ -1,14 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TestContext } from "ra-test";
|
import { render } from "@testing-library/react";
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
shallow(
|
render(<App />);
|
||||||
<TestContext>
|
|
||||||
<App />
|
|
||||||
</TestContext>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { useTranslate } from "ra-core";
|
import { useTranslate } from "ra-core";
|
||||||
import Container from "@material-ui/core/Container/Container";
|
import Container from "@material-ui/core/Container/Container";
|
||||||
import { generateRandomUser } from "./users";
|
import { generateRandomUser } from "./users";
|
||||||
|
import ShowUserPdf from "./ShowUserPdf";
|
||||||
|
|
||||||
const LOGGING = true;
|
const LOGGING = true;
|
||||||
|
|
||||||
@@ -59,6 +60,8 @@ const FilePicker = props => {
|
|||||||
|
|
||||||
const [progress, setProgress] = useState(null);
|
const [progress, setProgress] = useState(null);
|
||||||
|
|
||||||
|
const [pdfRecords, setPdfRecords] = useState(null);
|
||||||
|
|
||||||
const [importResults, setImportResults] = useState(null);
|
const [importResults, setImportResults] = useState(null);
|
||||||
const [skippedRecords, setSkippedRecords] = useState(null);
|
const [skippedRecords, setSkippedRecords] = useState(null);
|
||||||
|
|
||||||
@@ -66,17 +69,23 @@ const FilePicker = props => {
|
|||||||
const [passwordMode, setPasswordMode] = useState(true);
|
const [passwordMode, setPasswordMode] = useState(true);
|
||||||
const [useridMode, setUseridMode] = useState("ignore");
|
const [useridMode, setUseridMode] = useState("ignore");
|
||||||
|
|
||||||
|
const [showingPdf, setShowingPdf] = useState(false);
|
||||||
|
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
const onFileChange = async e => {
|
const onFileChange = async e => {
|
||||||
if (progress !== null) return;
|
if (progress !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (LOGGING) console.log("onFileChange was called");
|
||||||
setValues(null);
|
setValues(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setStats(null);
|
setStats(null);
|
||||||
|
setPdfRecords(null);
|
||||||
|
|
||||||
setImportResults(null);
|
setImportResults(null);
|
||||||
const file = e.target.files ? e.target.files[0] : null;
|
const file = e.target.files ? e.target.files[0] : null;
|
||||||
/* Let's refuse some unreasonably big files instead of freezing
|
/* Let's refuse some unreasonably big files instead of freezing
|
||||||
@@ -126,6 +135,11 @@ const FilePicker = props => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (eF.length !== 0) {
|
if (eF.length !== 0) {
|
||||||
|
if (LOGGING) {
|
||||||
|
console.log(meta.fields);
|
||||||
|
console.log(eF);
|
||||||
|
console.log(oF);
|
||||||
|
}
|
||||||
setError(
|
setError(
|
||||||
translate("import_users.error.required_field", { field: eF[0] })
|
translate("import_users.error.required_field", { field: eF[0] })
|
||||||
);
|
);
|
||||||
@@ -226,6 +240,9 @@ const FilePicker = props => {
|
|||||||
setProgress,
|
setProgress,
|
||||||
setError
|
setError
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setPdfRecords(results.recordsForPdf);
|
||||||
|
|
||||||
setImportResults(results);
|
setImportResults(results);
|
||||||
// offer CSV download of skipped or errored records
|
// offer CSV download of skipped or errored records
|
||||||
// (so that the user doesn't have to filter out successful
|
// (so that the user doesn't have to filter out successful
|
||||||
@@ -251,6 +268,8 @@ const FilePicker = props => {
|
|||||||
let skippedRecords = [];
|
let skippedRecords = [];
|
||||||
let erroredRecords = [];
|
let erroredRecords = [];
|
||||||
let succeededRecords = [];
|
let succeededRecords = [];
|
||||||
|
let recordsForPdf = [];
|
||||||
|
|
||||||
let changeStats = {
|
let changeStats = {
|
||||||
toAdmin: 0,
|
toAdmin: 0,
|
||||||
toGuest: 0,
|
toGuest: 0,
|
||||||
@@ -365,6 +384,14 @@ const FilePicker = props => {
|
|||||||
await dataProvider.create("users", { data: recordData });
|
await dataProvider.create("users", { data: recordData });
|
||||||
}
|
}
|
||||||
succeededRecords.push(recordData);
|
succeededRecords.push(recordData);
|
||||||
|
|
||||||
|
if (recordData.password !== undefined) {
|
||||||
|
recordsForPdf.push({
|
||||||
|
id: recordData.id,
|
||||||
|
password: recordData.password,
|
||||||
|
displayname: recordData.displayname,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -389,6 +416,7 @@ const FilePicker = props => {
|
|||||||
erroredRecords,
|
erroredRecords,
|
||||||
succeededRecords,
|
succeededRecords,
|
||||||
totalRecordCount: entriesCount,
|
totalRecordCount: entriesCount,
|
||||||
|
recordsForPdf,
|
||||||
changeStats,
|
changeStats,
|
||||||
wasDryRun: dryRun,
|
wasDryRun: dryRun,
|
||||||
};
|
};
|
||||||
@@ -618,6 +646,10 @@ const FilePicker = props => {
|
|||||||
<br />,
|
<br />,
|
||||||
]
|
]
|
||||||
: ""}
|
: ""}
|
||||||
|
{translate(
|
||||||
|
"import_users.cards.results.for_print",
|
||||||
|
importResults.recordsForPdf.length
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
{importResults.wasDryRun && [
|
{importResults.wasDryRun && [
|
||||||
translate("import_users.cards.results.simulated_only"),
|
translate("import_users.cards.results.simulated_only"),
|
||||||
@@ -655,6 +687,27 @@ const FilePicker = props => {
|
|||||||
</CardActions>
|
</CardActions>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let pdfDisplay =
|
||||||
|
pdfRecords && showingPdf && pdfRecords.length ? (
|
||||||
|
<ShowUserPdf records={pdfRecords} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
let pdfActions = pdfRecords ? (
|
||||||
|
<CardActions>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
onClick={e => {
|
||||||
|
setShowingPdf(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate("import_users.goToPdf")}
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
if (pdfRecords && showingPdf) {
|
||||||
|
return <Card>{pdfDisplay}</Card>;
|
||||||
|
} else {
|
||||||
let allCards = [];
|
let allCards = [];
|
||||||
if (uploadCard) allCards.push(uploadCard);
|
if (uploadCard) allCards.push(uploadCard);
|
||||||
if (errorCards) allCards.push(errorCards);
|
if (errorCards) allCards.push(errorCards);
|
||||||
@@ -662,6 +715,7 @@ const FilePicker = props => {
|
|||||||
if (statsCards) allCards.push(...statsCards);
|
if (statsCards) allCards.push(...statsCards);
|
||||||
if (startImportCard) allCards.push(startImportCard);
|
if (startImportCard) allCards.push(startImportCard);
|
||||||
if (resultsCard) allCards.push(resultsCard);
|
if (resultsCard) allCards.push(resultsCard);
|
||||||
|
if (pdfActions) allCards.push(pdfActions);
|
||||||
|
|
||||||
let cardContainer = <Card>{allCards}</Card>;
|
let cardContainer = <Card>{allCards}</Card>;
|
||||||
|
|
||||||
@@ -669,6 +723,7 @@ const FilePicker = props => {
|
|||||||
<Title defaultTitle={translate("import_users.title")} />,
|
<Title defaultTitle={translate("import_users.title")} />,
|
||||||
cardContainer,
|
cardContainer,
|
||||||
];
|
];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImportFeature = FilePicker;
|
export const ImportFeature = FilePicker;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
const setLocale = useSetLocale();
|
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 renderInput = ({
|
const renderInput = ({
|
||||||
meta: { touched, error } = {},
|
meta: { touched, error } = {},
|
||||||
@@ -111,7 +112,9 @@ const LoginPage = ({ theme }) => {
|
|||||||
if (!values.base_url.match(/^(http|https):\/\//)) {
|
if (!values.base_url.match(/^(http|https):\/\//)) {
|
||||||
errors.base_url = translate("synapseadmin.auth.protocol_error");
|
errors.base_url = translate("synapseadmin.auth.protocol_error");
|
||||||
} else if (
|
} 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");
|
errors.base_url = translate("synapseadmin.auth.url_error");
|
||||||
}
|
}
|
||||||
@@ -147,7 +150,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
const [serverVersion, setServerVersion] = useState("");
|
const [serverVersion, setServerVersion] = useState("");
|
||||||
|
|
||||||
const handleUsernameChange = _ => {
|
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
|
// check if username is a full qualified userId then set base_url accordially
|
||||||
const home_server = extractHomeServer(formData.username);
|
const home_server = extractHomeServer(formData.username);
|
||||||
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
|
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
|
||||||
@@ -199,6 +202,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
label={translate("ra.auth.username")}
|
label={translate("ra.auth.username")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onBlur={handleUsernameChange}
|
onBlur={handleUsernameChange}
|
||||||
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,6 +213,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
label={translate("ra.auth.password")}
|
label={translate("ra.auth.password")}
|
||||||
type="password"
|
type="password"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,7 +222,8 @@ const LoginPage = ({ theme }) => {
|
|||||||
name="base_url"
|
name="base_url"
|
||||||
component={renderInput}
|
component={renderInput}
|
||||||
label={translate("synapseadmin.auth.base_url")}
|
label={translate("synapseadmin.auth.base_url")}
|
||||||
disabled={loading}
|
disabled={cfg_base_url || loading}
|
||||||
|
resettable
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,7 +234,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
initialValues={{ base_url: base_url }}
|
initialValues={{ base_url: cfg_base_url || base_url }}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
render={({ handleSubmit }) => (
|
render={({ handleSubmit }) => (
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
import { TestContext } from "ra-test";
|
import { TestContext } from "ra-test";
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import LoginPage from "./LoginPage";
|
import LoginPage from "./LoginPage";
|
||||||
|
|
||||||
describe("LoginForm", () => {
|
describe("LoginForm", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
shallow(
|
render(
|
||||||
<TestContext>
|
<TestContext>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</TestContext>
|
</TestContext>
|
||||||
|
|||||||
@@ -171,10 +171,14 @@ const RoomDirectoryFilter = ({ ...props }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FilterableRoomDirectoryList = ({ ...props }) => {
|
export const FilterableRoomDirectoryList = ({
|
||||||
|
roomDirectoryFilters,
|
||||||
|
dispatch,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const filter = props.roomDirectoryFilters;
|
const filter = roomDirectoryFilters;
|
||||||
const roomIdFilter = filter && filter.room_id ? true : false;
|
const roomIdFilter = filter && filter.room_id ? true : false;
|
||||||
const topicFilter = filter && filter.topic ? true : false;
|
const topicFilter = filter && filter.topic ? true : false;
|
||||||
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
|
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
|
||||||
|
|||||||
+148
-95
@@ -1,8 +1,9 @@
|
|||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { Title, Button } from "react-admin";
|
import { Title, Button } from "react-admin";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import { PDFExport } from "@progress/kendo-react-pdf";
|
import { PDFExport } from "@progress/kendo-react-pdf";
|
||||||
import QRCode from "qrcode.react";
|
import QRCode from "qrcode.react";
|
||||||
|
import { string, any } from "prop-types";
|
||||||
|
|
||||||
function xor(a, b) {
|
function xor(a, b) {
|
||||||
var res = "";
|
var res = "";
|
||||||
@@ -14,15 +15,113 @@ function xor(a, b) {
|
|||||||
|
|
||||||
function calculateQrString(serverUrl, username, password) {
|
function calculateQrString(serverUrl, username, password) {
|
||||||
const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7";
|
const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7";
|
||||||
var urlString = "user=" + username + "&password=" + password;
|
const origUrlString = "user=" + username + "&password=" + password;
|
||||||
|
|
||||||
urlString = xor(urlString, magicString); // xor with magic string
|
var urlString = xor(origUrlString, magicString); // xor with magic string
|
||||||
|
if (origUrlString !== xor(urlString, magicString)) {
|
||||||
|
console.error(
|
||||||
|
"xoring this url string with magicString twice gave different results:",
|
||||||
|
origUrlString,
|
||||||
|
urlString,
|
||||||
|
xor(urlString, magicString)
|
||||||
|
);
|
||||||
|
}
|
||||||
urlString = btoa(urlString); // to base64
|
urlString = btoa(urlString); // to base64
|
||||||
|
|
||||||
return serverUrl + "/#" + urlString;
|
return serverUrl + "/#" + urlString;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShowUserPdf = props => {
|
UserPdfPage.propTypes = {
|
||||||
|
classes: any,
|
||||||
|
displayname: string,
|
||||||
|
qrCode: any,
|
||||||
|
serverUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserPdfPage({
|
||||||
|
classes,
|
||||||
|
displayname,
|
||||||
|
qrCode,
|
||||||
|
serverUrl,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={classes.page}>
|
||||||
|
<div className={classes.header}>
|
||||||
|
<div className={classes.name}>{displayname}</div>
|
||||||
|
<img className={classes.logo} alt="Logo" src="images/logo.png" />
|
||||||
|
</div>
|
||||||
|
<div className={classes.body}>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="200px">
|
||||||
|
<div className={classes.code_note}>
|
||||||
|
Ihr persönlicher Anmeldecode:
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={classes.table_cell}>
|
||||||
|
<div className={classes.credentials_note}>
|
||||||
|
Ihre persönlichen Zugangsdaten:
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className={classes.qr}>{qrCode}</div>
|
||||||
|
</td>
|
||||||
|
<td className={classes.table_cell}>
|
||||||
|
<div className={classes.credentials_text}>
|
||||||
|
<br />
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Heimserver:</td>
|
||||||
|
<td>
|
||||||
|
<span className={classes.credentials}>
|
||||||
|
{serverUrl}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Benutzername:</td>
|
||||||
|
<td>
|
||||||
|
<span className={classes.credentials}>
|
||||||
|
{username}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Passwort:</td>
|
||||||
|
<td>
|
||||||
|
<span className={classes.credentials}>
|
||||||
|
{password}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className={classes.note}>
|
||||||
|
Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase
|
||||||
|
notieren:
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
page: {
|
page: {
|
||||||
height: 800,
|
height: 800,
|
||||||
@@ -87,36 +186,33 @@ const ShowUserPdf = props => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const ShowUserPdf = props => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const userPdf = useRef(null);
|
||||||
var resume;
|
|
||||||
|
|
||||||
const exportPDF = () => {
|
const exportPDF = () => {
|
||||||
resume.save();
|
userPdf.current.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
var qrCode = "";
|
let userRecords;
|
||||||
var displayname = "";
|
|
||||||
var id = "";
|
if (props.records) {
|
||||||
var password = "";
|
userRecords = props.records;
|
||||||
var username = "";
|
}
|
||||||
var serverUrl = "";
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
props.location &&
|
||||||
props.location.state &&
|
props.location.state &&
|
||||||
props.location.state.id &&
|
props.location.state.id &&
|
||||||
props.location.state.password
|
props.location.state.password
|
||||||
) {
|
) {
|
||||||
id = props.location.state.id;
|
userRecords = [
|
||||||
password = props.location.state.password;
|
{
|
||||||
|
id: props.location.state.id,
|
||||||
username = id.substring(1, id.indexOf(":"));
|
password: props.location.state.password,
|
||||||
serverUrl = "https://" + id.substring(id.indexOf(":") + 1);
|
displayname: props.location.state.displayname,
|
||||||
|
},
|
||||||
const qrString = calculateQrString(serverUrl, username, password);
|
];
|
||||||
|
|
||||||
qrCode = <QRCode value={qrString} size={128} />;
|
|
||||||
displayname = props.location.state.displayname;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -126,82 +222,39 @@ const ShowUserPdf = props => {
|
|||||||
|
|
||||||
<PDFExport
|
<PDFExport
|
||||||
paperSize={"A4"}
|
paperSize={"A4"}
|
||||||
fileName="User.pdf"
|
fileName="Users.pdf"
|
||||||
title=""
|
title=""
|
||||||
subject=""
|
subject=""
|
||||||
keywords=""
|
keywords=""
|
||||||
ref={r => (resume = r)}
|
ref={userPdf}
|
||||||
|
//ref={r => (resume = r)}
|
||||||
>
|
>
|
||||||
<div className={classes.page}>
|
{userRecords.map(record => {
|
||||||
<div className={classes.header}>
|
if (record.id && record.password) {
|
||||||
<div className={classes.name}>{displayname}</div>
|
const username = record.id.substring(1, record.id.indexOf(":"));
|
||||||
<img className={classes.logo} alt="Logo" src="images/logo.png" />
|
const serverUrl =
|
||||||
</div>
|
"https://" + record.id.substring(record.id.indexOf(":") + 1);
|
||||||
<div className={classes.body}>
|
const qrString = calculateQrString(
|
||||||
<table>
|
serverUrl,
|
||||||
<tbody>
|
username,
|
||||||
<tr>
|
record.password
|
||||||
<td width="200px">
|
);
|
||||||
<div className={classes.code_note}>
|
const qrCode = <QRCode value={qrString} size={128} />;
|
||||||
Ihr persönlicher Anmeldecode:
|
return (
|
||||||
</div>
|
<UserPdfPage
|
||||||
</td>
|
classes={classes}
|
||||||
<td className={classes.table_cell}>
|
displayname={record.displayname}
|
||||||
<div className={classes.credentials_note}>
|
qrCode={qrCode}
|
||||||
Ihre persönlichen Zugangsdaten:
|
serverUrl={serverUrl}
|
||||||
</div>
|
username={username}
|
||||||
</td>
|
password={record.password}
|
||||||
</tr>
|
/>
|
||||||
<tr>
|
);
|
||||||
<td>
|
} else {
|
||||||
<div className={classes.qr}>{qrCode}</div>
|
/* Skip empty PDF pages */
|
||||||
</td>
|
return null;
|
||||||
<td className={classes.table_cell}>
|
}
|
||||||
<div className={classes.credentials_text}>
|
})}
|
||||||
<br />
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Heimserver:</td>
|
|
||||||
<td>
|
|
||||||
<span className={classes.credentials}>
|
|
||||||
{serverUrl}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Benutzername:</td>
|
|
||||||
<td>
|
|
||||||
<span className={classes.credentials}>
|
|
||||||
{username}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Passwort:</td>
|
|
||||||
<td>
|
|
||||||
<span className={classes.credentials}>
|
|
||||||
{password}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div className={classes.note}>
|
|
||||||
Hier können Sie Ihre selbst gewählte
|
|
||||||
Schlüsselsicherungs-Passphrase notieren:
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PDFExport>
|
</PDFExport>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+65
-3
@@ -16,6 +16,7 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
FormTab,
|
FormTab,
|
||||||
List,
|
List,
|
||||||
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
ReferenceArrayInput,
|
ReferenceArrayInput,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
@@ -33,10 +34,13 @@ import {
|
|||||||
Toolbar,
|
Toolbar,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
useDataProvider,
|
useDataProvider,
|
||||||
|
useRecordContext,
|
||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -47,6 +51,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
|
import FastForwardIcon from "@material-ui/icons/FastForward";
|
||||||
import HttpsIcon from "@material-ui/icons/Https";
|
import HttpsIcon from "@material-ui/icons/Https";
|
||||||
import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
|
import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
|
||||||
import PageviewIcon from "@material-ui/icons/Pageview";
|
import PageviewIcon from "@material-ui/icons/Pageview";
|
||||||
@@ -62,6 +67,13 @@ import {
|
|||||||
RoomDirectorySaveButton,
|
RoomDirectorySaveButton,
|
||||||
} from "./RoomDirectory";
|
} from "./RoomDirectory";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
helper_forward_extremities: {
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
margin: "0.5em",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const RoomPagination = props => (
|
const RoomPagination = props => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
);
|
);
|
||||||
@@ -481,6 +493,7 @@ 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 />}>
|
||||||
@@ -592,6 +605,7 @@ export const RoomShow = props => {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
label={translate("resources.room_state.name", { smart_count: 2 })}
|
label={translate("resources.room_state.name", { smart_count: 2 })}
|
||||||
icon={<EventIcon />}
|
icon={<EventIcon />}
|
||||||
@@ -628,6 +642,40 @@ export const RoomShow = props => {
|
|||||||
</Datagrid>
|
</Datagrid>
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</Tab>
|
</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>
|
</TabbedShowLayout>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
@@ -679,8 +727,22 @@ const RoomFilter = ({ ...props }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FilterableRoomList = ({ ...props }) => {
|
const RoomNameField = props => {
|
||||||
const filter = props.roomFilters;
|
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 =
|
const localMembersFilter =
|
||||||
filter && filter.joined_local_members ? true : false;
|
filter && filter.joined_local_members ? true : false;
|
||||||
const stateEventsFilter = filter && filter.state_events ? true : false;
|
const stateEventsFilter = filter && filter.state_events ? true : false;
|
||||||
@@ -701,7 +763,7 @@ const FilterableRoomList = ({ ...props }) => {
|
|||||||
sortBy="encryption"
|
sortBy="encryption"
|
||||||
label={<HttpsIcon />}
|
label={<HttpsIcon />}
|
||||||
/>
|
/>
|
||||||
<TextField source="name" />
|
<RoomNameField source="name" />
|
||||||
<TextField source="joined_members" />
|
<TextField source="joined_members" />
|
||||||
{localMembersFilter && <TextField source="joined_local_members" />}
|
{localMembersFilter && <TextField source="joined_local_members" />}
|
||||||
{stateEventsFilter && <TextField source="state_events" />}
|
{stateEventsFilter && <TextField source="state_events" />}
|
||||||
|
|||||||
+28
-19
@@ -12,10 +12,12 @@ import {
|
|||||||
ArrayInput,
|
ArrayInput,
|
||||||
ArrayField,
|
ArrayField,
|
||||||
Button,
|
Button,
|
||||||
|
CreateButton,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
Create,
|
Create,
|
||||||
Edit,
|
Edit,
|
||||||
|
ExportButton,
|
||||||
List,
|
List,
|
||||||
Filter,
|
Filter,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
@@ -37,11 +39,8 @@ import {
|
|||||||
DeleteButton,
|
DeleteButton,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
regex,
|
regex,
|
||||||
useRedirect,
|
|
||||||
useTranslate,
|
useTranslate,
|
||||||
Pagination,
|
Pagination,
|
||||||
CreateButton,
|
|
||||||
ExportButton,
|
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
sanitizeListRestProps,
|
sanitizeListRestProps,
|
||||||
NumberField,
|
NumberField,
|
||||||
@@ -50,6 +49,13 @@ import SaveQrButton from "./SaveQrButton";
|
|||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
||||||
import { DeviceRemoveButton } from "./devices";
|
import { DeviceRemoveButton } from "./devices";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const redirect = () => {
|
||||||
|
return {
|
||||||
|
pathname: "/importcsv",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
small: {
|
small: {
|
||||||
@@ -81,7 +87,6 @@ const UserListActions = ({
|
|||||||
total,
|
total,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const redirectTo = useRedirect();
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
{filters &&
|
{filters &&
|
||||||
@@ -101,6 +106,10 @@ const UserListActions = ({
|
|||||||
exporter={exporter}
|
exporter={exporter}
|
||||||
maxResults={maxResults}
|
maxResults={maxResults}
|
||||||
/>
|
/>
|
||||||
|
{/* Add your custom actions */}
|
||||||
|
<Button component={Link} to={redirect} label="CSV Import">
|
||||||
|
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
|
||||||
|
</Button>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -149,6 +158,7 @@ export const UserList = props => {
|
|||||||
{...props}
|
{...props}
|
||||||
filters={<UserFilter />}
|
filters={<UserFilter />}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||||
|
sort={{ field: "name", order: "ASC" }}
|
||||||
actions={<UserListActions maxResults={10000} />}
|
actions={<UserListActions maxResults={10000} />}
|
||||||
bulkActionButtons={<UserBulkActionButtons />}
|
bulkActionButtons={<UserBulkActionButtons />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
@@ -156,13 +166,13 @@ export const UserList = props => {
|
|||||||
<Datagrid rowClick="edit">
|
<Datagrid rowClick="edit">
|
||||||
<AvatarField
|
<AvatarField
|
||||||
source="avatar_src"
|
source="avatar_src"
|
||||||
sortable={false}
|
|
||||||
className={classes.small}
|
className={classes.small}
|
||||||
|
sortBy="avatar_url"
|
||||||
/>
|
/>
|
||||||
<TextField source="id" sortable={false} />
|
<TextField source="id" sortBy="name" />
|
||||||
<TextField source="displayname" />
|
<TextField source="displayname" />
|
||||||
<BooleanField source="is_guest" sortable={false} />
|
<BooleanField source="is_guest" />
|
||||||
<BooleanField source="admin" sortable={false} />
|
<BooleanField source="admin" />
|
||||||
<SelectField
|
<SelectField
|
||||||
source="user_type"
|
source="user_type"
|
||||||
choices={[
|
choices={[
|
||||||
@@ -171,14 +181,14 @@ export const UserList = props => {
|
|||||||
{ id: "limited", name: "resources.users.type.limited" },
|
{ id: "limited", name: "resources.users.type.limited" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<BooleanField source="deactivated" sortable={false} />
|
<BooleanField source="deactivated" />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// redirect to the related Author show page
|
// redirect to the related Author show page
|
||||||
const redirect = (basePath, id, data) => {
|
const redirectToPdf = (basePath, id, data) => {
|
||||||
return {
|
return {
|
||||||
pathname: "/showpdf",
|
pathname: "/showpdf",
|
||||||
state: {
|
state: {
|
||||||
@@ -193,7 +203,7 @@ const UserCreateToolbar = props => (
|
|||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<SaveQrButton
|
<SaveQrButton
|
||||||
label="synapseadmin.action.save_and_show"
|
label="synapseadmin.action.save_and_show"
|
||||||
redirect={redirect}
|
redirect={redirectToPdf}
|
||||||
submitOnEnter={true}
|
submitOnEnter={true}
|
||||||
/>
|
/>
|
||||||
<SaveButton
|
<SaveButton
|
||||||
@@ -471,6 +481,7 @@ export const UserEdit = props => {
|
|||||||
addLabel={false}
|
addLabel={false}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
perPage={50}
|
perPage={50}
|
||||||
|
sort={{ field: "created_ts", order: "DESC" }}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<DateField
|
<DateField
|
||||||
@@ -484,7 +495,6 @@ export const UserEdit = props => {
|
|||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
}}
|
}}
|
||||||
sortable={false}
|
|
||||||
/>
|
/>
|
||||||
<DateField
|
<DateField
|
||||||
source="last_access_ts"
|
source="last_access_ts"
|
||||||
@@ -497,14 +507,13 @@ export const UserEdit = props => {
|
|||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
}}
|
}}
|
||||||
sortable={false}
|
|
||||||
/>
|
/>
|
||||||
<TextField source="media_id" sortable={false} />
|
<TextField source="media_id" />
|
||||||
<NumberField source="media_length" sortable={false} />
|
<NumberField source="media_length" />
|
||||||
<TextField source="media_type" sortable={false} />
|
<TextField source="media_type" />
|
||||||
<TextField source="upload_name" sortable={false} />
|
<TextField source="upload_name" />
|
||||||
<TextField source="quarantined_by" sortable={false} />
|
<TextField source="quarantined_by" />
|
||||||
<BooleanField source="safe_from_quarantine" sortable={false} />
|
<BooleanField source="safe_from_quarantine" />
|
||||||
<DeleteButton mutationMode="pessimistic" redirect={false} />
|
<DeleteButton mutationMode="pessimistic" redirect={false} />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
|
|||||||
+23
-5
@@ -1,6 +1,6 @@
|
|||||||
import germanMessages from "ra-language-german";
|
import germanMessages from "ra-language-german";
|
||||||
|
|
||||||
export default {
|
const de = {
|
||||||
...germanMessages,
|
...germanMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -104,6 +104,8 @@ export default {
|
|||||||
with_error:
|
with_error:
|
||||||
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
|
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
|
||||||
simulated_only: "Import-Vorgang war nur simuliert",
|
simulated_only: "Import-Vorgang war nur simuliert",
|
||||||
|
for_print:
|
||||||
|
"%{smart_count} Eintrag zum Drucken verfügbar |||| %{smart_count} Einträge zum Drucken verfügbar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -184,6 +186,10 @@ export default {
|
|||||||
topic: "Thema",
|
topic: "Thema",
|
||||||
avatar: "Avatar",
|
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: {
|
enums: {
|
||||||
join_rules: {
|
join_rules: {
|
||||||
public: "Öffentlich",
|
public: "Öffentlich",
|
||||||
@@ -286,8 +292,7 @@ export default {
|
|||||||
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
send:
|
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
||||||
"Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pushers: {
|
pushers: {
|
||||||
@@ -316,8 +321,7 @@ export default {
|
|||||||
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
send:
|
send: 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
|
||||||
'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user_media_statistics: {
|
user_media_statistics: {
|
||||||
@@ -327,6 +331,15 @@ export default {
|
|||||||
media_length: "Größe der Dateien",
|
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: {
|
room_state: {
|
||||||
name: "Zustandsereignisse",
|
name: "Zustandsereignisse",
|
||||||
fields: {
|
fields: {
|
||||||
@@ -381,5 +394,10 @@ export default {
|
|||||||
empty: "Keine Einträge vorhanden",
|
empty: "Keine Einträge vorhanden",
|
||||||
invite: "",
|
invite: "",
|
||||||
},
|
},
|
||||||
|
navigation: {
|
||||||
|
...germanMessages.ra.navigation,
|
||||||
|
skip_nav: "Zum Inhalt springen",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export default de;
|
||||||
|
|||||||
+20
-6
@@ -1,6 +1,6 @@
|
|||||||
import englishMessages from "ra-language-english";
|
import englishMessages from "ra-language-english";
|
||||||
|
|
||||||
export default {
|
const en = {
|
||||||
...englishMessages,
|
...englishMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -104,6 +104,8 @@ export default {
|
|||||||
with_error:
|
with_error:
|
||||||
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
||||||
simulated_only: "Run was only simulated",
|
simulated_only: "Run was only simulated",
|
||||||
|
for_print:
|
||||||
|
"%{smart_count} entry available for printing |||| %{smart_count} entries available for printing",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -182,6 +184,10 @@ export default {
|
|||||||
topic: "Topic",
|
topic: "Topic",
|
||||||
avatar: "Avatar",
|
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: {
|
enums: {
|
||||||
join_rules: {
|
join_rules: {
|
||||||
public: "Public",
|
public: "Public",
|
||||||
@@ -260,7 +266,7 @@ export default {
|
|||||||
name: "Media",
|
name: "Media",
|
||||||
fields: {
|
fields: {
|
||||||
media_id: "Media ID",
|
media_id: "Media ID",
|
||||||
media_length: "Lenght",
|
media_length: "File Size (in Bytes)",
|
||||||
media_type: "Type",
|
media_type: "Type",
|
||||||
upload_name: "File name",
|
upload_name: "File name",
|
||||||
quarantined_by: "Quarantined by",
|
quarantined_by: "Quarantined by",
|
||||||
@@ -282,8 +288,7 @@ export default {
|
|||||||
send_failure: "An error has occurred.",
|
send_failure: "An error has occurred.",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
send:
|
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
||||||
"This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pushers: {
|
pushers: {
|
||||||
@@ -312,8 +317,7 @@ export default {
|
|||||||
send_failure: "An error has occurred.",
|
send_failure: "An error has occurred.",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
send:
|
send: 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
|
||||||
'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user_media_statistics: {
|
user_media_statistics: {
|
||||||
@@ -323,6 +327,15 @@ export default {
|
|||||||
media_length: "Media length",
|
media_length: "Media length",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
forward_extremities: {
|
||||||
|
name: "Forward Extremities",
|
||||||
|
fields: {
|
||||||
|
id: "Event ID",
|
||||||
|
received_ts: "Timestamp",
|
||||||
|
depth: "Depth",
|
||||||
|
state_group: "State group",
|
||||||
|
},
|
||||||
|
},
|
||||||
room_state: {
|
room_state: {
|
||||||
name: "State events",
|
name: "State events",
|
||||||
fields: {
|
fields: {
|
||||||
@@ -351,3 +364,4 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export default en;
|
||||||
|
|||||||
+4
-5
@@ -1,6 +1,6 @@
|
|||||||
import chineseMessages from "ra-language-chinese";
|
import chineseMessages from "ra-language-chinese";
|
||||||
|
|
||||||
export default {
|
const zh = {
|
||||||
...chineseMessages,
|
...chineseMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -245,8 +245,7 @@ export default {
|
|||||||
send_failure: "出现了一个错误。",
|
send_failure: "出现了一个错误。",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
send:
|
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
||||||
"这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pushers: {
|
pushers: {
|
||||||
@@ -275,8 +274,7 @@ export default {
|
|||||||
send_failure: "出现了一个错误。",
|
send_failure: "出现了一个错误。",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
send:
|
send: '向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
|
||||||
'向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user_media_statistics: {
|
user_media_statistics: {
|
||||||
@@ -288,3 +286,4 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export default zh;
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { configure } from "enzyme";
|
|
||||||
import Adapter from "enzyme-adapter-react-16";
|
|
||||||
import fetchMock from "jest-fetch-mock";
|
import fetchMock from "jest-fetch-mock";
|
||||||
|
|
||||||
configure({ adapter: new Adapter() });
|
|
||||||
fetchMock.enableMocks();
|
fetchMock.enableMocks();
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { fetchUtils } from "react-admin";
|
|||||||
const authProvider = {
|
const authProvider = {
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
login: ({ base_url, username, password }) => {
|
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 ");
|
console.log("login ");
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -10,6 +13,7 @@ const authProvider = {
|
|||||||
type: "m.login.password",
|
type: "m.login.password",
|
||||||
user: username,
|
user: username,
|
||||||
password: password,
|
password: password,
|
||||||
|
device_id: localStorage.getItem("device_id"),
|
||||||
initial_device_display_name: "Synapse Admin",
|
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
|
// 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
|
// server, since the admin might want to access the admin API via some
|
||||||
// private address
|
// private address
|
||||||
|
base_url = base_url.replace(/\/+$/g, "");
|
||||||
localStorage.setItem("base_url", base_url);
|
localStorage.setItem("base_url", base_url);
|
||||||
|
|
||||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||||
@@ -48,7 +53,6 @@ const authProvider = {
|
|||||||
if (typeof access_token === "string") {
|
if (typeof access_token === "string") {
|
||||||
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
|
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
localStorage.removeItem("device_id");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ const mxcUrlToHttp = mxcUrl => {
|
|||||||
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const powerLevelToRole = powerLevel =>
|
|
||||||
powerLevel < 100 ? (powerLevel < 50 ? "user" : "mod") : "admin";
|
|
||||||
|
|
||||||
const POWER_LEVELS = {
|
const POWER_LEVELS = {
|
||||||
admin: 100,
|
admin: 100,
|
||||||
mod: 50,
|
mod: 50,
|
||||||
@@ -110,9 +107,8 @@ const resourceMap = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/rooms/${params.id}/delete`,
|
endpoint: `/_synapse/admin/v1/rooms/${params.id}`,
|
||||||
body: { block: false },
|
body: { block: false },
|
||||||
method: "POST",
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
reports: {
|
reports: {
|
||||||
@@ -251,6 +247,22 @@ const resourceMap = {
|
|||||||
return json.total;
|
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: {
|
room_directory: {
|
||||||
path: "/_matrix/client/r0/publicRooms",
|
path: "/_matrix/client/r0/publicRooms",
|
||||||
map: rd => ({
|
map: rd => ({
|
||||||
@@ -357,10 +369,13 @@ const dataProvider = {
|
|||||||
getManyReference: (resource, params) => {
|
getManyReference: (resource, params) => {
|
||||||
console.log("getManyReference " + resource);
|
console.log("getManyReference " + resource);
|
||||||
const { page, perPage } = params.pagination;
|
const { page, perPage } = params.pagination;
|
||||||
|
const { field, order } = params.sort;
|
||||||
const from = (page - 1) * perPage;
|
const from = (page - 1) * perPage;
|
||||||
const query = {
|
const query = {
|
||||||
from: from,
|
from: from,
|
||||||
limit: perPage,
|
limit: perPage,
|
||||||
|
order_by: field,
|
||||||
|
dir: getSearchOrder(order),
|
||||||
};
|
};
|
||||||
|
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
|
|||||||
Reference in New Issue
Block a user