mirror of
https://github.com/Awesome-Technologies/synapse-admin.git
synced 2025-08-17 16:08:18 +02:00
Compare commits
25 Commits
TIM/0.10.3
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
e15411a04c | ||
|
3902dcd3d1 | ||
|
ef8ae9b38f | ||
|
7a286ad506 | ||
|
fa3f2437a3 | ||
|
8dc5238fcb | ||
|
238350b940 | ||
|
99bf7b1889 | ||
|
d72c91644d | ||
|
e8e28b5df1 | ||
|
d5c10b6e02 | ||
|
3085b9ffa0 | ||
|
b2a3fb0f87 | ||
|
1e8b4cc885 | ||
|
4d1a9cc147 | ||
|
1b8b702270 | ||
|
61c32fb473 | ||
|
ad876bb790 | ||
|
2524848dae | ||
|
669c1f3079 | ||
|
590f673167 | ||
|
307793f000 | ||
|
96f549fe42 | ||
|
3de4332477 | ||
|
9fc005032c |
3
.github/workflows/build-test.yml
vendored
3
.github/workflows/build-test.yml
vendored
@ -5,6 +5,9 @@ on:
|
|||||||
branches: ["master"]
|
branches: ["master"]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
4
.github/workflows/edge_ghpage.yml
vendored
4
.github/workflows/edge_ghpage.yml
vendored
@ -5,6 +5,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- master
|
- master
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -23,7 +25,7 @@ jobs:
|
|||||||
yarn build --base=/synapse-admin
|
yarn build --base=/synapse-admin
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.6.3
|
uses: JamesIves/github-pages-deploy-action@v4.7.3
|
||||||
with:
|
with:
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
folder: dist
|
folder: dist
|
||||||
|
2
.github/workflows/github-release.yml
vendored
2
.github/workflows/github-release.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
version=`git describe --dirty --tags || echo unknown`
|
version=`git describe --dirty --tags || echo unknown`
|
||||||
cp -r dist synapse-admin-$version
|
cp -r dist synapse-admin-$version
|
||||||
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
||||||
- uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191
|
- uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||||
with:
|
with:
|
||||||
files: dist/*.tar.gz
|
files: dist/*.tar.gz
|
||||||
env:
|
env:
|
||||||
|
893
.yarn/releases/yarn-4.1.1.cjs
vendored
893
.yarn/releases/yarn-4.1.1.cjs
vendored
File diff suppressed because one or more lines are too long
925
.yarn/releases/yarn-4.4.1.cjs
vendored
Executable file
925
.yarn/releases/yarn-4.4.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Builder
|
# Builder
|
||||||
FROM node:lts as builder
|
FROM node:lts AS builder
|
||||||
LABEL org.opencontainers.image.url=https://registry.awesome-technologies.de/tim/synapse-admin org.opencontainers.image.source=https://gerrit.awesome-technologies.de/q/project:matrix/synapse-admin
|
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
||||||
# Base path for synapse admin
|
# Base path for synapse admin
|
||||||
ARG BASE_PATH=./
|
ARG BASE_PATH=./
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import type { JestConfigWithTsJest } from "ts-jest";
|
|||||||
|
|
||||||
const config: JestConfigWithTsJest = {
|
const config: JestConfigWithTsJest = {
|
||||||
preset: "ts-jest",
|
preset: "ts-jest",
|
||||||
testEnvironment: "jsdom",
|
testEnvironment: "jest-fixed-jsdom",
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coveragePathIgnorePatterns: ["node_modules", "dist"],
|
coveragePathIgnorePatterns: ["node_modules", "dist"],
|
||||||
coverageDirectory: "<rootDir>/coverage/",
|
coverageDirectory: "<rootDir>/coverage/",
|
||||||
|
43
package.json
43
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "0.10.3-tim-1",
|
"version": "0.10.3",
|
||||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||||
@ -10,10 +10,12 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.4.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.7.0",
|
"@eslint/js": "^9.7.0",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@mui/system": "^7.1.0",
|
||||||
|
"@mui/utils": "^7.1.0",
|
||||||
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
@ -30,46 +32,47 @@
|
|||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"eslint-plugin-yaml": "^1.0.3",
|
"eslint-plugin-yaml": "^1.0.3",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
|
"jest-fixed-jsdom": "^0.0.9",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.3.1",
|
||||||
"ts-jest": "^29.2.3",
|
"ts-jest": "^29.3.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^7.16.1",
|
"typescript-eslint": "^8.32.1",
|
||||||
"vite": "^5.3.4",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-version-mark": "^0.1.0"
|
"vite-plugin-version-mark": "^0.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@haleos/ra-language-german": "^1.0.0",
|
"@haleos/ra-language-german": "^1.0.0",
|
||||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||||
"@mui/icons-material": "^5.16.4",
|
"@mui/icons-material": "^7.1.0",
|
||||||
"@mui/material": "^5.16.4",
|
"@mui/material": "^7.1.0",
|
||||||
|
"@tanstack/react-query": "^5.59.12",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"ra-core": "^4.16.20",
|
"ra-core": "^5.8.3",
|
||||||
"ra-i18n-polyglot": "^4.16.20",
|
"ra-i18n-polyglot": "^5.8.3",
|
||||||
"ra-language-english": "^4.16.20",
|
"ra-language-english": "^5.8.3",
|
||||||
"ra-language-farsi": "^4.2.0",
|
"ra-language-farsi": "^5.0.0",
|
||||||
"ra-language-french": "^4.16.20",
|
"ra-language-french": "^5.8.3",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"ra-language-russian": "^4.14.2",
|
"ra-language-russian": "^4.14.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-admin": "^4.16.20",
|
"react-admin": "^5.8.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-query": "^3.39.3",
|
"react-router": "^7.6.1",
|
||||||
"react-router": "^6.25.1",
|
"react-router-dom": "^7.6.1"
|
||||||
"react-router-dom": "^6.25.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import fetchMock from "jest-fetch-mock";
|
||||||
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
|
62
src/App.tsx
62
src/App.tsx
@ -4,6 +4,7 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
|
|||||||
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ImportFeature } from "./components/ImportFeature";
|
||||||
import germanMessages from "./i18n/de";
|
import germanMessages from "./i18n/de";
|
||||||
import englishMessages from "./i18n/en";
|
import englishMessages from "./i18n/en";
|
||||||
import frenchMessages from "./i18n/fr";
|
import frenchMessages from "./i18n/fr";
|
||||||
@ -20,6 +21,7 @@ import userMediaStats from "./resources/user_media_statistics";
|
|||||||
import users from "./resources/users";
|
import users from "./resources/users";
|
||||||
import authProvider from "./synapse/authProvider";
|
import authProvider from "./synapse/authProvider";
|
||||||
import dataProvider from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@ -44,34 +46,40 @@ const i18nProvider = polyglotI18nProvider(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<Admin
|
<QueryClientProvider client={queryClient}>
|
||||||
disableTelemetry
|
<Admin
|
||||||
requireAuth
|
disableTelemetry
|
||||||
loginPage={LoginPage}
|
requireAuth
|
||||||
authProvider={authProvider}
|
loginPage={LoginPage}
|
||||||
dataProvider={dataProvider}
|
authProvider={authProvider}
|
||||||
i18nProvider={i18nProvider}
|
dataProvider={dataProvider}
|
||||||
darkTheme={{ palette: { mode: "dark" } }}
|
i18nProvider={i18nProvider}
|
||||||
>
|
>
|
||||||
<Resource {...users} />
|
<CustomRoutes>
|
||||||
<Resource {...rooms} />
|
<Route path="/import_users" element={<ImportFeature />} />
|
||||||
<Resource {...userMediaStats} />
|
</CustomRoutes>
|
||||||
<Resource {...reports} />
|
<Resource {...users} />
|
||||||
<Resource {...roomDirectory} />
|
<Resource {...rooms} />
|
||||||
<Resource {...destinations} />
|
<Resource {...userMediaStats} />
|
||||||
<Resource {...registrationToken} />
|
<Resource {...reports} />
|
||||||
<Resource name="connections" />
|
<Resource {...roomDirectory} />
|
||||||
<Resource name="devices" />
|
<Resource {...destinations} />
|
||||||
<Resource name="room_members" />
|
<Resource {...registrationToken} />
|
||||||
<Resource name="users_media" />
|
<Resource name="connections" />
|
||||||
<Resource name="joined_rooms" />
|
<Resource name="devices" />
|
||||||
<Resource name="pushers" />
|
<Resource name="room_members" />
|
||||||
<Resource name="servernotices" />
|
<Resource name="users_media" />
|
||||||
<Resource name="forward_extremities" />
|
<Resource name="joined_rooms" />
|
||||||
<Resource name="room_state" />
|
<Resource name="pushers" />
|
||||||
<Resource name="destination_rooms" />
|
<Resource name="servernotices" />
|
||||||
</Admin>
|
<Resource name="forward_extremities" />
|
||||||
|
<Resource name="room_state" />
|
||||||
|
<Resource name="destination_rooms" />
|
||||||
|
</Admin>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
559
src/components/ImportFeature.tsx
Normal file
559
src/components/ImportFeature.tsx
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
|
||||||
|
import { ChangeEvent, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardActions,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Checkbox,
|
||||||
|
Container,
|
||||||
|
FormControlLabel,
|
||||||
|
NativeSelect,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { DataProvider, useTranslate } from "ra-core";
|
||||||
|
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
|
||||||
|
|
||||||
|
import { generateRandomMxId, generateRandomPassword } from "../synapse/synapse";
|
||||||
|
|
||||||
|
const LOGGING = true;
|
||||||
|
|
||||||
|
const expectedFields = ["id", "displayname"].sort();
|
||||||
|
|
||||||
|
function TranslatableOption({ value, text }) {
|
||||||
|
const translate = useTranslate();
|
||||||
|
return <option value={value}>{translate(text)}</option>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Progress = {
|
||||||
|
done: number;
|
||||||
|
limit: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
interface ImportLine {
|
||||||
|
id: string;
|
||||||
|
displayname: string;
|
||||||
|
user_type?: string;
|
||||||
|
name?: string;
|
||||||
|
deactivated?: boolean;
|
||||||
|
guest?: boolean;
|
||||||
|
admin?: boolean;
|
||||||
|
is_admin?: boolean;
|
||||||
|
password?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChangeStats {
|
||||||
|
total: number;
|
||||||
|
id: number;
|
||||||
|
is_guest: number;
|
||||||
|
admin: number;
|
||||||
|
password: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportResult {
|
||||||
|
skippedRecords: RaRecord[];
|
||||||
|
erroredRecords: RaRecord[];
|
||||||
|
succeededRecords: RaRecord[];
|
||||||
|
totalRecordCount: number;
|
||||||
|
changeStats: ChangeStats;
|
||||||
|
wasDryRun: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilePicker = () => {
|
||||||
|
const [values, setValues] = useState<ImportLine[]>([]);
|
||||||
|
const [error, setError] = useState<string | string[] | null>(null);
|
||||||
|
const [stats, setStats] = useState<ChangeStats | null>(null);
|
||||||
|
const [dryRun, setDryRun] = useState(true);
|
||||||
|
|
||||||
|
const [progress, setProgress] = useState<Progress>(null);
|
||||||
|
|
||||||
|
const [importResults, setImportResults] = useState<ImportResult | null>(null);
|
||||||
|
const [skippedRecords, setSkippedRecords] = useState<string>("");
|
||||||
|
|
||||||
|
const [conflictMode, setConflictMode] = useState("stop");
|
||||||
|
const [passwordMode, setPasswordMode] = useState(true);
|
||||||
|
const [useridMode, setUseridMode] = useState("ignore");
|
||||||
|
|
||||||
|
const translate = useTranslate();
|
||||||
|
const notify = useNotify();
|
||||||
|
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
|
const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (progress !== null) return;
|
||||||
|
|
||||||
|
setValues([]);
|
||||||
|
setError(null);
|
||||||
|
setStats(null);
|
||||||
|
setImportResults(null);
|
||||||
|
const file = e.target.files ? e.target.files[0] : null;
|
||||||
|
if (!file) return;
|
||||||
|
/* Let's refuse some unreasonably big files instead of freezing
|
||||||
|
* up the browser */
|
||||||
|
if (file.size > 100000000) {
|
||||||
|
const message = translate("import_users.errors.unreasonably_big", {
|
||||||
|
size: (file.size / (1024 * 1024)).toFixed(2),
|
||||||
|
});
|
||||||
|
notify(message);
|
||||||
|
setError(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
parseCsv<ImportLine>(file, {
|
||||||
|
header: true,
|
||||||
|
skipEmptyLines: true /* especially for a final EOL in the csv file */,
|
||||||
|
complete: result => {
|
||||||
|
if (result.errors) {
|
||||||
|
setError(result.errors.map(e => e.toString()));
|
||||||
|
}
|
||||||
|
/* Papaparse is very lenient, we may be able to salvage
|
||||||
|
* the data in the file. */
|
||||||
|
verifyCsv(result, { setValues, setStats, setError });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
setError("Unknown error");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
||||||
|
/* First, verify the presence of required fields */
|
||||||
|
const missingFields = expectedFields.filter(eF => !meta.fields?.includes(eF));
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX after deciding on how "name" and friends should be handled below,
|
||||||
|
// this place will want changes, too.
|
||||||
|
|
||||||
|
/* Collect some stats to prevent sneaky csv files from adding admin
|
||||||
|
users or something.
|
||||||
|
*/
|
||||||
|
const stats = {
|
||||||
|
user_types: { default: 0 },
|
||||||
|
is_guest: 0,
|
||||||
|
admin: 0,
|
||||||
|
deactivated: 0,
|
||||||
|
password: 0,
|
||||||
|
avatar_url: 0,
|
||||||
|
id: 0,
|
||||||
|
|
||||||
|
total: data.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorMessages = errors.map(e => e.message);
|
||||||
|
data.forEach((line, idx) => {
|
||||||
|
if (line.user_type === undefined || line.user_type === "") {
|
||||||
|
stats.user_types.default++;
|
||||||
|
} else {
|
||||||
|
stats.user_types[line.user_type] += 1;
|
||||||
|
}
|
||||||
|
/* XXX correct the csv export that react-admin offers for the users
|
||||||
|
* resource so it gives sensible field names and doesn't duplicate
|
||||||
|
* id as "name"?
|
||||||
|
*/
|
||||||
|
if (meta.fields?.includes("name")) {
|
||||||
|
delete line.name;
|
||||||
|
}
|
||||||
|
if (meta.fields?.includes("user_type")) {
|
||||||
|
delete line.user_type;
|
||||||
|
}
|
||||||
|
if (meta.fields?.includes("is_admin")) {
|
||||||
|
delete line.is_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
["is_guest", "admin", "deactivated"].forEach(f => {
|
||||||
|
if (line[f] === "true") {
|
||||||
|
stats[f]++;
|
||||||
|
line[f] = true; // we need true booleans instead of strings
|
||||||
|
} else {
|
||||||
|
if (line[f] !== "false" && line[f] !== "") {
|
||||||
|
errorMessages.push(
|
||||||
|
translate("import_users.error.invalid_value", {
|
||||||
|
field: f,
|
||||||
|
row: idx,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
line[f] = false; // default values to false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (line.password !== undefined && line.password !== "") {
|
||||||
|
stats.password++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.avatar_url !== undefined && line.avatar_url !== "") {
|
||||||
|
stats.avatar_url++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.id !== undefined && line.id !== "") {
|
||||||
|
stats.id++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorMessages.length > 0) {
|
||||||
|
setError(errorMessages);
|
||||||
|
}
|
||||||
|
setStats(stats);
|
||||||
|
setValues(data);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runImport = async () => {
|
||||||
|
if (progress !== null) {
|
||||||
|
notify("import_users.errors.already_in_progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await doImport(
|
||||||
|
dataProvider,
|
||||||
|
values,
|
||||||
|
conflictMode,
|
||||||
|
passwordMode,
|
||||||
|
useridMode,
|
||||||
|
dryRun,
|
||||||
|
setProgress,
|
||||||
|
setError
|
||||||
|
);
|
||||||
|
setImportResults(results);
|
||||||
|
// offer CSV download of skipped or errored records
|
||||||
|
// (so that the user doesn't have to filter out successful
|
||||||
|
// records manually when fixing stuff in the CSV)
|
||||||
|
setSkippedRecords(unparseCsv(results.skippedRecords));
|
||||||
|
if (LOGGING) console.log("Skipped records:");
|
||||||
|
if (LOGGING) console.log(skippedRecords);
|
||||||
|
};
|
||||||
|
|
||||||
|
// XXX every single one of the requests will restart the activity indicator
|
||||||
|
// which doesn't look very good.
|
||||||
|
|
||||||
|
const doImport = async (
|
||||||
|
dataProvider: DataProvider,
|
||||||
|
data: ImportLine[],
|
||||||
|
conflictMode: string,
|
||||||
|
passwordMode: boolean,
|
||||||
|
useridMode: string,
|
||||||
|
dryRun: boolean,
|
||||||
|
setProgress: (progress: Progress) => void,
|
||||||
|
setError: (message: string) => void
|
||||||
|
): Promise<ImportResult> => {
|
||||||
|
const skippedRecords: ImportLine[] = [];
|
||||||
|
const erroredRecords: ImportLine[] = [];
|
||||||
|
const succeededRecords: ImportLine[] = [];
|
||||||
|
const changeStats: ChangeStats = {
|
||||||
|
total: 0,
|
||||||
|
id: 0,
|
||||||
|
is_guest: 0,
|
||||||
|
admin: 0,
|
||||||
|
password: 0,
|
||||||
|
};
|
||||||
|
let entriesDone = 0;
|
||||||
|
const entriesCount = data.length;
|
||||||
|
try {
|
||||||
|
setProgress({ done: entriesDone, limit: entriesCount });
|
||||||
|
for (const entry of data) {
|
||||||
|
const userRecord = { ...entry };
|
||||||
|
// No need to do a bunch of cryptographic random number getting if
|
||||||
|
// we are using neither a generated password nor a generated user id.
|
||||||
|
if (useridMode === "ignore" || userRecord.id === undefined) {
|
||||||
|
userRecord.id = generateRandomMxId();
|
||||||
|
}
|
||||||
|
if (passwordMode === false || entry.password === undefined) {
|
||||||
|
userRecord.password = generateRandomPassword();
|
||||||
|
}
|
||||||
|
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
|
||||||
|
|
||||||
|
/* For these modes we will consider the ID that's in the record.
|
||||||
|
* If the mode is "stop", we will not continue adding more records, and
|
||||||
|
* we will offer information on what was already added and what was
|
||||||
|
* skipped.
|
||||||
|
*
|
||||||
|
* If the mode is "skip", we record the record for later, but don't
|
||||||
|
* send it to the server.
|
||||||
|
*
|
||||||
|
* If the mode is "update", we change fields that are reasonable to
|
||||||
|
* update.
|
||||||
|
* - If the "password mode" is "true" (i.e. "use passwords from csv"):
|
||||||
|
* - if the record has a password
|
||||||
|
* - send the password along with the record
|
||||||
|
* - if the record has no password
|
||||||
|
* - generate a new password
|
||||||
|
* - If the "password mode" is "false"
|
||||||
|
* - never generate a new password to update existing users with
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* We just act as if there are no IDs in the CSV, so every user will be
|
||||||
|
* created anew.
|
||||||
|
* We do a simple retry loop so that an accidental hit on an existing ID
|
||||||
|
* doesn't trip us up.
|
||||||
|
*/
|
||||||
|
if (LOGGING) console.log("will check for existence of record " + JSON.stringify(userRecord));
|
||||||
|
let retries = 0;
|
||||||
|
const submitRecord = (recordData: ImportLine) => {
|
||||||
|
return dataProvider.getOne("users", { id: recordData.id }).then(
|
||||||
|
async () => {
|
||||||
|
if (LOGGING) console.log("already existed");
|
||||||
|
|
||||||
|
if (useridMode === "update" || conflictMode === "skip") {
|
||||||
|
skippedRecords.push(recordData);
|
||||||
|
} else if (conflictMode === "stop") {
|
||||||
|
throw new Error(
|
||||||
|
translate("import_users.error.id_exits", {
|
||||||
|
id: recordData.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const newRecordData = Object.assign({}, recordData, {
|
||||||
|
id: generateRandomMxId(),
|
||||||
|
});
|
||||||
|
retries++;
|
||||||
|
if (retries > 512) {
|
||||||
|
console.warn("retry loop got stuck? pathological situation?");
|
||||||
|
skippedRecords.push(recordData);
|
||||||
|
} else {
|
||||||
|
await submitRecord(newRecordData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
if (LOGGING) console.log("OK to create record " + recordData.id + " (" + recordData.displayname + ").");
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
await dataProvider.create("users", { data: recordData });
|
||||||
|
}
|
||||||
|
succeededRecords.push(recordData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
await submitRecord(userRecord);
|
||||||
|
entriesDone++;
|
||||||
|
setProgress({ done: entriesDone, limit: data.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(null);
|
||||||
|
} catch (e) {
|
||||||
|
setError(
|
||||||
|
translate("import_users.error.at_entry", {
|
||||||
|
entry: entriesDone + 1,
|
||||||
|
message: e instanceof Error ? e.message : String(e),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setProgress(null);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
skippedRecords,
|
||||||
|
erroredRecords,
|
||||||
|
succeededRecords,
|
||||||
|
totalRecordCount: entriesCount,
|
||||||
|
changeStats,
|
||||||
|
wasDryRun: dryRun,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadSkippedRecords = () => {
|
||||||
|
const element = document.createElement("a");
|
||||||
|
console.log(skippedRecords);
|
||||||
|
const file = new Blob([skippedRecords], {
|
||||||
|
type: "text/comma-separated-values",
|
||||||
|
});
|
||||||
|
element.href = URL.createObjectURL(file);
|
||||||
|
element.download = "skippedRecords.csv";
|
||||||
|
document.body.appendChild(element); // Required for this to work in FireFox
|
||||||
|
element.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConflictModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
if (progress !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = e.target.value;
|
||||||
|
setConflictMode(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPasswordModeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (progress !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPasswordMode(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUseridModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
if (progress !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = e.target.value;
|
||||||
|
setUseridMode(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDryRunModeChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (progress !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDryRun(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
// render individual small components
|
||||||
|
|
||||||
|
const statsCards = stats &&
|
||||||
|
!importResults && [
|
||||||
|
<Container>
|
||||||
|
<CardHeader title={translate("import_users.cards.importstats.header")} />
|
||||||
|
<CardContent>
|
||||||
|
<div>{translate("import_users.cards.importstats.users_total", stats.total)}</div>
|
||||||
|
<div>{translate("import_users.cards.importstats.guest_count", stats.is_guest)}</div>
|
||||||
|
<div>{translate("import_users.cards.importstats.admin_count", stats.admin)}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Container>,
|
||||||
|
<Container>
|
||||||
|
<CardHeader title={translate("import_users.cards.ids.header")} />
|
||||||
|
<CardContent>
|
||||||
|
<div>
|
||||||
|
{stats.id === stats.total
|
||||||
|
? translate("import_users.cards.ids.all_ids_present")
|
||||||
|
: translate("import_users.cards.ids.count_ids_present", stats.id)}
|
||||||
|
</div>
|
||||||
|
{stats.id > 0 ? (
|
||||||
|
<div>
|
||||||
|
<NativeSelect onChange={onUseridModeChanged} value={useridMode} disabled={progress !== null}>
|
||||||
|
<TranslatableOption value="ignore" text="import_users.cards.ids.mode.ignore" />
|
||||||
|
<TranslatableOption value="update" text="import_users.cards.ids.mode.update" />
|
||||||
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Container>,
|
||||||
|
<Container>
|
||||||
|
<CardHeader title={translate("import_users.cards.passwords.header")} />
|
||||||
|
<CardContent>
|
||||||
|
<div>
|
||||||
|
{stats.password === stats.total
|
||||||
|
? translate("import_users.cards.passwords.all_passwords_present")
|
||||||
|
: translate("import_users.cards.passwords.count_passwords_present", stats.password)}
|
||||||
|
</div>
|
||||||
|
{stats.password > 0 ? (
|
||||||
|
<div>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox checked={passwordMode} disabled={progress !== null} onChange={onPasswordModeChange} />
|
||||||
|
}
|
||||||
|
label={translate("import_users.cards.passwords.use_passwords")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Container>,
|
||||||
|
];
|
||||||
|
|
||||||
|
const conflictCards = stats && !importResults && (
|
||||||
|
<Container>
|
||||||
|
<CardHeader title={translate("import_users.cards.conflicts.header")} />
|
||||||
|
<CardContent>
|
||||||
|
<div>
|
||||||
|
<NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
|
||||||
|
<TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
|
||||||
|
<TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
|
||||||
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorCards = error && (
|
||||||
|
<Container>
|
||||||
|
<CardHeader title={translate("import_users.error.error")} />
|
||||||
|
<CardContent>
|
||||||
|
{(Array.isArray(error) ? error : [error]).map(e => (
|
||||||
|
<div>{e}</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadCard = !importResults && (
|
||||||
|
<Container>
|
||||||
|
<CardHeader title={translate("import_users.cards.upload.header")} />
|
||||||
|
<CardContent>
|
||||||
|
{translate("import_users.cards.upload.explanation")}
|
||||||
|
<a href="./data/example.csv">example.csv</a>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<input type="file" onChange={onFileChange} disabled={progress !== null} />
|
||||||
|
</CardContent>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultsCard = importResults && (
|
||||||
|
<CardContent>
|
||||||
|
<CardHeader title={translate("import_users.cards.results.header")} />
|
||||||
|
<div>
|
||||||
|
{translate("import_users.cards.results.total", importResults.totalRecordCount)}
|
||||||
|
<br />
|
||||||
|
{translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
|
||||||
|
<br />
|
||||||
|
{importResults.skippedRecords.length
|
||||||
|
? [
|
||||||
|
translate("import_users.cards.results.skipped", importResults.skippedRecords.length),
|
||||||
|
<div>
|
||||||
|
<button onClick={downloadSkippedRecords}>
|
||||||
|
{translate("import_users.cards.results.download_skipped")}
|
||||||
|
</button>
|
||||||
|
</div>,
|
||||||
|
<br />,
|
||||||
|
]
|
||||||
|
: ""}
|
||||||
|
{importResults.erroredRecords.length
|
||||||
|
? [translate("import_users.cards.results.skipped", importResults.erroredRecords.length), <br />]
|
||||||
|
: ""}
|
||||||
|
<br />
|
||||||
|
{importResults.wasDryRun && [translate("import_users.cards.results.simulated_only"), <br />]}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
const startImportCard =
|
||||||
|
!values || values.length === 0 || importResults ? undefined : (
|
||||||
|
<CardActions>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Checkbox checked={dryRun} onChange={onDryRunModeChanged} disabled={progress !== null} />}
|
||||||
|
label={translate("import_users.cards.startImport.simulate_only")}
|
||||||
|
/>
|
||||||
|
<Button size="large" onClick={runImport} disabled={progress !== null}>
|
||||||
|
{translate("import_users.cards.startImport.run_import")}
|
||||||
|
</Button>
|
||||||
|
{progress !== null ? (
|
||||||
|
<div>
|
||||||
|
{progress.done} of {progress.limit} done
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</CardActions>
|
||||||
|
);
|
||||||
|
|
||||||
|
const allCards: JSX.Element[] = [];
|
||||||
|
if (uploadCard) allCards.push(uploadCard);
|
||||||
|
if (errorCards) allCards.push(errorCards);
|
||||||
|
if (conflictCards) allCards.push(conflictCards);
|
||||||
|
if (statsCards) allCards.push(...statsCards);
|
||||||
|
if (startImportCard) allCards.push(startImportCard);
|
||||||
|
if (resultsCard) allCards.push(resultsCard);
|
||||||
|
|
||||||
|
const cardContainer = <Card>{allCards}</Card>;
|
||||||
|
|
||||||
|
return [<Title defaultTitle={translate("import_users.title")} />, cardContainer];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImportFeature = FilePicker;
|
@ -20,7 +20,7 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@ -43,7 +43,6 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
source="body"
|
source="body"
|
||||||
label="resources.servernotices.fields.body"
|
label="resources.servernotices.fields.body"
|
||||||
fullWidth
|
|
||||||
multiline
|
multiline
|
||||||
rows="4"
|
rows="4"
|
||||||
resettable
|
resettable
|
||||||
@ -64,6 +63,10 @@ export const ServerNoticeButton = () => {
|
|||||||
const handleDialogOpen = () => setOpen(true);
|
const handleDialogOpen = () => setOpen(true);
|
||||||
const handleDialogClose = () => setOpen(false);
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = (values: Partial<RaRecord>) => {
|
const handleSend = (values: Partial<RaRecord>) => {
|
||||||
create(
|
create(
|
||||||
"servernotices",
|
"servernotices",
|
||||||
@ -100,28 +103,26 @@ export const ServerNoticeBulkButton = () => {
|
|||||||
const unselectAllUsers = useUnselectAll("users");
|
const unselectAllUsers = useUnselectAll("users");
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
const { mutate: sendNotices, isLoading } = useMutation(
|
const { mutate: sendNotices, isPending } = useMutation({
|
||||||
data =>
|
mutationFn: (data) =>
|
||||||
dataProvider.createMany("servernotices", {
|
dataProvider.createMany("servernotices", {
|
||||||
ids: selectedIds,
|
ids: selectedIds,
|
||||||
data: data,
|
data: data,
|
||||||
}),
|
}),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("resources.servernotices.action.send_success");
|
||||||
notify("resources.servernotices.action.send_success");
|
unselectAllUsers();
|
||||||
unselectAllUsers();
|
closeDialog();
|
||||||
closeDialog();
|
},
|
||||||
},
|
onError: () =>
|
||||||
onError: () =>
|
notify("resources.servernotices.action.send_failure", {
|
||||||
notify("resources.servernotices.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
|
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isPending}>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { dateParser } from "./date";
|
import { dateParser } from "./date";
|
||||||
@ -55,14 +55,12 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
||||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
fullWidth
|
|
||||||
source="before_ts"
|
source="before_ts"
|
||||||
label="delete_media.fields.before_ts"
|
label="delete_media.fields.before_ts"
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
parse={dateParser}
|
parse={dateParser}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
fullWidth
|
|
||||||
source="size_gt"
|
source="size_gt"
|
||||||
label="delete_media.fields.size_gt"
|
label="delete_media.fields.size_gt"
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
@ -70,7 +68,6 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
step={1024}
|
step={1024}
|
||||||
/>
|
/>
|
||||||
<BooleanInput
|
<BooleanInput
|
||||||
fullWidth
|
|
||||||
source="keep_profiles"
|
source="keep_profiles"
|
||||||
label="delete_media.fields.keep_profiles"
|
label="delete_media.fields.keep_profiles"
|
||||||
defaultValue={true}
|
defaultValue={true}
|
||||||
@ -86,20 +83,18 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
const { mutate: deleteMedia, isLoading } = useMutation(
|
const { mutate: deleteMedia, isPending } = useMutation({
|
||||||
(values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
mutationFn: (values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("delete_media.action.send_success");
|
||||||
notify("delete_media.action.send_success");
|
closeDialog();
|
||||||
closeDialog();
|
},
|
||||||
},
|
onError: () => {
|
||||||
onError: () => {
|
notify("delete_media.action.send_failure", {
|
||||||
notify("delete_media.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
});
|
||||||
});
|
},
|
||||||
},
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const openDialog = () => setOpen(true);
|
const openDialog = () => setOpen(true);
|
||||||
const closeDialog = () => setOpen(false);
|
const closeDialog = () => setOpen(false);
|
||||||
@ -110,7 +105,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
label="delete_media.action.send"
|
label="delete_media.action.send"
|
||||||
onClick={openDialog}
|
onClick={openDialog}
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
|
@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const de: SynapseTranslationMessages = {
|
const de: SynapseTranslationMessages = {
|
||||||
...formalGermanMessages,
|
...formalGermanMessages,
|
||||||
|
ra: {
|
||||||
|
...formalGermanMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...formalGermanMessages.ra.navigation,
|
||||||
|
no_filtered_results: "Keine Ergebnisse",
|
||||||
|
clear_filters: "Alle Filter entfernen",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "Heimserver URL",
|
base_url: "Heimserver URL",
|
||||||
|
@ -124,6 +124,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
erased: "Erased",
|
erased: "Erased",
|
||||||
guests: "Show guests",
|
guests: "Show guests",
|
||||||
show_deactivated: "Show deactivated users",
|
show_deactivated: "Show deactivated users",
|
||||||
|
show_locked: "Show locked users",
|
||||||
user_id: "Search user",
|
user_id: "Search user",
|
||||||
displayname: "Displayname",
|
displayname: "Displayname",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
|
1
src/i18n/index.d.ts
vendored
1
src/i18n/index.d.ts
vendored
@ -120,6 +120,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
erased?: string; // TODO: fa, fr, it, zh
|
erased?: string; // TODO: fa, fr, it, zh
|
||||||
guests: string;
|
guests: string;
|
||||||
show_deactivated: string;
|
show_deactivated: string;
|
||||||
|
show_locked?: string; // TODO: de, fa, fr, it, zh
|
||||||
user_id: string;
|
user_id: string;
|
||||||
displayname: string;
|
displayname: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const ru: SynapseTranslationMessages = {
|
const ru: SynapseTranslationMessages = {
|
||||||
...russianMessages,
|
...russianMessages,
|
||||||
|
ra: {
|
||||||
|
...russianMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...russianMessages.ra.navigation,
|
||||||
|
no_filtered_results: "Нет результатов",
|
||||||
|
clear_filters: "Все фильтры сбросить",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "Адрес домашнего сервера",
|
base_url: "Адрес домашнего сервера",
|
||||||
|
@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const zh: SynapseTranslationMessages = {
|
const zh: SynapseTranslationMessages = {
|
||||||
...chineseMessages,
|
...chineseMessages,
|
||||||
|
ra: {
|
||||||
|
...chineseMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...chineseMessages.ra.navigation,
|
||||||
|
no_filtered_results: "没有结果",
|
||||||
|
clear_filters: "清除所有过滤器",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "服务器 URL",
|
base_url: "服务器 URL",
|
||||||
|
@ -5,7 +5,13 @@ import { createRoot } from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { AppContext } from "./AppContext";
|
import { AppContext } from "./AppContext";
|
||||||
|
|
||||||
fetch("config.json")
|
const baseUrl = import.meta.env.BASE_URL;
|
||||||
|
const configJSON = "config.json";
|
||||||
|
// if import.meta.env.BASE_URL have a trailing slash, remove it
|
||||||
|
// load config.json from relative path if import.meta.env.BASE_URL is None or empty
|
||||||
|
const configJSONUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}/${configJSON}` : configJSON;
|
||||||
|
|
||||||
|
fetch(configJSONUrl)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(props =>
|
.then(props =>
|
||||||
createRoot(document.getElementById("root")).render(
|
createRoot(document.getElementById("root")).render(
|
||||||
|
@ -217,7 +217,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
onBlur={handleUsernameChange}
|
onBlur={handleUsernameChange}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -229,7 +228,6 @@ const LoginPage = () => {
|
|||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -242,7 +240,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
readOnly={allowSingleBaseUrl}
|
readOnly={allowSingleBaseUrl}
|
||||||
resettable={allowAnyBaseUrl}
|
resettable={allowAnyBaseUrl}
|
||||||
fullWidth
|
|
||||||
validate={[required(), validateBaseUrl]}
|
validate={[required(), validateBaseUrl]}
|
||||||
>
|
>
|
||||||
{allowMultipleBaseUrls &&
|
{allowMultipleBaseUrls &&
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { get } from "lodash";
|
||||||
import { MouseEvent } from "react";
|
import { MouseEvent } from "react";
|
||||||
|
|
||||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
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";
|
||||||
|
import { blue } from "@mui/material/colors";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
@ -27,16 +29,14 @@ import {
|
|||||||
useNotify,
|
useNotify,
|
||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
|
DateFieldProps,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import { DATE_FORMAT } from "../components/date";
|
import { DATE_FORMAT } from "../components/date";
|
||||||
|
import { lighten, useTheme } from '@mui/material';
|
||||||
|
|
||||||
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
const destinationRowSx = (record: RaRecord) => ({
|
|
||||||
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
|
||||||
});
|
|
||||||
|
|
||||||
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
||||||
|
|
||||||
export const DestinationReconnectButton = () => {
|
export const DestinationReconnectButton = () => {
|
||||||
@ -92,7 +92,25 @@ const DestinationTitle = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RetryDateField = (props: DateFieldProps) => {
|
||||||
|
const record = useRecordContext(props);
|
||||||
|
if (props.source && get(record, props.source) === 0) {
|
||||||
|
return <DateField {...props} record={{ ...record, [props.source]: null }} />;
|
||||||
|
}
|
||||||
|
return <DateField {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
export const DestinationList = (props: ListProps) => {
|
export const DestinationList = (props: ListProps) => {
|
||||||
|
const { palette: { error, mode }, } = useTheme();
|
||||||
|
const destinationRowSx = (record: RaRecord) => ({
|
||||||
|
backgroundColor: record.retry_last_ts > 0 ? lighten(error[mode], 0.5) : undefined,
|
||||||
|
"& > td": mode === 'dark' ? {
|
||||||
|
color: record.retry_last_ts > 0 ? "black" : "white",
|
||||||
|
"& > button": {
|
||||||
|
color: blue[700],
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
@ -103,7 +121,7 @@ export const DestinationList = (props: ListProps) => {
|
|||||||
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
<Datagrid rowSx={destinationRowSx} rowClick={id => `${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} />
|
||||||
<DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||||
<TextField source="retry_interval" />
|
<TextField source="retry_interval" />
|
||||||
<TextField source="last_successful_stream_ordering" />
|
<TextField source="last_successful_stream_ordering" />
|
||||||
<DestinationReconnectButton />
|
<DestinationReconnectButton />
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
useRefresh,
|
useRefresh,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
|
|
||||||
@ -70,27 +70,25 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
|||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const unselectAllRooms = useUnselectAll("rooms");
|
const unselectAllRooms = useUnselectAll("rooms");
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
const { mutate, isLoading } = useMutation(
|
const { mutate, isPending } = useMutation({
|
||||||
() =>
|
mutationFn: () =>
|
||||||
dataProvider.createMany("room_directory", {
|
dataProvider.createMany("room_directory", {
|
||||||
ids: selectedIds,
|
ids: selectedIds,
|
||||||
data: {},
|
data: {},
|
||||||
}),
|
}),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("resources.room_directory.action.send_success");
|
||||||
notify("resources.room_directory.action.send_success");
|
unselectAllRooms();
|
||||||
unselectAllRooms();
|
refresh();
|
||||||
refresh();
|
},
|
||||||
},
|
onError: () =>
|
||||||
onError: () =>
|
notify("resources.room_directory.action.send_failure", {
|
||||||
notify("resources.room_directory.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isLoading}>
|
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isPending}>
|
||||||
<RoomDirectoryIcon />
|
<RoomDirectoryIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -102,6 +100,10 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
|||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const [create, { isLoading }] = useCreate();
|
const [create, { isLoading }] = useCreate();
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
create(
|
create(
|
||||||
"room_directory",
|
"room_directory",
|
||||||
|
@ -14,10 +14,13 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
|
Create,
|
||||||
|
CreateProps,
|
||||||
Edit,
|
Edit,
|
||||||
EditProps,
|
EditProps,
|
||||||
List,
|
List,
|
||||||
ListProps,
|
ListProps,
|
||||||
|
SimpleForm,
|
||||||
SimpleFormIterator,
|
SimpleFormIterator,
|
||||||
TabbedForm,
|
TabbedForm,
|
||||||
FormTab,
|
FormTab,
|
||||||
@ -31,15 +34,20 @@ import {
|
|||||||
ResourceProps,
|
ResourceProps,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
|
BulkDeleteButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
|
maxLength,
|
||||||
|
regex,
|
||||||
required,
|
required,
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
CreateButton,
|
||||||
ExportButton,
|
ExportButton,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
NumberField,
|
NumberField,
|
||||||
useListContext,
|
useListContext,
|
||||||
|
Identifier,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
@ -63,27 +71,32 @@ const UserListActions = () => {
|
|||||||
const { isLoading, total } = useListContext();
|
const { isLoading, total } = useListContext();
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
|
<CreateButton />
|
||||||
<ExportButton disabled={isLoading || total === 0} maxResults={10000} />
|
<ExportButton disabled={isLoading || total === 0} maxResults={10000} />
|
||||||
|
<Button component={Link} to="/import_users" label="CSV Import">
|
||||||
|
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
|
||||||
|
</Button>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
UserListActions.defaultProps = {
|
|
||||||
selectedIds: [],
|
|
||||||
onUnselectItems: () => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
const userFilters = [
|
const userFilters = [
|
||||||
<SearchInput source="name" alwaysOn />,
|
<SearchInput source="name" alwaysOn />,
|
||||||
<BooleanInput source="guests" alwaysOn />,
|
<BooleanInput source="guests" alwaysOn />,
|
||||||
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
||||||
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserBulkActionButtons = () => (
|
const UserBulkActionButtons = () => (
|
||||||
<>
|
<>
|
||||||
<ServerNoticeBulkButton />
|
<ServerNoticeBulkButton />
|
||||||
|
<BulkDeleteButton
|
||||||
|
label="resources.users.action.erase"
|
||||||
|
confirmTitle="resources.users.helper.erase"
|
||||||
|
mutationMode="pessimistic"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,12 +104,15 @@ export const UserList = (props: ListProps) => (
|
|||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={userFilters}
|
filters={userFilters}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
actions={<UserListActions />}
|
actions={<UserListActions />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
<Datagrid
|
||||||
|
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
|
||||||
|
bulkActionButtons={<UserBulkActionButtons />}
|
||||||
|
>
|
||||||
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
||||||
<TextField source="id" sortBy="name" />
|
<TextField source="id" sortBy="name" />
|
||||||
<TextField source="displayname" />
|
<TextField source="displayname" />
|
||||||
@ -110,6 +126,14 @@ export const UserList = (props: ListProps) => (
|
|||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||||||
|
// here only local part of user_id
|
||||||
|
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||||||
|
// storage.getItem("home_server").length is not valid here
|
||||||
|
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
|
||||||
|
|
||||||
|
const validateAddress = [required(), maxLength(255)];
|
||||||
|
|
||||||
const UserEditActions = () => {
|
const UserEditActions = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@ -117,10 +141,46 @@ const UserEditActions = () => {
|
|||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{!record?.deactivated && <ServerNoticeButton />}
|
{!record?.deactivated && <ServerNoticeButton />}
|
||||||
|
<DeleteButton
|
||||||
|
label="resources.users.action.erase"
|
||||||
|
confirmTitle={translate("resources.users.helper.erase", {
|
||||||
|
smart_count: 1,
|
||||||
|
})}
|
||||||
|
mutationMode="pessimistic"
|
||||||
|
/>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UserCreate = (props: CreateProps) => (
|
||||||
|
<Create
|
||||||
|
{...props}
|
||||||
|
redirect={(resource: string | undefined, id: Identifier | undefined) => {
|
||||||
|
return `${resource}/${id}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
|
<TextInput source="displayname" validate={maxLength(256)} />
|
||||||
|
<PasswordInput source="password" autoComplete="new-password" validate={maxLength(512)} />
|
||||||
|
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||||||
|
<BooleanInput source="admin" />
|
||||||
|
<ArrayInput source="threepids">
|
||||||
|
<SimpleFormIterator disableReordering>
|
||||||
|
<SelectInput source="medium" choices={choices_medium} validate={required()} />
|
||||||
|
<TextInput source="address" validate={validateAddress} />
|
||||||
|
</SimpleFormIterator>
|
||||||
|
</ArrayInput>
|
||||||
|
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
|
||||||
|
<SimpleFormIterator disableReordering>
|
||||||
|
<TextInput source="auth_provider" validate={required()} />
|
||||||
|
<TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
|
||||||
|
</SimpleFormIterator>
|
||||||
|
</ArrayInput>
|
||||||
|
</SimpleForm>
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
|
|
||||||
const UserTitle = () => {
|
const UserTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@ -268,6 +328,7 @@ const resource: ResourceProps = {
|
|||||||
icon: UserIcon,
|
icon: UserIcon,
|
||||||
list: UserList,
|
list: UserList,
|
||||||
edit: UserEdit,
|
edit: UserEdit,
|
||||||
|
create: UserCreate,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default resource;
|
export default resource;
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
const storage = sessionStorage;
|
const storage = localStorage;
|
||||||
|
|
||||||
export default storage;
|
export default storage;
|
||||||
|
@ -30,7 +30,17 @@ describe("authProvider", () => {
|
|||||||
|
|
||||||
expect(ret).toBe(undefined);
|
expect(ret).toBe(undefined);
|
||||||
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
|
body: JSON.stringify({
|
||||||
|
device_id: null,
|
||||||
|
initial_device_display_name: "Synapse Admin",
|
||||||
|
type: "m.login.password",
|
||||||
|
user: "@user:example.com",
|
||||||
|
password: "secret",
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: "@user:example.com",
|
||||||
|
}
|
||||||
|
}),
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -33,6 +33,10 @@ const authProvider: AuthProvider = {
|
|||||||
type: "m.login.password",
|
type: "m.login.password",
|
||||||
user: username,
|
user: username,
|
||||||
password: password,
|
password: password,
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: username,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { stringify } from "query-string";
|
import { stringify } from "query-string";
|
||||||
|
import {
|
||||||
import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
DataProvider,
|
||||||
|
DeleteParams,
|
||||||
|
Identifier,
|
||||||
|
Options,
|
||||||
|
PaginationPayload,
|
||||||
|
RaRecord,
|
||||||
|
SortPayload,
|
||||||
|
fetchUtils
|
||||||
|
} from "react-admin";
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
|
|
||||||
// Adds the access token to all requests
|
// Adds the access token to all requests
|
||||||
@ -491,9 +498,9 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
|||||||
const dataProvider: SynapseDataProvider = {
|
const dataProvider: SynapseDataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const { user_id, name, guests, deactivated, search_term, destination, valid } = params.filter;
|
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
||||||
const { page, perPage } = params.pagination;
|
const { page, perPage } = params.pagination as PaginationPayload;
|
||||||
const { field, order } = params.sort;
|
const { field, order } = params.sort as SortPayload;
|
||||||
const from = (page - 1) * perPage;
|
const from = (page - 1) * perPage;
|
||||||
const query = {
|
const query = {
|
||||||
from: from,
|
from: from,
|
||||||
@ -504,6 +511,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
destination: destination,
|
destination: destination,
|
||||||
guests: guests,
|
guests: guests,
|
||||||
deactivated: deactivated,
|
deactivated: deactivated,
|
||||||
|
locked: locked,
|
||||||
valid: valid,
|
valid: valid,
|
||||||
order_by: field,
|
order_by: field,
|
||||||
dir: getSearchOrder(order),
|
dir: getSearchOrder(order),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user