mirror of
https://github.com/UA-Fediland/synapse-admin.git
synced 2024-10-30 03:14:51 +00:00
Compare commits
No commits in common. "b5ca951b323b4eeb3d6b901980264aca86c0050e" and "b112689b8c796686872cb573ad2a582dcaacf3b5" have entirely different histories.
b5ca951b32
...
b112689b8c
53 changed files with 3556 additions and 2330 deletions
|
@ -1,13 +0,0 @@
|
||||||
# EditorConfig https://EditorConfig.org
|
|
||||||
|
|
||||||
# top-most EditorConfig file
|
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
indent_size = 2
|
|
||||||
indent_style = space
|
|
||||||
insert_final_newline = true
|
|
||||||
max_line_length = 120
|
|
||||||
trim_trailing_whitespace = true
|
|
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
|
@ -17,7 +17,5 @@ jobs:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn --immutable
|
run: yarn --immutable
|
||||||
- name: Run checks
|
|
||||||
run: yarn lint
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
.vscode
|
|
||||||
.yarn
|
.yarn
|
||||||
|
|
11
.prettierrc
Normal file
11
.prettierrc
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
10
README.md
10
README.md
|
@ -13,7 +13,7 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
|
||||||
|
|
||||||
### Supported Synapse
|
### Supported Synapse
|
||||||
|
|
||||||
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.93.0 for all functions to work as expected!
|
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.52.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://element-hq.github.io/synapse/latest/admin_api/version_api.html).
|
See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
|
||||||
|
@ -104,7 +104,10 @@ or to a list of homeservers:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"restrictBaseUrl": ["https://your-first-matrix-server.example.com", "https://your-second-matrix-server.example.com"]
|
"restrictBaseUrl": [
|
||||||
|
"https://your-first-matrix-server.example.com",
|
||||||
|
"https://your-second-matrix-server.example.com"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -163,6 +166,5 @@ services:
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
|
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
|
||||||
- Use `yarn lint` to run all style and linter checks
|
- Use `yarn test` to run all style, lint and unit tests
|
||||||
- Use `yarn test` to run all unit tests
|
|
||||||
- Use `yarn fix` to fix the coding style
|
- Use `yarn fix` to fix the coding style
|
||||||
|
|
|
@ -119,7 +119,7 @@
|
||||||
<div class="loader">Loading...</div>
|
<div class="loader">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
<footer
|
<footer
|
||||||
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||||
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import type { JestConfigWithTsJest } from "ts-jest";
|
|
||||||
|
|
||||||
const config: JestConfigWithTsJest = {
|
|
||||||
preset: "ts-jest",
|
|
||||||
testEnvironment: "jsdom",
|
|
||||||
collectCoverage: true,
|
|
||||||
coveragePathIgnorePatterns: ["node_modules", "dist"],
|
|
||||||
coverageDirectory: "<rootDir>/coverage/",
|
|
||||||
coverageReporters: ["html", "text", "text-summary", "cobertura"],
|
|
||||||
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
|
||||||
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
|
||||||
};
|
|
||||||
export default config;
|
|
140
package.json
140
package.json
|
@ -12,35 +12,24 @@
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.1.1",
|
"@babel/core": "^7.24.4",
|
||||||
|
"@babel/preset-env": "^7.24.4",
|
||||||
|
"@babel/preset-react": "^7.24.1",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@testing-library/dom": "^10.0.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^15.0.2",
|
"@testing-library/react": "^15.0.2",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/lodash": "^4.17.0",
|
|
||||||
"@types/node": "^20.12.7",
|
|
||||||
"@types/papaparse": "^5.3.14",
|
|
||||||
"@types/react": "^18.3.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
|
||||||
"@typescript-eslint/parser": "^7.8.0",
|
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"babel-jest": "^29.7.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
|
||||||
"eslint-plugin-yaml": "^0.5.0",
|
|
||||||
"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",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.2.0",
|
||||||
"ts-jest": "^29.1.2",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.4.5",
|
|
||||||
"typescript-eslint": "^7.8.0",
|
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-plugin-version-mark": "^0.0.13"
|
"vite-plugin-version-mark": "^0.0.13"
|
||||||
},
|
},
|
||||||
|
@ -49,103 +38,58 @@
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.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.15.16",
|
"@mui/icons-material": "^5.15.15",
|
||||||
"@mui/material": "^5.15.16",
|
"@mui/material": "^5.15.15",
|
||||||
"history": "^5.1.0",
|
"history": "^5.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"ra-core": "^4.16.17",
|
"ra-core": "^4.16.15",
|
||||||
"ra-i18n-polyglot": "^4.16.17",
|
"ra-i18n-polyglot": "^4.16.15",
|
||||||
"ra-language-english": "^4.16.17",
|
"ra-language-english": "^4.16.15",
|
||||||
"ra-language-farsi": "^4.2.0",
|
"ra-language-farsi": "^4.2.0",
|
||||||
"ra-language-french": "^4.16.17",
|
"ra-language-french": "^4.16.15",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.0.0",
|
||||||
"react-admin": "^4.16.17",
|
"react-admin": "^4.16.15",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.0.0",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.2.0",
|
||||||
"react-query": "^3.32.1",
|
"react-query": "^3.32.1",
|
||||||
"react-router": "^6.23.0",
|
"react-router": "^6.1.0",
|
||||||
"react-router-dom": "^6.23.0"
|
"react-router-dom": "^6.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx,.yml,.yaml .",
|
"fix:other": "yarn prettier --write",
|
||||||
"fix": "yarn lint --fix",
|
"fix:code": "yarn test:lint --fix",
|
||||||
"test": "yarn jest",
|
"fix": "yarn fix:code && yarn fix:other",
|
||||||
"test:watch": "yarn jest --watch"
|
"prettier": "prettier \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
|
||||||
|
"test:code": "jest",
|
||||||
|
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
|
||||||
|
"test:style": "yarn prettier --check",
|
||||||
|
"test": "yarn test:style && yarn test:lint && yarn test:code"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
[
|
||||||
|
"@babel/preset-react",
|
||||||
|
{
|
||||||
|
"runtime": "automatic"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"env": {
|
"extends": "react-app"
|
||||||
"browser": true
|
|
||||||
},
|
},
|
||||||
"plugins": [
|
"jest": {
|
||||||
"import",
|
"testEnvironment": "jsdom",
|
||||||
"prettier",
|
"setupFilesAfterEnv": [
|
||||||
"unused-imports",
|
"<rootDir>/src/setupTests.js"
|
||||||
"@typescript-eslint",
|
|
||||||
"yaml"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@typescript-eslint/stylistic",
|
|
||||||
"plugin:import/typescript",
|
|
||||||
"plugin:yaml/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.eslint.json"
|
|
||||||
},
|
|
||||||
"root": true,
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"import/no-extraneous-dependencies": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"devDependencies": [
|
|
||||||
"**/vite.config.ts",
|
|
||||||
"**/jest.setup.ts",
|
|
||||||
"**/*.test.ts",
|
|
||||||
"**/*.test.tsx"
|
|
||||||
]
|
]
|
||||||
}
|
|
||||||
],
|
|
||||||
"import/order": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"alphabetize": {
|
|
||||||
"order": "asc",
|
|
||||||
"caseInsensitive": false
|
|
||||||
},
|
|
||||||
"newlines-between": "always",
|
|
||||||
"groups": [
|
|
||||||
"external",
|
|
||||||
"builtin",
|
|
||||||
"internal",
|
|
||||||
[
|
|
||||||
"parent",
|
|
||||||
"sibling",
|
|
||||||
"index"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"unused-imports/no-unused-imports-ts": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prettier": {
|
|
||||||
"printWidth": 120,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "avoid"
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
import { merge } from "lodash";
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Admin,
|
||||||
|
CustomRoutes,
|
||||||
|
Resource,
|
||||||
|
resolveBrowserLocale,
|
||||||
|
} from "react-admin";
|
||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||||
|
import merge from "lodash/merge";
|
||||||
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
import authProvider from "./synapse/authProvider";
|
||||||
import { Route } from "react-router-dom";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
|
import users from "./components/users";
|
||||||
import reports from "./components/EventReports";
|
|
||||||
import { ImportFeature } from "./components/ImportFeature";
|
|
||||||
import LoginPage from "./components/LoginPage";
|
|
||||||
import registrationToken from "./components/RegistrationTokens";
|
|
||||||
import roomDirectory from "./components/RoomDirectory";
|
|
||||||
import destinations from "./components/destinations";
|
|
||||||
import rooms from "./components/rooms";
|
import rooms from "./components/rooms";
|
||||||
import userMediaStats from "./components/statistics";
|
import userMediaStats from "./components/statistics";
|
||||||
import users from "./components/users";
|
import reports from "./components/EventReports";
|
||||||
|
import roomDirectory from "./components/RoomDirectory";
|
||||||
|
import destinations from "./components/destinations";
|
||||||
|
import registrationToken from "./components/RegistrationTokens";
|
||||||
|
import LoginPage from "./components/LoginPage";
|
||||||
|
import { ImportFeature } from "./components/ImportFeature";
|
||||||
|
import { Route } from "react-router-dom";
|
||||||
import germanMessages from "./i18n/de";
|
import germanMessages from "./i18n/de";
|
||||||
import englishMessages from "./i18n/en";
|
import englishMessages from "./i18n/en";
|
||||||
import frenchMessages from "./i18n/fr";
|
import frenchMessages from "./i18n/fr";
|
||||||
import italianMessages from "./i18n/it";
|
|
||||||
import chineseMessages from "./i18n/zh";
|
import chineseMessages from "./i18n/zh";
|
||||||
import authProvider from "./synapse/authProvider";
|
import italianMessages from "./i18n/it";
|
||||||
import dataProvider from "./synapse/dataProvider";
|
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
|
@ -30,7 +34,8 @@ const messages = {
|
||||||
zh: chineseMessages,
|
zh: chineseMessages,
|
||||||
};
|
};
|
||||||
const i18nProvider = polyglotI18nProvider(
|
const i18nProvider = polyglotI18nProvider(
|
||||||
locale => (messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en),
|
locale =>
|
||||||
|
messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en,
|
||||||
resolveBrowserLocale(),
|
resolveBrowserLocale(),
|
||||||
[
|
[
|
||||||
{ locale: "en", name: "English" },
|
{ locale: "en", name: "English" },
|
|
@ -1,5 +1,5 @@
|
||||||
|
import React from "react";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
5
src/AppContext.jsx
Normal file
5
src/AppContext.jsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
export const AppContext = createContext({});
|
||||||
|
|
||||||
|
export const useAppContext = () => useContext(AppContext);
|
|
@ -1,9 +0,0 @@
|
||||||
import { createContext, useContext } from "react";
|
|
||||||
|
|
||||||
interface AppContextType {
|
|
||||||
restrictBaseUrl: string | string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppContext = createContext({});
|
|
||||||
|
|
||||||
export const useAppContext = () => useContext(AppContext) as AppContextType;
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { get } from "lodash";
|
import React from "react";
|
||||||
|
import get from "lodash/get";
|
||||||
import { Avatar } from "@mui/material";
|
import { Avatar } from "@mui/material";
|
||||||
import { useRecordContext } from "react-admin";
|
import { useRecordContext } from "react-admin";
|
||||||
|
|
||||||
|
@ -7,7 +7,16 @@ const AvatarField = ({ source, ...rest }) => {
|
||||||
const record = useRecordContext(rest);
|
const record = useRecordContext(rest);
|
||||||
const src = get(record, source)?.toString();
|
const src = get(record, source)?.toString();
|
||||||
const { alt, classes, sizes, sx, variant } = rest;
|
const { alt, classes, sizes, sx, variant } = rest;
|
||||||
return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
|
return (
|
||||||
|
<Avatar
|
||||||
|
alt={alt}
|
||||||
|
classes={classes}
|
||||||
|
sizes={sizes}
|
||||||
|
src={src}
|
||||||
|
sx={sx}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AvatarField;
|
export default AvatarField;
|
|
@ -1,6 +1,6 @@
|
||||||
import { render, screen } from "@testing-library/react";
|
import React from "react";
|
||||||
import { RecordContextProvider } from "react-admin";
|
import { RecordContextProvider } from "react-admin";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
import AvatarField from "./AvatarField";
|
import AvatarField from "./AvatarField";
|
||||||
|
|
||||||
describe("AvatarField", () => {
|
describe("AvatarField", () => {
|
|
@ -1,18 +1,13 @@
|
||||||
import PageviewIcon from "@mui/icons-material/Pageview";
|
import React from "react";
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
|
||||||
import ReportIcon from "@mui/icons-material/Warning";
|
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
List,
|
List,
|
||||||
ListProps,
|
|
||||||
NumberField,
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
ResourceProps,
|
|
||||||
Show,
|
Show,
|
||||||
ShowProps,
|
|
||||||
Tab,
|
Tab,
|
||||||
TabbedShowLayout,
|
TabbedShowLayout,
|
||||||
TextField,
|
TextField,
|
||||||
|
@ -20,13 +15,25 @@ import {
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import { DATE_FORMAT } from "./date";
|
|
||||||
import { MXCField } from "./media";
|
import { MXCField } from "./media";
|
||||||
|
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||||
|
import ReportIcon from "@mui/icons-material/Warning";
|
||||||
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
|
||||||
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const date_format = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
export const ReportShow = (props: ShowProps) => {
|
const ReportPagination = () => (
|
||||||
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ReportShow = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Show {...props} actions={<ReportShowActions />}>
|
<Show {...props} actions={<ReportShowActions />}>
|
||||||
|
@ -37,21 +44,43 @@ export const ReportShow = (props: ShowProps) => {
|
||||||
})}
|
})}
|
||||||
icon={<ViewListIcon />}
|
icon={<ViewListIcon />}
|
||||||
>
|
>
|
||||||
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} />
|
<DateField
|
||||||
|
source="received_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={true}
|
||||||
|
/>
|
||||||
<ReferenceField source="user_id" reference="users">
|
<ReferenceField source="user_id" reference="users">
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<NumberField source="score" />
|
<NumberField source="score" />
|
||||||
<TextField source="reason" />
|
<TextField source="reason" />
|
||||||
<TextField source="name" />
|
<TextField source="name" />
|
||||||
<TextField source="canonical_alias" label="resources.rooms.fields.canonical_alias" />
|
<TextField
|
||||||
<ReferenceField source="room_id" reference="rooms" link="show" label="resources.rooms.fields.room_id">
|
source="canonical_alias"
|
||||||
|
label="resources.rooms.fields.canonical_alias"
|
||||||
|
/>
|
||||||
|
<ReferenceField
|
||||||
|
source="room_id"
|
||||||
|
reference="rooms"
|
||||||
|
link="show"
|
||||||
|
label="resources.rooms.fields.room_id"
|
||||||
|
>
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label="synapseadmin.reports.tabs.detail" icon={<PageviewIcon />} path="detail">
|
<Tab
|
||||||
<DateField source="event_json.origin_server_ts" showTime options={DATE_FORMAT} sortable={true} />
|
label="synapseadmin.reports.tabs.detail"
|
||||||
|
icon={<PageviewIcon />}
|
||||||
|
path="detail"
|
||||||
|
>
|
||||||
|
<DateField
|
||||||
|
source="event_json.origin_server_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={true}
|
||||||
|
/>
|
||||||
<ReferenceField source="sender" reference="users">
|
<ReferenceField source="sender" reference="users">
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
|
@ -66,7 +95,10 @@ export const ReportShow = (props: ShowProps) => {
|
||||||
<TextField source="event_json.content.format" />
|
<TextField source="event_json.content.format" />
|
||||||
<TextField source="event_json.content.formatted_body" />
|
<TextField source="event_json.content.formatted_body" />
|
||||||
<TextField source="event_json.content.algorithm" />
|
<TextField source="event_json.content.algorithm" />
|
||||||
<TextField source="event_json.content.device_id" label="resources.devices.fields.device_id" />
|
<TextField
|
||||||
|
source="event_json.content.device_id"
|
||||||
|
label="resources.devices.fields.device_id"
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabbedShowLayout>
|
</TabbedShowLayout>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -88,11 +120,20 @@ const ReportShowActions = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReportList = (props: ListProps) => (
|
export const ReportList = props => (
|
||||||
<List {...props} pagination={<ReportPagination />} sort={{ field: "received_ts", order: "DESC" }}>
|
<List
|
||||||
|
{...props}
|
||||||
|
pagination={<ReportPagination />}
|
||||||
|
sort={{ field: "received_ts", order: "DESC" }}
|
||||||
|
>
|
||||||
<Datagrid rowClick="show" bulkActionButtons={false}>
|
<Datagrid rowClick="show" bulkActionButtons={false}>
|
||||||
<TextField source="id" sortable={false} />
|
<TextField source="id" sortable={false} />
|
||||||
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} />
|
<DateField
|
||||||
|
source="received_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={true}
|
||||||
|
/>
|
||||||
<TextField sortable={false} source="user_id" />
|
<TextField sortable={false} source="user_id" />
|
||||||
<TextField sortable={false} source="name" />
|
<TextField sortable={false} source="name" />
|
||||||
<TextField sortable={false} source="score" />
|
<TextField sortable={false} source="score" />
|
||||||
|
@ -100,7 +141,7 @@ export const ReportList = (props: ListProps) => (
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
const resource: ResourceProps = {
|
const resource = {
|
||||||
name: "reports",
|
name: "reports",
|
||||||
icon: ReportIcon,
|
icon: ReportIcon,
|
||||||
list: ReportList,
|
list: ReportList,
|
|
@ -1,6 +1,6 @@
|
||||||
import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
|
import React, { useState } from "react";
|
||||||
import { ChangeEvent, useState } from "react";
|
import { useDataProvider, useNotify, Title } from "react-admin";
|
||||||
|
import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
@ -12,65 +12,36 @@ import {
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
NativeSelect,
|
NativeSelect,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { DataProvider, useTranslate } from "ra-core";
|
import { useTranslate } from "ra-core";
|
||||||
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
|
import { generateRandomUser } from "./users";
|
||||||
|
|
||||||
import { generateRandomMxId, generateRandomPassword } from "../synapse/synapse";
|
|
||||||
|
|
||||||
const LOGGING = true;
|
const LOGGING = true;
|
||||||
|
|
||||||
const expectedFields = ["id", "displayname"].sort();
|
const expectedFields = ["id", "displayname"].sort();
|
||||||
|
const optionalFields = [
|
||||||
|
"user_type",
|
||||||
|
"guest",
|
||||||
|
"admin",
|
||||||
|
"deactivated",
|
||||||
|
"avatar_url",
|
||||||
|
"password",
|
||||||
|
].sort();
|
||||||
|
|
||||||
function TranslatableOption({ value, text }) {
|
function TranslatableOption({ value, text }) {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return <option value={value}>{translate(text)}</option>;
|
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 FilePicker = () => {
|
||||||
const [values, setValues] = useState<ImportLine[]>([]);
|
const [values, setValues] = useState(null);
|
||||||
const [error, setError] = useState<string | string[] | null>(null);
|
const [error, setError] = useState(null);
|
||||||
const [stats, setStats] = useState<ChangeStats | null>(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [dryRun, setDryRun] = useState(true);
|
const [dryRun, setDryRun] = useState(true);
|
||||||
|
|
||||||
const [progress, setProgress] = useState<Progress>(null);
|
const [progress, setProgress] = useState(null);
|
||||||
|
|
||||||
const [importResults, setImportResults] = useState<ImportResult | null>(null);
|
const [importResults, setImportResults] = useState(null);
|
||||||
const [skippedRecords, setSkippedRecords] = useState<string>("");
|
const [skippedRecords, setSkippedRecords] = useState(null);
|
||||||
|
|
||||||
const [conflictMode, setConflictMode] = useState("stop");
|
const [conflictMode, setConflictMode] = useState("stop");
|
||||||
const [passwordMode, setPasswordMode] = useState(true);
|
const [passwordMode, setPasswordMode] = useState(true);
|
||||||
|
@ -81,15 +52,14 @@ const FilePicker = () => {
|
||||||
|
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
const onFileChange = async e => {
|
||||||
if (progress !== null) return;
|
if (progress !== null) return;
|
||||||
|
|
||||||
setValues([]);
|
setValues(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setStats(null);
|
setStats(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;
|
||||||
if (!file) return;
|
|
||||||
/* Let's refuse some unreasonably big files instead of freezing
|
/* Let's refuse some unreasonably big files instead of freezing
|
||||||
* up the browser */
|
* up the browser */
|
||||||
if (file.size > 100000000) {
|
if (file.size > 100000000) {
|
||||||
|
@ -101,12 +71,12 @@ const FilePicker = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
parseCsv<ImportLine>(file, {
|
parseCsv(file, {
|
||||||
header: true,
|
header: true,
|
||||||
skipEmptyLines: true /* especially for a final EOL in the csv file */,
|
skipEmptyLines: true /* especially for a final EOL in the csv file */,
|
||||||
complete: result => {
|
complete: result => {
|
||||||
if (result.errors) {
|
if (result.error) {
|
||||||
setError(result.errors.map(e => e.toString()));
|
setError(result.error);
|
||||||
}
|
}
|
||||||
/* Papaparse is very lenient, we may be able to salvage
|
/* Papaparse is very lenient, we may be able to salvage
|
||||||
* the data in the file. */
|
* the data in the file. */
|
||||||
|
@ -114,17 +84,32 @@ const FilePicker = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
setError("Unknown error");
|
setError(true);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
const verifyCsv = (
|
||||||
|
{ data, meta, errors },
|
||||||
|
{ setValues, setStats, setError }
|
||||||
|
) => {
|
||||||
/* First, verify the presence of required fields */
|
/* First, verify the presence of required fields */
|
||||||
const missingFields = expectedFields.filter(eF => meta.fields?.find(mF => eF === mF));
|
let eF = Array.from(expectedFields);
|
||||||
|
let oF = Array.from(optionalFields);
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
meta.fields.forEach(name => {
|
||||||
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
if (eF.includes(name)) {
|
||||||
|
eF = eF.filter(v => v !== name);
|
||||||
|
}
|
||||||
|
if (oF.includes(name)) {
|
||||||
|
oF = oF.filter(v => v !== name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eF.length !== 0) {
|
||||||
|
setError(
|
||||||
|
translate("import_users.error.required_field", { field: eF[0] })
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +119,7 @@ const FilePicker = () => {
|
||||||
/* Collect some stats to prevent sneaky csv files from adding admin
|
/* Collect some stats to prevent sneaky csv files from adding admin
|
||||||
users or something.
|
users or something.
|
||||||
*/
|
*/
|
||||||
const stats = {
|
let stats = {
|
||||||
user_types: { default: 0 },
|
user_types: { default: 0 },
|
||||||
is_guest: 0,
|
is_guest: 0,
|
||||||
admin: 0,
|
admin: 0,
|
||||||
|
@ -146,7 +131,6 @@ const FilePicker = () => {
|
||||||
total: data.length,
|
total: data.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
const errorMessages = errors.map(e => e.message);
|
|
||||||
data.forEach((line, idx) => {
|
data.forEach((line, idx) => {
|
||||||
if (line.user_type === undefined || line.user_type === "") {
|
if (line.user_type === undefined || line.user_type === "") {
|
||||||
stats.user_types.default++;
|
stats.user_types.default++;
|
||||||
|
@ -157,13 +141,14 @@ const FilePicker = () => {
|
||||||
* resource so it gives sensible field names and doesn't duplicate
|
* resource so it gives sensible field names and doesn't duplicate
|
||||||
* id as "name"?
|
* id as "name"?
|
||||||
*/
|
*/
|
||||||
if (meta.fields?.includes("name")) {
|
if (meta.fields.includes("name")) {
|
||||||
delete line.name;
|
delete line.name;
|
||||||
}
|
}
|
||||||
if (meta.fields?.includes("user_type")) {
|
if (meta.fields.includes("user_type")) {
|
||||||
delete line.user_type;
|
delete line.user_type;
|
||||||
}
|
}
|
||||||
if (meta.fields?.includes("is_admin")) {
|
if (meta.fields.includes("is_admin")) {
|
||||||
|
line.admin = line.is_admin;
|
||||||
delete line.is_admin;
|
delete line.is_admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +158,7 @@ const FilePicker = () => {
|
||||||
line[f] = true; // we need true booleans instead of strings
|
line[f] = true; // we need true booleans instead of strings
|
||||||
} else {
|
} else {
|
||||||
if (line[f] !== "false" && line[f] !== "") {
|
if (line[f] !== "false" && line[f] !== "") {
|
||||||
errorMessages.push(
|
errors.push(
|
||||||
translate("import_users.error.invalid_value", {
|
translate("import_users.error.invalid_value", {
|
||||||
field: f,
|
field: f,
|
||||||
row: idx,
|
row: idx,
|
||||||
|
@ -197,8 +182,8 @@ const FilePicker = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (errorMessages.length > 0) {
|
if (errors.length > 0) {
|
||||||
setError(errorMessages);
|
setError(errors);
|
||||||
}
|
}
|
||||||
setStats(stats);
|
setStats(stats);
|
||||||
setValues(data);
|
setValues(data);
|
||||||
|
@ -206,7 +191,7 @@ const FilePicker = () => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runImport = async () => {
|
const runImport = async _e => {
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
notify("import_users.errors.already_in_progress");
|
notify("import_users.errors.already_in_progress");
|
||||||
return;
|
return;
|
||||||
|
@ -235,40 +220,61 @@ const FilePicker = () => {
|
||||||
// which doesn't look very good.
|
// which doesn't look very good.
|
||||||
|
|
||||||
const doImport = async (
|
const doImport = async (
|
||||||
dataProvider: DataProvider,
|
dataProvider,
|
||||||
data: ImportLine[],
|
data,
|
||||||
conflictMode: string,
|
conflictMode,
|
||||||
passwordMode: boolean,
|
passwordMode,
|
||||||
useridMode: string,
|
useridMode,
|
||||||
dryRun: boolean,
|
dryRun,
|
||||||
setProgress: (progress: Progress) => void,
|
setProgress,
|
||||||
setError: (message: string) => void
|
setError
|
||||||
): Promise<ImportResult> => {
|
) => {
|
||||||
const skippedRecords: ImportLine[] = [];
|
let skippedRecords = [];
|
||||||
const erroredRecords: ImportLine[] = [];
|
let erroredRecords = [];
|
||||||
const succeededRecords: ImportLine[] = [];
|
let succeededRecords = [];
|
||||||
const changeStats: ChangeStats = {
|
let changeStats = {
|
||||||
total: 0,
|
toAdmin: 0,
|
||||||
id: 0,
|
toGuest: 0,
|
||||||
is_guest: 0,
|
toRegular: 0,
|
||||||
admin: 0,
|
replacedPassword: 0,
|
||||||
password: 0,
|
|
||||||
};
|
};
|
||||||
let entriesDone = 0;
|
let entriesDone = 0;
|
||||||
const entriesCount = data.length;
|
let entriesCount = data.length;
|
||||||
try {
|
try {
|
||||||
setProgress({ done: entriesDone, limit: entriesCount });
|
setProgress({ done: entriesDone, limit: entriesCount });
|
||||||
for (const entry of data) {
|
for (const entry of data) {
|
||||||
const userRecord = { ...entry };
|
let userRecord = {};
|
||||||
|
let overwriteData = {};
|
||||||
// No need to do a bunch of cryptographic random number getting if
|
// No need to do a bunch of cryptographic random number getting if
|
||||||
// we are using neither a generated password nor a generated user id.
|
// we are using neither a generated password nor a generated user id.
|
||||||
if (useridMode === "ignore" || userRecord.id === undefined) {
|
if (
|
||||||
userRecord.id = generateRandomMxId();
|
useridMode === "ignore" ||
|
||||||
|
entry.id === undefined ||
|
||||||
|
entry.password === undefined ||
|
||||||
|
passwordMode === false
|
||||||
|
) {
|
||||||
|
overwriteData = generateRandomUser();
|
||||||
|
// Ignoring IDs or the entry lacking an ID means we keep the
|
||||||
|
// ID field in the overwrite data.
|
||||||
|
if (!(useridMode === "ignore" || entry.id === undefined)) {
|
||||||
|
delete overwriteData.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not using passwords from the csv or this entry lacking a password
|
||||||
|
// means we keep the password field in the overwrite data.
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
passwordMode === false ||
|
||||||
|
entry.password === undefined ||
|
||||||
|
entry.password === ""
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete overwriteData.password;
|
||||||
}
|
}
|
||||||
if (passwordMode === false || entry.password === undefined) {
|
|
||||||
userRecord.password = generateRandomPassword();
|
|
||||||
}
|
}
|
||||||
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
|
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
|
||||||
|
Object.assign(userRecord, entry);
|
||||||
|
Object.assign(userRecord, overwriteData);
|
||||||
|
|
||||||
/* For these modes we will consider the ID that's in the record.
|
/* 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
|
* If the mode is "stop", we will not continue adding more records, and
|
||||||
|
@ -294,11 +300,14 @@ const FilePicker = () => {
|
||||||
* We do a simple retry loop so that an accidental hit on an existing ID
|
* We do a simple retry loop so that an accidental hit on an existing ID
|
||||||
* doesn't trip us up.
|
* doesn't trip us up.
|
||||||
*/
|
*/
|
||||||
if (LOGGING) console.log("will check for existence of record " + JSON.stringify(userRecord));
|
if (LOGGING)
|
||||||
|
console.log(
|
||||||
|
"will check for existence of record " + JSON.stringify(userRecord)
|
||||||
|
);
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
const submitRecord = (recordData: ImportLine) => {
|
const submitRecord = recordData => {
|
||||||
return dataProvider.getOne("users", { id: recordData.id }).then(
|
return dataProvider.getOne("users", { id: recordData.id }).then(
|
||||||
async () => {
|
async _alreadyExists => {
|
||||||
if (LOGGING) console.log("already existed");
|
if (LOGGING) console.log("already existed");
|
||||||
|
|
||||||
if (useridMode === "update" || conflictMode === "skip") {
|
if (useridMode === "update" || conflictMode === "skip") {
|
||||||
|
@ -310,8 +319,9 @@ const FilePicker = () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const overwriteData = generateRandomUser();
|
||||||
const newRecordData = Object.assign({}, recordData, {
|
const newRecordData = Object.assign({}, recordData, {
|
||||||
id: generateRandomMxId(),
|
id: overwriteData.id,
|
||||||
});
|
});
|
||||||
retries++;
|
retries++;
|
||||||
if (retries > 512) {
|
if (retries > 512) {
|
||||||
|
@ -322,8 +332,15 @@ const FilePicker = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async () => {
|
async _okToSubmit => {
|
||||||
if (LOGGING) console.log("OK to create record " + recordData.id + " (" + recordData.displayname + ").");
|
if (LOGGING)
|
||||||
|
console.log(
|
||||||
|
"OK to create record " +
|
||||||
|
recordData.id +
|
||||||
|
" (" +
|
||||||
|
recordData.displayname +
|
||||||
|
")."
|
||||||
|
);
|
||||||
|
|
||||||
if (!dryRun) {
|
if (!dryRun) {
|
||||||
await dataProvider.create("users", { data: recordData });
|
await dataProvider.create("users", { data: recordData });
|
||||||
|
@ -343,7 +360,7 @@ const FilePicker = () => {
|
||||||
setError(
|
setError(
|
||||||
translate("import_users.error.at_entry", {
|
translate("import_users.error.at_entry", {
|
||||||
entry: entriesDone + 1,
|
entry: entriesDone + 1,
|
||||||
message: e instanceof Error ? e.message : String(e),
|
message: e.message,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setProgress(null);
|
setProgress(null);
|
||||||
|
@ -370,7 +387,7 @@ const FilePicker = () => {
|
||||||
element.click();
|
element.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConflictModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
|
const onConflictModeChanged = async e => {
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -379,7 +396,7 @@ const FilePicker = () => {
|
||||||
setConflictMode(value);
|
setConflictMode(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPasswordModeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const onPasswordModeChange = e => {
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -387,7 +404,7 @@ const FilePicker = () => {
|
||||||
setPasswordMode(e.target.checked);
|
setPasswordMode(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUseridModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
|
const onUseridModeChanged = async e => {
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -396,11 +413,11 @@ const FilePicker = () => {
|
||||||
setUseridMode(value);
|
setUseridMode(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDryRunModeChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
const onDryRunModeChanged = ev => {
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDryRun(e.target.checked);
|
setDryRun(ev.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
// render individual small components
|
// render individual small components
|
||||||
|
@ -408,11 +425,28 @@ const FilePicker = () => {
|
||||||
const statsCards = stats &&
|
const statsCards = stats &&
|
||||||
!importResults && [
|
!importResults && [
|
||||||
<Container>
|
<Container>
|
||||||
<CardHeader title={translate("import_users.cards.importstats.header")} />
|
<CardHeader
|
||||||
|
title={translate("import_users.cards.importstats.header")}
|
||||||
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div>{translate("import_users.cards.importstats.users_total", stats.total)}</div>
|
<div>
|
||||||
<div>{translate("import_users.cards.importstats.guest_count", stats.is_guest)}</div>
|
{translate(
|
||||||
<div>{translate("import_users.cards.importstats.admin_count", stats.admin)}</div>
|
"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>
|
</CardContent>
|
||||||
</Container>,
|
</Container>,
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -425,9 +459,19 @@ const FilePicker = () => {
|
||||||
</div>
|
</div>
|
||||||
{stats.id > 0 ? (
|
{stats.id > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<NativeSelect onChange={onUseridModeChanged} value={useridMode} disabled={progress !== null}>
|
<NativeSelect
|
||||||
<TranslatableOption value="ignore" text="import_users.cards.ids.mode.ignore" />
|
onChange={onUseridModeChanged}
|
||||||
<TranslatableOption value="update" text="import_users.cards.ids.mode.update" />
|
value={useridMode}
|
||||||
|
enabled={(progress !== null).toString()}
|
||||||
|
>
|
||||||
|
<TranslatableOption
|
||||||
|
value="ignore"
|
||||||
|
text="import_users.cards.ids.mode.ignore"
|
||||||
|
/>
|
||||||
|
<TranslatableOption
|
||||||
|
value="update"
|
||||||
|
text="import_users.cards.ids.mode.update"
|
||||||
|
/>
|
||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -441,13 +485,20 @@ const FilePicker = () => {
|
||||||
<div>
|
<div>
|
||||||
{stats.password === stats.total
|
{stats.password === stats.total
|
||||||
? translate("import_users.cards.passwords.all_passwords_present")
|
? translate("import_users.cards.passwords.all_passwords_present")
|
||||||
: translate("import_users.cards.passwords.count_passwords_present", stats.password)}
|
: translate(
|
||||||
|
"import_users.cards.passwords.count_passwords_present",
|
||||||
|
stats.password
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{stats.password > 0 ? (
|
{stats.password > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox checked={passwordMode} disabled={progress !== null} onChange={onPasswordModeChange} />
|
<Checkbox
|
||||||
|
checked={passwordMode}
|
||||||
|
enabled={(progress !== null).toString()}
|
||||||
|
onChange={onPasswordModeChange}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
label={translate("import_users.cards.passwords.use_passwords")}
|
label={translate("import_users.cards.passwords.use_passwords")}
|
||||||
/>
|
/>
|
||||||
|
@ -459,21 +510,31 @@ const FilePicker = () => {
|
||||||
</Container>,
|
</Container>,
|
||||||
];
|
];
|
||||||
|
|
||||||
const conflictCards = stats && !importResults && (
|
let conflictCards = stats && !importResults && (
|
||||||
<Container>
|
<Container>
|
||||||
<CardHeader title={translate("import_users.cards.conflicts.header")} />
|
<CardHeader title={translate("import_users.cards.conflicts.header")} />
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div>
|
<div>
|
||||||
<NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
|
<NativeSelect
|
||||||
<TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
|
onChange={onConflictModeChanged}
|
||||||
<TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
|
value={conflictMode}
|
||||||
|
enabled={(progress !== null).toString()}
|
||||||
|
>
|
||||||
|
<TranslatableOption
|
||||||
|
value="stop"
|
||||||
|
text="import_users.cards.conflicts.mode.stop"
|
||||||
|
/>
|
||||||
|
<TranslatableOption
|
||||||
|
value="skip"
|
||||||
|
text="import_users.cards.conflicts.mode.skip"
|
||||||
|
/>
|
||||||
</NativeSelect>
|
</NativeSelect>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorCards = error && (
|
let errorCards = error && (
|
||||||
<Container>
|
<Container>
|
||||||
<CardHeader title={translate("import_users.error.error")} />
|
<CardHeader title={translate("import_users.error.error")} />
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
@ -484,7 +545,7 @@ const FilePicker = () => {
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
||||||
const uploadCard = !importResults && (
|
let uploadCard = !importResults && (
|
||||||
<Container>
|
<Container>
|
||||||
<CardHeader title={translate("import_users.cards.upload.header")} />
|
<CardHeader title={translate("import_users.cards.upload.header")} />
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
@ -492,22 +553,35 @@ const FilePicker = () => {
|
||||||
<a href="./data/example.csv">example.csv</a>
|
<a href="./data/example.csv">example.csv</a>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<input type="file" onChange={onFileChange} disabled={progress !== null} />
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={onFileChange}
|
||||||
|
enabled={(progress !== null).toString()}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
||||||
const resultsCard = importResults && (
|
let resultsCard = importResults && (
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<CardHeader title={translate("import_users.cards.results.header")} />
|
<CardHeader title={translate("import_users.cards.results.header")} />
|
||||||
<div>
|
<div>
|
||||||
{translate("import_users.cards.results.total", importResults.totalRecordCount)}
|
{translate(
|
||||||
|
"import_users.cards.results.total",
|
||||||
|
importResults.totalRecordCount
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
{translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
|
{translate(
|
||||||
|
"import_users.cards.results.successful",
|
||||||
|
importResults.succeededRecords.length
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
{importResults.skippedRecords.length
|
{importResults.skippedRecords.length
|
||||||
? [
|
? [
|
||||||
translate("import_users.cards.results.skipped", importResults.skippedRecords.length),
|
translate(
|
||||||
|
"import_users.cards.results.skipped",
|
||||||
|
importResults.skippedRecords.length
|
||||||
|
),
|
||||||
<div>
|
<div>
|
||||||
<button onClick={downloadSkippedRecords}>
|
<button onClick={downloadSkippedRecords}>
|
||||||
{translate("import_users.cards.results.download_skipped")}
|
{translate("import_users.cards.results.download_skipped")}
|
||||||
|
@ -517,22 +591,41 @@ const FilePicker = () => {
|
||||||
]
|
]
|
||||||
: ""}
|
: ""}
|
||||||
{importResults.erroredRecords.length
|
{importResults.erroredRecords.length
|
||||||
? [translate("import_users.cards.results.skipped", importResults.erroredRecords.length), <br />]
|
? [
|
||||||
|
translate(
|
||||||
|
"import_users.cards.results.skipped",
|
||||||
|
importResults.erroredRecords.length
|
||||||
|
),
|
||||||
|
<br />,
|
||||||
|
]
|
||||||
: ""}
|
: ""}
|
||||||
<br />
|
<br />
|
||||||
{importResults.wasDryRun && [translate("import_users.cards.results.simulated_only"), <br />]}
|
{importResults.wasDryRun && [
|
||||||
|
translate("import_users.cards.results.simulated_only"),
|
||||||
|
<br />,
|
||||||
|
]}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
);
|
);
|
||||||
|
|
||||||
const startImportCard =
|
let startImportCard =
|
||||||
!values || values.length === 0 || importResults ? undefined : (
|
!values || values.length === 0 || importResults ? undefined : (
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Checkbox checked={dryRun} onChange={onDryRunModeChanged} disabled={progress !== null} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={dryRun}
|
||||||
|
onChange={onDryRunModeChanged}
|
||||||
|
enabled={(progress !== null).toString()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={translate("import_users.cards.startImport.simulate_only")}
|
label={translate("import_users.cards.startImport.simulate_only")}
|
||||||
/>
|
/>
|
||||||
<Button size="large" onClick={runImport} disabled={progress !== null}>
|
<Button
|
||||||
|
size="large"
|
||||||
|
onClick={runImport}
|
||||||
|
enabled={(progress !== null).toString()}
|
||||||
|
>
|
||||||
{translate("import_users.cards.startImport.run_import")}
|
{translate("import_users.cards.startImport.run_import")}
|
||||||
</Button>
|
</Button>
|
||||||
{progress !== null ? (
|
{progress !== null ? (
|
||||||
|
@ -543,7 +636,7 @@ const FilePicker = () => {
|
||||||
</CardActions>
|
</CardActions>
|
||||||
);
|
);
|
||||||
|
|
||||||
const allCards: JSX.Element[] = [];
|
let allCards = [];
|
||||||
if (uploadCard) allCards.push(uploadCard);
|
if (uploadCard) allCards.push(uploadCard);
|
||||||
if (errorCards) allCards.push(errorCards);
|
if (errorCards) allCards.push(errorCards);
|
||||||
if (conflictCards) allCards.push(conflictCards);
|
if (conflictCards) allCards.push(conflictCards);
|
||||||
|
@ -551,9 +644,12 @@ const FilePicker = () => {
|
||||||
if (startImportCard) allCards.push(startImportCard);
|
if (startImportCard) allCards.push(startImportCard);
|
||||||
if (resultsCard) allCards.push(resultsCard);
|
if (resultsCard) allCards.push(resultsCard);
|
||||||
|
|
||||||
const cardContainer = <Card>{allCards}</Card>;
|
let cardContainer = <Card>{allCards}</Card>;
|
||||||
|
|
||||||
return [<Title defaultTitle={translate("import_users.title")} />, cardContainer];
|
return [
|
||||||
|
<Title defaultTitle={translate("import_users.title")} />,
|
||||||
|
cardContainer,
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImportFeature = FilePicker;
|
export const ImportFeature = FilePicker;
|
|
@ -1,8 +1,4 @@
|
||||||
import { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
|
||||||
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Typography } from "@mui/material";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormDataConsumer,
|
FormDataConsumer,
|
||||||
|
@ -17,6 +13,19 @@ import {
|
||||||
useLocales,
|
useLocales,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardActions,
|
||||||
|
CircularProgress,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
|
||||||
import { useAppContext } from "../AppContext";
|
import { useAppContext } from "../AppContext";
|
||||||
import {
|
import {
|
||||||
|
@ -94,7 +103,9 @@ const LoginPage = () => {
|
||||||
const [locale, setLocale] = useLocaleState();
|
const [locale, setLocale] = useLocaleState();
|
||||||
const locales = useLocales();
|
const locales = useLocales();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
|
const base_url = allowSingleBaseUrl
|
||||||
|
? restrictBaseUrl
|
||||||
|
: localStorage.getItem("base_url");
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||||
|
|
||||||
|
@ -102,7 +113,11 @@ const LoginPage = () => {
|
||||||
const ssoToken = loginToken[1];
|
const ssoToken = loginToken[1];
|
||||||
console.log("SSO token is", ssoToken);
|
console.log("SSO token is", ssoToken);
|
||||||
// Prevent further requests
|
// Prevent further requests
|
||||||
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
window.location.href.replace(loginToken[0], "#").split("#")[0]
|
||||||
|
);
|
||||||
const baseUrl = localStorage.getItem("sso_base_url");
|
const baseUrl = localStorage.getItem("sso_base_url");
|
||||||
localStorage.removeItem("sso_base_url");
|
localStorage.removeItem("sso_base_url");
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
|
@ -131,7 +146,9 @@ const LoginPage = () => {
|
||||||
const validateBaseUrl = value => {
|
const validateBaseUrl = value => {
|
||||||
if (!value.match(/^(http|https):\/\//)) {
|
if (!value.match(/^(http|https):\/\//)) {
|
||||||
return translate("synapseadmin.auth.protocol_error");
|
return translate("synapseadmin.auth.protocol_error");
|
||||||
} else if (!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)) {
|
} else if (
|
||||||
|
!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)
|
||||||
|
) {
|
||||||
return translate("synapseadmin.auth.url_error");
|
return translate("synapseadmin.auth.url_error");
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -166,13 +183,16 @@ const LoginPage = () => {
|
||||||
const [serverVersion, setServerVersion] = useState("");
|
const [serverVersion, setServerVersion] = useState("");
|
||||||
const [matrixVersions, setMatrixVersions] = useState("");
|
const [matrixVersions, setMatrixVersions] = useState("");
|
||||||
|
|
||||||
const handleUsernameChange = () => {
|
const handleUsernameChange = _ => {
|
||||||
if (formData.base_url || allowSingleBaseUrl) return;
|
if (formData.base_url || allowSingleBaseUrl) return;
|
||||||
// check if username is a full qualified userId then set base_url accordingly
|
// check if username is a full qualified userId then set base_url accordingly
|
||||||
const domain = splitMxid(formData.username)?.domain;
|
const domain = splitMxid(formData.username)?.domain;
|
||||||
if (domain) {
|
if (domain) {
|
||||||
getWellKnownUrl(domain).then(url => {
|
getWellKnownUrl(domain).then(url => {
|
||||||
if (allowAnyBaseUrl || (allowMultipleBaseUrls && restrictBaseUrl.includes(url)))
|
if (
|
||||||
|
allowAnyBaseUrl ||
|
||||||
|
(allowMultipleBaseUrls && restrictBaseUrl.includes(url))
|
||||||
|
)
|
||||||
form.setValue("base_url", url);
|
form.setValue("base_url", url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -185,20 +205,28 @@ const LoginPage = () => {
|
||||||
if (!isValidBaseUrl(formData.base_url)) return;
|
if (!isValidBaseUrl(formData.base_url)) return;
|
||||||
|
|
||||||
getServerVersion(formData.base_url)
|
getServerVersion(formData.base_url)
|
||||||
.then(serverVersion => setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`))
|
.then(serverVersion =>
|
||||||
|
setServerVersion(
|
||||||
|
`${translate("synapseadmin.auth.server_version")} ${serverVersion}`
|
||||||
|
)
|
||||||
|
)
|
||||||
.catch(() => setServerVersion(""));
|
.catch(() => setServerVersion(""));
|
||||||
|
|
||||||
getSupportedFeatures(formData.base_url)
|
getSupportedFeatures(formData.base_url)
|
||||||
.then(features =>
|
.then(features =>
|
||||||
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`)
|
setMatrixVersions(
|
||||||
|
`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.catch(() => setMatrixVersions(""));
|
.catch(() => setMatrixVersions(""));
|
||||||
|
|
||||||
// Set SSO Url
|
// Set SSO Url
|
||||||
getSupportedLoginFlows(formData.base_url)
|
getSupportedLoginFlows(formData.base_url)
|
||||||
.then(loginFlows => {
|
.then(loginFlows => {
|
||||||
const supportPass = loginFlows.find(f => f.type === "m.login.password") !== undefined;
|
const supportPass =
|
||||||
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
loginFlows.find(f => f.type === "m.login.password") !== undefined;
|
||||||
|
const supportSSO =
|
||||||
|
loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
||||||
setSupportPassAuth(supportPass);
|
setSupportPassAuth(supportPass);
|
||||||
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
||||||
})
|
})
|
||||||
|
@ -210,7 +238,7 @@ const LoginPage = () => {
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
source="username"
|
name="username"
|
||||||
label="ra.auth.username"
|
label="ra.auth.username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
|
@ -222,7 +250,7 @@ const LoginPage = () => {
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
source="password"
|
name="password"
|
||||||
label="ra.auth.password"
|
label="ra.auth.password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
|
@ -234,7 +262,7 @@ const LoginPage = () => {
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
source="base_url"
|
name="base_url"
|
||||||
label="synapseadmin.auth.base_url"
|
label="synapseadmin.auth.base_url"
|
||||||
select={allowMultipleBaseUrls}
|
select={allowMultipleBaseUrls}
|
||||||
autoComplete="url"
|
autoComplete="url"
|
||||||
|
@ -259,7 +287,11 @@ const LoginPage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
<Form
|
||||||
|
defaultValues={{ base_url: base_url }}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
mode="onTouched"
|
||||||
|
>
|
||||||
<FormBox>
|
<FormBox>
|
||||||
<Card className="card">
|
<Card className="card">
|
||||||
<Box className="avatar">
|
<Box className="avatar">
|
||||||
|
@ -286,7 +318,9 @@ const LoginPage = () => {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
|
<FormDataConsumer>
|
||||||
|
{formDataProps => <UserData {...formDataProps} />}
|
||||||
|
</FormDataConsumer>
|
||||||
<CardActions className="actions">
|
<CardActions className="actions">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
71
src/components/LoginPage.test.jsx
Normal file
71
src/components/LoginPage.test.jsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { AdminContext } from "react-admin";
|
||||||
|
import LoginPage from "./LoginPage";
|
||||||
|
import { AppContext } from "../AppContext";
|
||||||
|
|
||||||
|
describe("LoginForm", () => {
|
||||||
|
it("renders with no restriction to homeserver", () => {
|
||||||
|
render(
|
||||||
|
<AdminContext>
|
||||||
|
<LoginPage />
|
||||||
|
</AdminContext>
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByText("synapseadmin.auth.welcome");
|
||||||
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||||
|
screen.getByText("ra.auth.password");
|
||||||
|
const baseUrlInput = screen.getByRole("textbox", {
|
||||||
|
name: "synapseadmin.auth.base_url",
|
||||||
|
});
|
||||||
|
expect(baseUrlInput.className.split(" ")).not.toContain("Mui-readOnly");
|
||||||
|
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with single restricted homeserver", () => {
|
||||||
|
render(
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{ restrictBaseUrl: "https://matrix.example.com" }}
|
||||||
|
>
|
||||||
|
<AdminContext>
|
||||||
|
<LoginPage />
|
||||||
|
</AdminContext>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByText("synapseadmin.auth.welcome");
|
||||||
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||||
|
screen.getByText("ra.auth.password");
|
||||||
|
const baseUrlInput = screen.getByRole("textbox", {
|
||||||
|
name: "synapseadmin.auth.base_url",
|
||||||
|
});
|
||||||
|
expect(baseUrlInput.className.split(" ")).toContain("Mui-readOnly");
|
||||||
|
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with multiple restricted homeservers", async () => {
|
||||||
|
render(
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
restrictBaseUrl: [
|
||||||
|
"https://matrix.example.com",
|
||||||
|
"https://matrix.example.org",
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AdminContext>
|
||||||
|
<LoginPage />
|
||||||
|
</AdminContext>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByText("synapseadmin.auth.welcome");
|
||||||
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||||
|
screen.getByText("ra.auth.password");
|
||||||
|
screen.getByRole("combobox", { name: "synapseadmin.auth.base_url" });
|
||||||
|
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,73 +0,0 @@
|
||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
|
||||||
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { AdminContext } from "react-admin";
|
|
||||||
|
|
||||||
import LoginPage from "./LoginPage";
|
|
||||||
import { AppContext } from "../AppContext";
|
|
||||||
import englishMessages from "../i18n/en";
|
|
||||||
|
|
||||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
|
||||||
|
|
||||||
describe("LoginForm", () => {
|
|
||||||
it("renders with no restriction to homeserver", () => {
|
|
||||||
render(
|
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
|
||||||
<LoginPage />
|
|
||||||
</AdminContext>
|
|
||||||
);
|
|
||||||
|
|
||||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
|
||||||
screen.getByRole("combobox", { name: "" });
|
|
||||||
screen.getByRole("textbox", { name: englishMessages.ra.auth.username });
|
|
||||||
screen.getByText(englishMessages.ra.auth.password);
|
|
||||||
const baseUrlInput = screen.getByRole("textbox", {
|
|
||||||
name: englishMessages.synapseadmin.auth.base_url,
|
|
||||||
});
|
|
||||||
expect(baseUrlInput.className.split(" ")).not.toContain("Mui-readOnly");
|
|
||||||
screen.getByRole("button", { name: englishMessages.ra.auth.sign_in });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders with single restricted homeserver", () => {
|
|
||||||
render(
|
|
||||||
<AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com" }}>
|
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
|
||||||
<LoginPage />
|
|
||||||
</AdminContext>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
|
||||||
screen.getByRole("combobox", { name: "" });
|
|
||||||
screen.getByRole("textbox", { name: englishMessages.ra.auth.username });
|
|
||||||
screen.getByText(englishMessages.ra.auth.password);
|
|
||||||
const baseUrlInput = screen.getByRole("textbox", {
|
|
||||||
name: englishMessages.synapseadmin.auth.base_url,
|
|
||||||
});
|
|
||||||
expect(baseUrlInput.className.split(" ")).toContain("Mui-readOnly");
|
|
||||||
screen.getByRole("button", { name: englishMessages.ra.auth.sign_in });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders with multiple restricted homeservers", async () => {
|
|
||||||
render(
|
|
||||||
<AppContext.Provider
|
|
||||||
value={{
|
|
||||||
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
|
||||||
<LoginPage />
|
|
||||||
</AdminContext>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
|
||||||
screen.getByRole("combobox", { name: "" });
|
|
||||||
screen.getByRole("textbox", { name: englishMessages.ra.auth.username });
|
|
||||||
screen.getByText(englishMessages.ra.auth.password);
|
|
||||||
screen.getByRole("combobox", {
|
|
||||||
name: englishMessages.synapseadmin.auth.base_url,
|
|
||||||
});
|
|
||||||
screen.getByRole("button", { name: englishMessages.ra.auth.sign_in });
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,37 +1,62 @@
|
||||||
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
Create,
|
Create,
|
||||||
CreateProps,
|
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
DateTimeInput,
|
DateTimeInput,
|
||||||
Edit,
|
Edit,
|
||||||
EditProps,
|
|
||||||
List,
|
List,
|
||||||
ListProps,
|
|
||||||
maxValue,
|
maxValue,
|
||||||
number,
|
number,
|
||||||
NumberField,
|
NumberField,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
regex,
|
regex,
|
||||||
ResourceProps,
|
|
||||||
SaveButton,
|
SaveButton,
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
TextInput,
|
TextInput,
|
||||||
TextField,
|
TextField,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
|
||||||
|
|
||||||
import { DATE_FORMAT, dateFormatter, dateParser } from "./date";
|
const date_format = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
|
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
|
||||||
const validateUsesAllowed = [number()];
|
const validateUsesAllowed = [number()];
|
||||||
const validateLength = [number(), maxValue(64)];
|
const validateLength = [number(), maxValue(64)];
|
||||||
|
|
||||||
|
const dateParser = v => {
|
||||||
|
const d = new Date(v);
|
||||||
|
if (isNaN(d)) return 0;
|
||||||
|
return d.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateFormatter = v => {
|
||||||
|
if (v === undefined || v === null) return;
|
||||||
|
const d = new Date(v);
|
||||||
|
|
||||||
|
const pad = "00";
|
||||||
|
const year = d.getFullYear().toString();
|
||||||
|
const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
|
||||||
|
const day = (pad + d.getDate().toString()).slice(-2);
|
||||||
|
const hour = (pad + d.getHours().toString()).slice(-2);
|
||||||
|
const minute = (pad + d.getMinutes().toString()).slice(-2);
|
||||||
|
|
||||||
|
// target format yyyy-MM-ddThh:mm
|
||||||
|
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||||
|
};
|
||||||
|
|
||||||
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
|
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
|
||||||
|
|
||||||
export const RegistrationTokenList = (props: ListProps) => (
|
export const RegistrationTokenList = props => (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={registrationTokenFilters}
|
filters={registrationTokenFilters}
|
||||||
|
@ -44,12 +69,17 @@ export const RegistrationTokenList = (props: ListProps) => (
|
||||||
<NumberField source="uses_allowed" sortable={false} />
|
<NumberField source="uses_allowed" sortable={false} />
|
||||||
<NumberField source="pending" sortable={false} />
|
<NumberField source="pending" sortable={false} />
|
||||||
<NumberField source="completed" sortable={false} />
|
<NumberField source="completed" sortable={false} />
|
||||||
<DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} />
|
<DateField
|
||||||
|
source="expiry_time"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RegistrationTokenCreate = (props: CreateProps) => (
|
export const RegistrationTokenCreate = props => (
|
||||||
<Create {...props} redirect="list">
|
<Create {...props} redirect="list">
|
||||||
<SimpleForm
|
<SimpleForm
|
||||||
toolbar={
|
toolbar={
|
||||||
|
@ -59,32 +89,49 @@ export const RegistrationTokenCreate = (props: CreateProps) => (
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextInput source="token" autoComplete="off" validate={validateToken} resettable />
|
<TextInput
|
||||||
|
source="token"
|
||||||
|
autoComplete="off"
|
||||||
|
validate={validateToken}
|
||||||
|
resettable
|
||||||
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
source="length"
|
source="length"
|
||||||
validate={validateLength}
|
validate={validateLength}
|
||||||
helperText="resources.registration_tokens.helper.length"
|
helperText="resources.registration_tokens.helper.length"
|
||||||
step={1}
|
step={1}
|
||||||
/>
|
/>
|
||||||
<NumberInput source="uses_allowed" validate={validateUsesAllowed} step={1} />
|
<NumberInput
|
||||||
|
source="uses_allowed"
|
||||||
|
validate={validateUsesAllowed}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
<DateTimeInput source="expiry_time" parse={dateParser} />
|
<DateTimeInput source="expiry_time" parse={dateParser} />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RegistrationTokenEdit = (props: EditProps) => (
|
export const RegistrationTokenEdit = props => (
|
||||||
<Edit {...props}>
|
<Edit {...props}>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="token" disabled />
|
<TextInput source="token" disabled />
|
||||||
<NumberInput source="pending" disabled />
|
<NumberInput source="pending" disabled />
|
||||||
<NumberInput source="completed" disabled />
|
<NumberInput source="completed" disabled />
|
||||||
<NumberInput source="uses_allowed" validate={validateUsesAllowed} step={1} />
|
<NumberInput
|
||||||
<DateTimeInput source="expiry_time" parse={dateParser} format={dateFormatter} />
|
source="uses_allowed"
|
||||||
|
validate={validateUsesAllowed}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
<DateTimeInput
|
||||||
|
source="expiry_time"
|
||||||
|
parse={dateParser}
|
||||||
|
format={dateFormatter}
|
||||||
|
/>
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
|
||||||
const resource: ResourceProps = {
|
const resource = {
|
||||||
name: "registration_tokens",
|
name: "registration_tokens",
|
||||||
icon: RegistrationTokenIcon,
|
icon: RegistrationTokenIcon,
|
||||||
list: RegistrationTokenList,
|
list: RegistrationTokenList,
|
|
@ -1,18 +1,14 @@
|
||||||
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
BulkDeleteButtonProps,
|
|
||||||
Button,
|
Button,
|
||||||
ButtonProps,
|
|
||||||
DatagridConfigurable,
|
DatagridConfigurable,
|
||||||
DeleteButtonProps,
|
|
||||||
ExportButton,
|
ExportButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
List,
|
List,
|
||||||
NumberField,
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
ResourceProps,
|
|
||||||
SelectColumnsButton,
|
SelectColumnsButton,
|
||||||
TextField,
|
TextField,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
|
@ -26,12 +22,14 @@ import {
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
|
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
|
||||||
import AvatarField from "./AvatarField";
|
import AvatarField from "./AvatarField";
|
||||||
|
|
||||||
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
|
const RoomDirectoryPagination = () => (
|
||||||
|
<Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
|
||||||
|
);
|
||||||
|
|
||||||
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
|
export const RoomDirectoryUnpublishButton = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -52,7 +50,7 @@ export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomDirectoryBulkUnpublishButton = (props: BulkDeleteButtonProps) => (
|
export const RoomDirectoryBulkUnpublishButton = props => (
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
{...props}
|
{...props}
|
||||||
label="resources.room_directory.action.erase"
|
label="resources.room_directory.action.erase"
|
||||||
|
@ -64,7 +62,7 @@ export const RoomDirectoryBulkUnpublishButton = (props: BulkDeleteButtonProps) =
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
export const RoomDirectoryBulkPublishButton = props => {
|
||||||
const { selectedIds } = useListContext();
|
const { selectedIds } = useListContext();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
|
@ -90,13 +88,18 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
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={isLoading}
|
||||||
|
>
|
||||||
<RoomDirectoryIcon />
|
<RoomDirectoryIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
export const RoomDirectoryPublishButton = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
|
@ -120,7 +123,12 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button {...props} label="resources.room_directory.action.create" onClick={handleSend} disabled={isLoading}>
|
<Button
|
||||||
|
{...props}
|
||||||
|
label="resources.room_directory.action.create"
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
<RoomDirectoryIcon />
|
<RoomDirectoryIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -134,9 +142,13 @@ const RoomDirectoryListActions = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RoomDirectoryList = () => (
|
export const RoomDirectoryList = () => (
|
||||||
<List pagination={<RoomDirectoryPagination />} perPage={100} actions={<RoomDirectoryListActions />}>
|
<List
|
||||||
|
pagination={<RoomDirectoryPagination />}
|
||||||
|
perPage={100}
|
||||||
|
actions={<RoomDirectoryListActions />}
|
||||||
|
>
|
||||||
<DatagridConfigurable
|
<DatagridConfigurable
|
||||||
rowClick={id => "/rooms/" + id + "/show"}
|
rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
|
||||||
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
||||||
omit={["room_id", "canonical_alias", "topic"]}
|
omit={["room_id", "canonical_alias", "topic"]}
|
||||||
>
|
>
|
||||||
|
@ -146,18 +158,46 @@ export const RoomDirectoryList = () => (
|
||||||
sx={{ height: "40px", width: "40px" }}
|
sx={{ height: "40px", width: "40px" }}
|
||||||
label="resources.rooms.fields.avatar"
|
label="resources.rooms.fields.avatar"
|
||||||
/>
|
/>
|
||||||
<TextField source="name" sortable={false} label="resources.rooms.fields.name" />
|
<TextField
|
||||||
<TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" />
|
source="name"
|
||||||
<TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" />
|
sortable={false}
|
||||||
<TextField source="topic" sortable={false} label="resources.rooms.fields.topic" />
|
label="resources.rooms.fields.name"
|
||||||
<NumberField source="num_joined_members" sortable={false} label="resources.rooms.fields.joined_members" />
|
/>
|
||||||
<BooleanField source="world_readable" sortable={false} label="resources.room_directory.fields.world_readable" />
|
<TextField
|
||||||
<BooleanField source="guest_can_join" sortable={false} label="resources.room_directory.fields.guest_can_join" />
|
source="room_id"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.room_id"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
source="canonical_alias"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.canonical_alias"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
source="topic"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.topic"
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
source="num_joined_members"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.joined_members"
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
source="world_readable"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.room_directory.fields.world_readable"
|
||||||
|
/>
|
||||||
|
<BooleanField
|
||||||
|
source="guest_can_join"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.room_directory.fields.guest_can_join"
|
||||||
|
/>
|
||||||
</DatagridConfigurable>
|
</DatagridConfigurable>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
const resource: ResourceProps = {
|
const resource = {
|
||||||
name: "room_directory",
|
name: "room_directory",
|
||||||
icon: RoomDirectoryIcon,
|
icon: RoomDirectoryIcon,
|
||||||
list: RoomDirectoryList,
|
list: RoomDirectoryList,
|
|
@ -1,16 +1,10 @@
|
||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import IconCancel from "@mui/icons-material/Cancel";
|
|
||||||
import MessageIcon from "@mui/icons-material/Message";
|
|
||||||
import { Dialog, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
RaRecord,
|
|
||||||
SaveButton,
|
SaveButton,
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
TextInput,
|
TextInput,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
ToolbarProps,
|
|
||||||
required,
|
required,
|
||||||
useCreate,
|
useCreate,
|
||||||
useDataProvider,
|
useDataProvider,
|
||||||
|
@ -21,13 +15,24 @@ import {
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
|
import MessageIcon from "@mui/icons-material/Message";
|
||||||
|
import IconCancel from "@mui/icons-material/Cancel";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
const ServerNoticeToolbar = (props: ToolbarProps & { pristine?: boolean }) => (
|
const ServerNoticeToolbar = props => (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<SaveButton label="resources.servernotices.action.send" disabled={props.pristine} />
|
<SaveButton
|
||||||
|
label="resources.servernotices.action.send"
|
||||||
|
disabled={props.pristine}
|
||||||
|
/>
|
||||||
<Button label="ra.action.cancel" onClick={onClose}>
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
<IconCancel />
|
<IconCancel />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -35,10 +40,14 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose}>
|
<Dialog open={open} onClose={onClose} loading={loading}>
|
||||||
<DialogTitle>{translate("resources.servernotices.action.send")}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{translate("resources.servernotices.action.send")}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>{translate("resources.servernotices.helper.send")}</DialogContentText>
|
<DialogContentText>
|
||||||
|
{translate("resources.servernotices.helper.send")}
|
||||||
|
</DialogContentText>
|
||||||
<SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
|
<SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
|
||||||
<TextInput
|
<TextInput
|
||||||
source="body"
|
source="body"
|
||||||
|
@ -59,12 +68,12 @@ export const ServerNoticeButton = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [create, { isLoading }] = useCreate();
|
const [create, { isloading }] = useCreate();
|
||||||
|
|
||||||
const handleDialogOpen = () => setOpen(true);
|
const handleDialogOpen = () => setOpen(true);
|
||||||
const handleDialogClose = () => setOpen(false);
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
const handleSend = (values: Partial<RaRecord>) => {
|
const handleSend = values => {
|
||||||
create(
|
create(
|
||||||
"servernotices",
|
"servernotices",
|
||||||
{ data: { id: record.id, ...values } },
|
{ data: { id: record.id, ...values } },
|
||||||
|
@ -83,10 +92,18 @@ export const ServerNoticeButton = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button label="resources.servernotices.send" onClick={handleDialogOpen} disabled={isLoading}>
|
<Button
|
||||||
|
label="resources.servernotices.send"
|
||||||
|
onClick={handleDialogOpen}
|
||||||
|
disabled={isloading}
|
||||||
|
>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog open={open} onClose={handleDialogClose} onSubmit={handleSend} />
|
<ServerNoticeDialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleDialogClose}
|
||||||
|
onSubmit={handleSend}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -121,10 +138,18 @@ export const ServerNoticeBulkButton = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
|
<Button
|
||||||
|
label="resources.servernotices.send"
|
||||||
|
onClick={openDialog}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
<ServerNoticeDialog
|
||||||
|
open={open}
|
||||||
|
onClose={closeDialog}
|
||||||
|
onSubmit={sendNotices}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,28 +0,0 @@
|
||||||
export const DATE_FORMAT: Intl.DateTimeFormatOptions = {
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dateParser = (v: string | number | Date): number => {
|
|
||||||
const d = new Date(v);
|
|
||||||
return d.getTime();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dateFormatter = (v: string | number | Date | undefined | null): string => {
|
|
||||||
if (v === undefined || v === null) return "";
|
|
||||||
const d = new Date(v);
|
|
||||||
|
|
||||||
const pad = "00";
|
|
||||||
const year = d.getFullYear().toString();
|
|
||||||
const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
|
|
||||||
const day = (pad + d.getDate().toString()).slice(-2);
|
|
||||||
const hour = (pad + d.getHours().toString()).slice(-2);
|
|
||||||
const minute = (pad + d.getMinutes().toString()).slice(-2);
|
|
||||||
|
|
||||||
// target format yyyy-MM-ddThh:mm
|
|
||||||
return `${year}-${month}-${day}T${hour}:${minute}`;
|
|
||||||
};
|
|
|
@ -1,23 +1,14 @@
|
||||||
import { MouseEvent } from "react";
|
import React from "react";
|
||||||
|
|
||||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
|
||||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
|
||||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
List,
|
List,
|
||||||
ListProps,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
RaRecord,
|
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
ReferenceManyField,
|
ReferenceManyField,
|
||||||
ResourceProps,
|
|
||||||
SearchInput,
|
SearchInput,
|
||||||
Show,
|
Show,
|
||||||
ShowProps,
|
|
||||||
Tab,
|
Tab,
|
||||||
TabbedShowLayout,
|
TabbedShowLayout,
|
||||||
TextField,
|
TextField,
|
||||||
|
@ -28,12 +19,25 @@ import {
|
||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||||
|
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
||||||
|
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||||
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
|
||||||
import { DATE_FORMAT } from "./date";
|
const DestinationPagination = () => (
|
||||||
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
|
);
|
||||||
|
|
||||||
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const date_format = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
const destinationRowSx = (record: RaRecord) => ({
|
const destinationRowSx = (record, _index) => ({
|
||||||
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -48,7 +52,7 @@ export const DestinationReconnectButton = () => {
|
||||||
// Reconnect is not required if no error has occurred. (`failure_ts`)
|
// Reconnect is not required if no error has occurred. (`failure_ts`)
|
||||||
if (!record || !record.failure_ts) return null;
|
if (!record || !record.failure_ts) return null;
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleClick = e => {
|
||||||
// Prevents redirection to the detail page when clicking in the list
|
// Prevents redirection to the detail page when clicking in the list
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -70,7 +74,11 @@ export const DestinationReconnectButton = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button label="resources.destinations.action.reconnect" onClick={handleClick} disabled={isLoading}>
|
<Button
|
||||||
|
label="resources.destinations.action.reconnect"
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
<AutorenewIcon />
|
<AutorenewIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -92,7 +100,7 @@ const DestinationTitle = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DestinationList = (props: ListProps) => {
|
export const DestinationList = props => {
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -100,10 +108,14 @@ export const DestinationList = (props: ListProps) => {
|
||||||
pagination={<DestinationPagination />}
|
pagination={<DestinationPagination />}
|
||||||
sort={{ field: "destination", order: "ASC" }}
|
sort={{ field: "destination", order: "ASC" }}
|
||||||
>
|
>
|
||||||
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
<Datagrid
|
||||||
|
rowSx={destinationRowSx}
|
||||||
|
rowClick={(id, _resource, _record) => `${id}/show/rooms`}
|
||||||
|
bulkActionButtons={false}
|
||||||
|
>
|
||||||
<TextField source="destination" />
|
<TextField source="destination" />
|
||||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
<DateField source="failure_ts" showTime options={date_format} />
|
||||||
<DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
<DateField 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 />
|
||||||
|
@ -112,29 +124,43 @@ export const DestinationList = (props: ListProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DestinationShow = (props: ShowProps) => {
|
export const DestinationShow = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Show actions={<DestinationShowActions />} title={<DestinationTitle />} {...props}>
|
<Show
|
||||||
|
actions={<DestinationShowActions />}
|
||||||
|
title={<DestinationTitle />}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<TabbedShowLayout>
|
<TabbedShowLayout>
|
||||||
<Tab label="status" icon={<ViewListIcon />}>
|
<Tab label="status" icon={<ViewListIcon />}>
|
||||||
<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} />
|
<DateField 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" />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<FolderSharedIcon />} path="rooms">
|
<Tab
|
||||||
|
label={translate("resources.rooms.name", { smart_count: 2 })}
|
||||||
|
icon={<FolderSharedIcon />}
|
||||||
|
path="rooms"
|
||||||
|
>
|
||||||
<ReferenceManyField
|
<ReferenceManyField
|
||||||
reference="destination_rooms"
|
reference="destination_rooms"
|
||||||
target="destination"
|
target="destination"
|
||||||
label={false}
|
addLabel={false}
|
||||||
pagination={<DestinationPagination />}
|
pagination={<DestinationPagination />}
|
||||||
perPage={50}
|
perPage={50}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }} rowClick={id => `/rooms/${id}/show`}>
|
<Datagrid
|
||||||
<TextField source="room_id" label="resources.rooms.fields.room_id" />
|
style={{ width: "100%" }}
|
||||||
|
rowClick={(id, resource, record) => `/rooms/${id}/show`}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
source="room_id"
|
||||||
|
label="resources.rooms.fields.room_id"
|
||||||
|
/>
|
||||||
<TextField source="stream_ordering" sortable={false} />
|
<TextField source="stream_ordering" sortable={false} />
|
||||||
<ReferenceField
|
<ReferenceField
|
||||||
label="resources.rooms.fields.name"
|
label="resources.rooms.fields.name"
|
||||||
|
@ -153,7 +179,7 @@ export const DestinationShow = (props: ShowProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource: ResourceProps = {
|
const resource = {
|
||||||
name: "destinations",
|
name: "destinations",
|
||||||
icon: DestinationsIcon,
|
icon: DestinationsIcon,
|
||||||
list: DestinationList,
|
list: DestinationList,
|
51
src/components/devices.jsx
Normal file
51
src/components/devices.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
DeleteButton,
|
||||||
|
useDelete,
|
||||||
|
useNotify,
|
||||||
|
useRecordContext,
|
||||||
|
useRefresh,
|
||||||
|
} from "react-admin";
|
||||||
|
|
||||||
|
export const DeviceRemoveButton = props => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
const refresh = useRefresh();
|
||||||
|
const notify = useNotify();
|
||||||
|
|
||||||
|
const [removeDevice] = useDelete();
|
||||||
|
|
||||||
|
if (!record) return null;
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
removeDevice(
|
||||||
|
"devices",
|
||||||
|
// needs previousData for user_id
|
||||||
|
{ id: record.id, previousData: record },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notify("resources.devices.action.erase.success");
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
notify("resources.devices.action.erase.failure", { type: "error" });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeleteButton
|
||||||
|
{...props}
|
||||||
|
label="ra.action.remove"
|
||||||
|
confirmTitle="resources.devices.action.erase.title"
|
||||||
|
confirmContent="resources.devices.action.erase.content"
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
mutationMode="pessimistic"
|
||||||
|
redirect={false}
|
||||||
|
translateOptions={{
|
||||||
|
id: record.id,
|
||||||
|
name: record.display_name ? record.display_name : record.id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,21 +0,0 @@
|
||||||
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
|
||||||
|
|
||||||
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
|
||||||
const record = useRecordContext();
|
|
||||||
if (!record) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DeleteWithConfirmButton
|
|
||||||
{...props}
|
|
||||||
label="ra.action.remove"
|
|
||||||
confirmTitle="resources.devices.action.erase.title"
|
|
||||||
confirmContent="resources.devices.action.erase.content"
|
|
||||||
mutationMode="pessimistic"
|
|
||||||
redirect={false}
|
|
||||||
translateOptions={{
|
|
||||||
id: record.id,
|
|
||||||
name: record.display_name ? record.display_name : record.id,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,25 +1,13 @@
|
||||||
import { get } from "lodash";
|
import React, { useState } from "react";
|
||||||
import { useState } from "react";
|
import get from "lodash/get";
|
||||||
|
|
||||||
import BlockIcon from "@mui/icons-material/Block";
|
|
||||||
import IconCancel from "@mui/icons-material/Cancel";
|
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
|
||||||
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
|
||||||
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
|
||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
|
||||||
import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material";
|
|
||||||
import { alpha, useTheme } from "@mui/material/styles";
|
|
||||||
import {
|
import {
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
Button,
|
Button,
|
||||||
ButtonProps,
|
|
||||||
DateTimeInput,
|
DateTimeInput,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
ToolbarProps,
|
|
||||||
useCreate,
|
useCreate,
|
||||||
useDelete,
|
useDelete,
|
||||||
useNotify,
|
useNotify,
|
||||||
|
@ -28,16 +16,39 @@ import {
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import BlockIcon from "@mui/icons-material/Block";
|
||||||
import { dateParser } from "./date";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
|
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
|
import IconCancel from "@mui/icons-material/Cancel";
|
||||||
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
|
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
||||||
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
import { getMediaUrl } from "../synapse/synapse";
|
import { getMediaUrl } from "../synapse/synapse";
|
||||||
|
|
||||||
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
const DeleteMediaToolbar = (props: ToolbarProps) => (
|
const dateParser = v => {
|
||||||
|
const d = new Date(v);
|
||||||
|
if (isNaN(d)) return 0;
|
||||||
|
return d.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteMediaToolbar = props => (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<SaveButton label="resources.delete_media.action.send" icon={<DeleteSweepIcon />} />
|
<SaveButton
|
||||||
|
label="resources.delete_media.action.send"
|
||||||
|
icon={<DeleteSweepIcon />}
|
||||||
|
/>
|
||||||
<Button label="ra.action.cancel" onClick={onClose}>
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
<IconCancel />
|
<IconCancel />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -45,10 +56,14 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose}>
|
<Dialog open={open} onClose={onClose} loading={loading}>
|
||||||
<DialogTitle>{translate("resources.delete_media.action.send")}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{translate("resources.delete_media.action.send")}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>{translate("resources.delete_media.helper.send")}</DialogContentText>
|
<DialogContentText>
|
||||||
|
{translate("resources.delete_media.helper.send")}
|
||||||
|
</DialogContentText>
|
||||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -77,7 +92,7 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteMediaButton = (props: ButtonProps) => {
|
export const DeleteMediaButton = props => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
@ -86,7 +101,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||||
const openDialog = () => setOpen(true);
|
const openDialog = () => setOpen(true);
|
||||||
const closeDialog = () => setOpen(false);
|
const closeDialog = () => setOpen(false);
|
||||||
|
|
||||||
const deleteMedia = (values: { before_ts: string; size_gt: number; keep_profiles: boolean }) => {
|
const deleteMedia = values => {
|
||||||
deleteOne(
|
deleteOne(
|
||||||
"delete_media",
|
"delete_media",
|
||||||
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
|
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
|
||||||
|
@ -124,12 +139,16 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||||
>
|
>
|
||||||
<DeleteSweepIcon />
|
<DeleteSweepIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<DeleteMediaDialog open={open} onClose={closeDialog} onSubmit={deleteMedia} />
|
<DeleteMediaDialog
|
||||||
|
open={open}
|
||||||
|
onClose={closeDialog}
|
||||||
|
onSubmit={deleteMedia}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProtectMediaButton = (props: ButtonProps) => {
|
export const ProtectMediaButton = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
|
@ -190,7 +209,7 @@ export const ProtectMediaButton = (props: ButtonProps) => {
|
||||||
Button instead BooleanField for
|
Button instead BooleanField for
|
||||||
consistent appearance and position in the column
|
consistent appearance and position in the column
|
||||||
*/}
|
*/}
|
||||||
<Button {...props} disabled={true}>
|
<Button disabled={true}>
|
||||||
<ClearIcon />
|
<ClearIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -204,7 +223,7 @@ export const ProtectMediaButton = (props: ButtonProps) => {
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button {...props} onClick={handleUnprotect} disabled={isLoading}>
|
<Button onClick={handleUnprotect} disabled={isLoading}>
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -217,7 +236,7 @@ export const ProtectMediaButton = (props: ButtonProps) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button {...props} onClick={handleProtect} disabled={isLoading}>
|
<Button onClick={handleProtect} disabled={isLoading}>
|
||||||
<LockOpenIcon />
|
<LockOpenIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -227,7 +246,7 @@ export const ProtectMediaButton = (props: ButtonProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QuarantineMediaButton = (props: ButtonProps) => {
|
export const QuarantineMediaButton = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
|
@ -293,7 +312,11 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button {...props} onClick={handleRemoveQuarantaine} disabled={isLoading}>
|
<Button
|
||||||
|
{...props}
|
||||||
|
onClick={handleRemoveQuarantaine}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
<BlockIcon color="error" />
|
<BlockIcon color="error" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -306,7 +329,7 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Button {...props} onClick={handleQuarantaine} disabled={isLoading}>
|
<Button onClick={handleQuarantaine} disabled={isLoading}>
|
||||||
<BlockIcon />
|
<BlockIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
|
@ -1,14 +1,4 @@
|
||||||
import EventIcon from "@mui/icons-material/Event";
|
import React from "react";
|
||||||
import FastForwardIcon from "@mui/icons-material/FastForward";
|
|
||||||
import UserIcon from "@mui/icons-material/Group";
|
|
||||||
import HttpsIcon from "@mui/icons-material/Https";
|
|
||||||
import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
|
|
||||||
import PageviewIcon from "@mui/icons-material/Pageview";
|
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
|
||||||
import RoomIcon from "@mui/icons-material/ViewList";
|
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
|
@ -19,17 +9,14 @@ import {
|
||||||
ExportButton,
|
ExportButton,
|
||||||
FunctionField,
|
FunctionField,
|
||||||
List,
|
List,
|
||||||
ListProps,
|
|
||||||
NumberField,
|
NumberField,
|
||||||
Pagination,
|
Pagination,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
ReferenceManyField,
|
ReferenceManyField,
|
||||||
ResourceProps,
|
|
||||||
SearchInput,
|
SearchInput,
|
||||||
SelectColumnsButton,
|
SelectColumnsButton,
|
||||||
SelectField,
|
SelectField,
|
||||||
Show,
|
Show,
|
||||||
ShowProps,
|
|
||||||
Tab,
|
Tab,
|
||||||
TabbedShowLayout,
|
TabbedShowLayout,
|
||||||
TextField,
|
TextField,
|
||||||
|
@ -37,21 +24,41 @@ import {
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import FastForwardIcon from "@mui/icons-material/FastForward";
|
||||||
|
import HttpsIcon from "@mui/icons-material/Https";
|
||||||
|
import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
|
||||||
|
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||||
|
import UserIcon from "@mui/icons-material/Group";
|
||||||
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import EventIcon from "@mui/icons-material/Event";
|
||||||
|
import RoomIcon from "@mui/icons-material/ViewList";
|
||||||
import {
|
import {
|
||||||
RoomDirectoryBulkUnpublishButton,
|
RoomDirectoryBulkUnpublishButton,
|
||||||
RoomDirectoryBulkPublishButton,
|
RoomDirectoryBulkPublishButton,
|
||||||
RoomDirectoryUnpublishButton,
|
RoomDirectoryUnpublishButton,
|
||||||
RoomDirectoryPublishButton,
|
RoomDirectoryPublishButton,
|
||||||
} from "./RoomDirectory";
|
} from "./RoomDirectory";
|
||||||
import { DATE_FORMAT } from "./date";
|
|
||||||
|
|
||||||
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const date_format = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RoomPagination = () => (
|
||||||
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
|
);
|
||||||
|
|
||||||
const RoomTitle = () => {
|
const RoomTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
let name = "";
|
var name = "";
|
||||||
if (record) {
|
if (record) {
|
||||||
name = record.name !== "" ? record.name : record.id;
|
name = record.name !== "" ? record.name : record.id;
|
||||||
}
|
}
|
||||||
|
@ -65,11 +72,15 @@ const RoomTitle = () => {
|
||||||
|
|
||||||
const RoomShowActions = () => {
|
const RoomShowActions = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const publishButton = record.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
|
var roomDirectoryStatus = "";
|
||||||
// FIXME: refresh after (un)publish
|
if (record) {
|
||||||
|
roomDirectoryStatus = record.public;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{publishButton}
|
{roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
|
||||||
|
{roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
mutationMode="pessimistic"
|
mutationMode="pessimistic"
|
||||||
confirmTitle="resources.rooms.action.erase.title"
|
confirmTitle="resources.rooms.action.erase.title"
|
||||||
|
@ -79,7 +90,7 @@ const RoomShowActions = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoomShow = (props: ShowProps) => {
|
export const RoomShow = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||||
|
@ -94,19 +105,42 @@ export const RoomShow = (props: ShowProps) => {
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label="synapseadmin.rooms.tabs.detail" icon={<PageviewIcon />} path="detail">
|
<Tab
|
||||||
|
label="synapseadmin.rooms.tabs.detail"
|
||||||
|
icon={<PageviewIcon />}
|
||||||
|
path="detail"
|
||||||
|
>
|
||||||
<TextField source="joined_members" />
|
<TextField source="joined_members" />
|
||||||
<TextField source="joined_local_members" />
|
<TextField source="joined_local_members" />
|
||||||
<TextField source="joined_local_devices" />
|
<TextField source="joined_local_devices" />
|
||||||
<TextField source="state_events" />
|
<TextField source="state_events" />
|
||||||
<TextField source="version" />
|
<TextField source="version" />
|
||||||
<TextField source="encryption" emptyText={translate("resources.rooms.enums.unencrypted")} />
|
<TextField
|
||||||
|
source="encryption"
|
||||||
|
emptyText={translate("resources.rooms.enums.unencrypted")}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />} path="members">
|
<Tab
|
||||||
<ReferenceManyField reference="room_members" target="room_id" label={false}>
|
label="synapseadmin.rooms.tabs.members"
|
||||||
<Datagrid style={{ width: "100%" }} rowClick={id => "/users/" + id} bulkActionButtons={false}>
|
icon={<UserIcon />}
|
||||||
<TextField source="id" sortable={false} label="resources.users.fields.id" />
|
path="members"
|
||||||
|
>
|
||||||
|
<ReferenceManyField
|
||||||
|
reference="room_members"
|
||||||
|
target="room_id"
|
||||||
|
addLabel={false}
|
||||||
|
>
|
||||||
|
<Datagrid
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
rowClick={(id, resource, record) => "/users/" + id}
|
||||||
|
bulkActionButtons={false}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
source="id"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.users.fields.id"
|
||||||
|
/>
|
||||||
<ReferenceField
|
<ReferenceField
|
||||||
label="resources.users.fields.displayname"
|
label="resources.users.fields.displayname"
|
||||||
source="id"
|
source="id"
|
||||||
|
@ -120,7 +154,11 @@ export const RoomShow = (props: ShowProps) => {
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label="synapseadmin.rooms.tabs.permission" icon={<VisibilityIcon />} path="permission">
|
<Tab
|
||||||
|
label="synapseadmin.rooms.tabs.permission"
|
||||||
|
icon={<VisibilityIcon />}
|
||||||
|
path="permission"
|
||||||
|
>
|
||||||
<BooleanField source="federatable" />
|
<BooleanField source="federatable" />
|
||||||
<BooleanField source="public" />
|
<BooleanField source="public" />
|
||||||
<SelectField
|
<SelectField
|
||||||
|
@ -171,20 +209,41 @@ export const RoomShow = (props: ShowProps) => {
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label={translate("resources.room_state.name", { smart_count: 2 })} icon={<EventIcon />} path="state">
|
<Tab
|
||||||
<ReferenceManyField reference="room_state" target="room_id" label={false}>
|
label={translate("resources.room_state.name", { smart_count: 2 })}
|
||||||
|
icon={<EventIcon />}
|
||||||
|
path="state"
|
||||||
|
>
|
||||||
|
<ReferenceManyField
|
||||||
|
reference="room_state"
|
||||||
|
target="room_id"
|
||||||
|
addLabel={false}
|
||||||
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="type" sortable={false} />
|
<TextField source="type" sortable={false} />
|
||||||
<DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} />
|
<DateField
|
||||||
|
source="origin_server_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
<TextField source="content" sortable={false} />
|
<TextField source="content" sortable={false} />
|
||||||
<ReferenceField source="sender" reference="users" sortable={false}>
|
<ReferenceField
|
||||||
|
source="sender"
|
||||||
|
reference="users"
|
||||||
|
sortable={false}
|
||||||
|
>
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab label="resources.forward_extremities.name" icon={<FastForwardIcon />} path="forward_extremities">
|
<Tab
|
||||||
|
label="resources.forward_extremities.name"
|
||||||
|
icon={<FastForwardIcon />}
|
||||||
|
path="forward_extremities"
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
@ -193,10 +252,19 @@ export const RoomShow = (props: ShowProps) => {
|
||||||
>
|
>
|
||||||
{translate("resources.rooms.helper.forward_extremities")}
|
{translate("resources.rooms.helper.forward_extremities")}
|
||||||
</Box>
|
</Box>
|
||||||
<ReferenceManyField reference="forward_extremities" target="room_id" label={false}>
|
<ReferenceManyField
|
||||||
|
reference="forward_extremities"
|
||||||
|
target="room_id"
|
||||||
|
addLabel={false}
|
||||||
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="id" sortable={false} />
|
<TextField source="id" sortable={false} />
|
||||||
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={false} />
|
<DateField
|
||||||
|
source="received_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
<NumberField source="depth" sortable={false} />
|
<NumberField source="depth" sortable={false} />
|
||||||
<TextField source="state_group" sortable={false} />
|
<TextField source="state_group" sortable={false} />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
|
@ -228,7 +296,7 @@ const RoomListActions = () => (
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RoomList = (props: ListProps) => {
|
export const RoomList = props => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -242,7 +310,12 @@ export const RoomList = (props: ListProps) => {
|
||||||
<DatagridConfigurable
|
<DatagridConfigurable
|
||||||
rowClick="show"
|
rowClick="show"
|
||||||
bulkActionButtons={<RoomBulkActionButtons />}
|
bulkActionButtons={<RoomBulkActionButtons />}
|
||||||
omit={["joined_local_members", "state_events", "version", "federatable"]}
|
omit={[
|
||||||
|
"joined_local_members",
|
||||||
|
"state_events",
|
||||||
|
"version",
|
||||||
|
"federatable",
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<BooleanField
|
<BooleanField
|
||||||
source="is_encrypted"
|
source="is_encrypted"
|
||||||
|
@ -255,7 +328,12 @@ export const RoomList = (props: ListProps) => {
|
||||||
[`& [data-testid="false"]`]: { color: theme.palette.error.main },
|
[`& [data-testid="false"]`]: { color: theme.palette.error.main },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FunctionField source="name" render={record => record["name"] || record["canonical_alias"] || record["id"]} />
|
<FunctionField
|
||||||
|
source="name"
|
||||||
|
render={record =>
|
||||||
|
record["name"] || record["canonical_alias"] || record["id"]
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TextField source="joined_members" />
|
<TextField source="joined_members" />
|
||||||
<TextField source="joined_local_members" />
|
<TextField source="joined_local_members" />
|
||||||
<TextField source="state_events" />
|
<TextField source="state_events" />
|
||||||
|
@ -267,7 +345,7 @@ export const RoomList = (props: ListProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource: ResourceProps = {
|
const resource = {
|
||||||
name: "rooms",
|
name: "rooms",
|
||||||
icon: RoomIcon,
|
icon: RoomIcon,
|
||||||
list: RoomList,
|
list: RoomList,
|
79
src/components/statistics.jsx
Normal file
79
src/components/statistics.jsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React from "react";
|
||||||
|
import { cloneElement } from "react";
|
||||||
|
import {
|
||||||
|
Datagrid,
|
||||||
|
ExportButton,
|
||||||
|
List,
|
||||||
|
NumberField,
|
||||||
|
Pagination,
|
||||||
|
sanitizeListRestProps,
|
||||||
|
SearchInput,
|
||||||
|
TextField,
|
||||||
|
TopToolbar,
|
||||||
|
useListContext,
|
||||||
|
} from "react-admin";
|
||||||
|
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
||||||
|
import { DeleteMediaButton } from "./media";
|
||||||
|
|
||||||
|
const ListActions = props => {
|
||||||
|
const { className, exporter, filters, maxResults, ...rest } = props;
|
||||||
|
const { sort, resource, displayedFilters, filterValues, showFilter, total } =
|
||||||
|
useListContext();
|
||||||
|
return (
|
||||||
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
{filters &&
|
||||||
|
cloneElement(filters, {
|
||||||
|
resource,
|
||||||
|
showFilter,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
context: "button",
|
||||||
|
})}
|
||||||
|
<DeleteMediaButton />
|
||||||
|
<ExportButton
|
||||||
|
disabled={total === 0}
|
||||||
|
resource={resource}
|
||||||
|
sort={sort}
|
||||||
|
filterValues={filterValues}
|
||||||
|
maxResults={maxResults}
|
||||||
|
/>
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserMediaStatsPagination = () => (
|
||||||
|
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
|
||||||
|
|
||||||
|
export const UserMediaStatsList = props => (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
actions={<ListActions />}
|
||||||
|
filters={userMediaStatsFilters}
|
||||||
|
pagination={<UserMediaStatsPagination />}
|
||||||
|
sort={{ field: "media_length", order: "DESC" }}
|
||||||
|
>
|
||||||
|
<Datagrid
|
||||||
|
rowClick={(id, resource, record) => "/users/" + id + "/media"}
|
||||||
|
bulkActionButtons={false}
|
||||||
|
>
|
||||||
|
<TextField source="user_id" label="resources.users.fields.id" />
|
||||||
|
<TextField
|
||||||
|
source="displayname"
|
||||||
|
label="resources.users.fields.displayname"
|
||||||
|
/>
|
||||||
|
<NumberField source="media_count" />
|
||||||
|
<NumberField source="media_length" />
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resource = {
|
||||||
|
name: "user_media_statistics",
|
||||||
|
icon: EqualizerIcon,
|
||||||
|
list: UserMediaStatsList,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resource;
|
|
@ -1,55 +0,0 @@
|
||||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
|
||||||
import {
|
|
||||||
Datagrid,
|
|
||||||
ExportButton,
|
|
||||||
List,
|
|
||||||
ListProps,
|
|
||||||
NumberField,
|
|
||||||
Pagination,
|
|
||||||
ResourceProps,
|
|
||||||
SearchInput,
|
|
||||||
TextField,
|
|
||||||
TopToolbar,
|
|
||||||
useListContext,
|
|
||||||
} from "react-admin";
|
|
||||||
|
|
||||||
import { DeleteMediaButton } from "./media";
|
|
||||||
|
|
||||||
const ListActions = () => {
|
|
||||||
const { isLoading, total } = useListContext();
|
|
||||||
return (
|
|
||||||
<TopToolbar>
|
|
||||||
<DeleteMediaButton />
|
|
||||||
<ExportButton disabled={isLoading || total === 0} />
|
|
||||||
</TopToolbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserMediaStatsPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
|
||||||
|
|
||||||
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
|
|
||||||
|
|
||||||
export const UserMediaStatsList = (props: ListProps) => (
|
|
||||||
<List
|
|
||||||
{...props}
|
|
||||||
actions={<ListActions />}
|
|
||||||
filters={userMediaStatsFilters}
|
|
||||||
pagination={<UserMediaStatsPagination />}
|
|
||||||
sort={{ field: "media_length", order: "DESC" }}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
|
|
||||||
<TextField source="user_id" label="resources.users.fields.id" />
|
|
||||||
<TextField source="displayname" label="resources.users.fields.displayname" />
|
|
||||||
<NumberField source="media_count" />
|
|
||||||
<NumberField source="media_length" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
const resource: ResourceProps = {
|
|
||||||
name: "user_media_statistics",
|
|
||||||
icon: EqualizerIcon,
|
|
||||||
list: UserMediaStatsList,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default resource;
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import React, { cloneElement } from "react";
|
||||||
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||||
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
||||||
import DevicesIcon from "@mui/icons-material/Devices";
|
import DevicesIcon from "@mui/icons-material/Devices";
|
||||||
import GetAppIcon from "@mui/icons-material/GetApp";
|
import GetAppIcon from "@mui/icons-material/GetApp";
|
||||||
import UserIcon from "@mui/icons-material/Group";
|
|
||||||
import NotificationsIcon from "@mui/icons-material/Notifications";
|
import NotificationsIcon from "@mui/icons-material/Notifications";
|
||||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||||
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
||||||
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
||||||
|
import UserIcon from "@mui/icons-material/Group";
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
import {
|
import {
|
||||||
ArrayInput,
|
ArrayInput,
|
||||||
|
@ -15,11 +16,9 @@ import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
Create,
|
Create,
|
||||||
CreateProps,
|
|
||||||
Edit,
|
Edit,
|
||||||
EditProps,
|
|
||||||
List,
|
List,
|
||||||
ListProps,
|
Toolbar,
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
SimpleFormIterator,
|
SimpleFormIterator,
|
||||||
TabbedForm,
|
TabbedForm,
|
||||||
|
@ -31,11 +30,11 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
ReferenceManyField,
|
ReferenceManyField,
|
||||||
ResourceProps,
|
|
||||||
SearchInput,
|
SearchInput,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
|
SaveButton,
|
||||||
maxLength,
|
maxLength,
|
||||||
regex,
|
regex,
|
||||||
required,
|
required,
|
||||||
|
@ -45,16 +44,18 @@ import {
|
||||||
CreateButton,
|
CreateButton,
|
||||||
ExportButton,
|
ExportButton,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
|
sanitizeListRestProps,
|
||||||
NumberField,
|
NumberField,
|
||||||
useListContext,
|
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import AvatarField from "./AvatarField";
|
import AvatarField from "./AvatarField";
|
||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
||||||
import { DATE_FORMAT } from "./date";
|
|
||||||
import { DeviceRemoveButton } from "./devices";
|
import { DeviceRemoveButton } from "./devices";
|
||||||
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "./media";
|
import {
|
||||||
|
MediaIDField,
|
||||||
|
ProtectMediaButton,
|
||||||
|
QuarantineMediaButton,
|
||||||
|
} from "./media";
|
||||||
|
|
||||||
const choices_medium = [
|
const choices_medium = [
|
||||||
{ id: "email", name: "resources.users.email" },
|
{ id: "email", name: "resources.users.email" },
|
||||||
|
@ -66,12 +67,52 @@ const choices_type = [
|
||||||
{ id: "support", name: "support" },
|
{ id: "support", name: "support" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserListActions = () => {
|
const date_format = {
|
||||||
const { isLoading, total } = useListContext();
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserListActions = ({
|
||||||
|
sort,
|
||||||
|
className,
|
||||||
|
resource,
|
||||||
|
filters,
|
||||||
|
displayedFilters,
|
||||||
|
exporter, // you can hide ExportButton if exporter = (null || false)
|
||||||
|
filterValues,
|
||||||
|
permanentFilter,
|
||||||
|
hasCreate, // you can hide CreateButton if hasCreate = false
|
||||||
|
selectedIds,
|
||||||
|
onUnselectItems,
|
||||||
|
showFilter,
|
||||||
|
maxResults,
|
||||||
|
total,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
{filters &&
|
||||||
|
cloneElement(filters, {
|
||||||
|
resource,
|
||||||
|
showFilter,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
context: "button",
|
||||||
|
})}
|
||||||
<CreateButton />
|
<CreateButton />
|
||||||
<ExportButton disabled={isLoading || total === 0} maxResults={10000} />
|
<ExportButton
|
||||||
|
disabled={total === 0}
|
||||||
|
resource={resource}
|
||||||
|
sort={sort}
|
||||||
|
filter={{ ...filterValues, ...permanentFilter }}
|
||||||
|
exporter={exporter}
|
||||||
|
maxResults={maxResults}
|
||||||
|
/>
|
||||||
|
{/* Add your custom actions */}
|
||||||
<Button component={Link} to="/import_users" label="CSV Import">
|
<Button component={Link} to="/import_users" label="CSV Import">
|
||||||
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
|
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -84,12 +125,18 @@ UserListActions.defaultProps = {
|
||||||
onUnselectItems: () => null,
|
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
|
||||||
|
/>,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserBulkActionButtons = () => (
|
const UserBulkActionButtons = () => (
|
||||||
|
@ -103,25 +150,32 @@ const UserBulkActionButtons = () => (
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UserList = (props: ListProps) => (
|
export const UserList = props => (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={userFilters}
|
filters={userFilters}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
actions={<UserListActions />}
|
actions={<UserListActions maxResults={10000} />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
<Datagrid rowClick="edit" 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" />
|
||||||
<BooleanField source="is_guest" />
|
<BooleanField source="is_guest" />
|
||||||
<BooleanField source="admin" />
|
<BooleanField source="admin" />
|
||||||
<BooleanField source="deactivated" />
|
<BooleanField source="deactivated" />
|
||||||
<BooleanField source="locked" />
|
<DateField
|
||||||
<BooleanField source="erased" sortable={false} />
|
source="creation_ts"
|
||||||
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
label="resources.users.fields.creation_ts_ms"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
/>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
@ -130,18 +184,73 @@ export const UserList = (props: ListProps) => (
|
||||||
// here only local part of user_id
|
// here only local part of user_id
|
||||||
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
|
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
|
||||||
// localStorage.getItem("home_server").length is not valid here
|
// localStorage.getItem("home_server").length is not valid here
|
||||||
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
|
const validateUser = [
|
||||||
|
required(),
|
||||||
|
maxLength(253),
|
||||||
|
regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id"),
|
||||||
|
];
|
||||||
|
|
||||||
const validateAddress = [required(), maxLength(255)];
|
const validateAddress = [required(), maxLength(255)];
|
||||||
|
|
||||||
const UserEditActions = () => {
|
export function generateRandomUser() {
|
||||||
const record = useRecordContext();
|
const homeserver = localStorage.getItem("home_server");
|
||||||
|
const user_id =
|
||||||
|
"@" +
|
||||||
|
Array(8)
|
||||||
|
.fill("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||||
|
.map(
|
||||||
|
x =>
|
||||||
|
x[
|
||||||
|
Math.floor(
|
||||||
|
(crypto.getRandomValues(new Uint32Array(1))[0] /
|
||||||
|
(0xffffffff + 1)) *
|
||||||
|
x.length
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.join("") +
|
||||||
|
":" +
|
||||||
|
homeserver;
|
||||||
|
|
||||||
|
const password = Array(20)
|
||||||
|
.fill(
|
||||||
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$"
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
x =>
|
||||||
|
x[
|
||||||
|
Math.floor(
|
||||||
|
(crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)) *
|
||||||
|
x.length
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user_id,
|
||||||
|
password: password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserEditToolbar = props => (
|
||||||
|
<Toolbar {...props}>
|
||||||
|
<SaveButton disabled={props.pristine} />
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserEditActions = ({ data }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
var userStatus = "";
|
||||||
|
if (data) {
|
||||||
|
userStatus = data.deactivated;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{!record.deactivated && <ServerNoticeButton />}
|
{!userStatus && <ServerNoticeButton record={data} />}
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
record={data}
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle={translate("resources.users.helper.erase", {
|
confirmTitle={translate("resources.users.helper.erase", {
|
||||||
smart_count: 1,
|
smart_count: 1,
|
||||||
|
@ -152,24 +261,41 @@ const UserEditActions = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserCreate = (props: CreateProps) => (
|
export const UserCreate = props => (
|
||||||
<Create {...props}>
|
<Create {...props}>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
<TextInput source="displayname" validate={maxLength(256)} />
|
<TextInput source="displayname" validate={maxLength(256)} />
|
||||||
<PasswordInput source="password" autoComplete="new-password" validate={maxLength(512)} />
|
<PasswordInput
|
||||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
source="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
validate={maxLength(512)}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
source="user_type"
|
||||||
|
choices={choices_type}
|
||||||
|
translateChoice={false}
|
||||||
|
resettable
|
||||||
|
/>
|
||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
<ArrayInput source="threepids">
|
<ArrayInput source="threepids">
|
||||||
<SimpleFormIterator disableReordering>
|
<SimpleFormIterator disableReordering>
|
||||||
<SelectInput source="medium" choices={choices_medium} validate={required()} />
|
<SelectInput
|
||||||
|
source="medium"
|
||||||
|
choices={choices_medium}
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
<TextInput source="address" validate={validateAddress} />
|
<TextInput source="address" validate={validateAddress} />
|
||||||
</SimpleFormIterator>
|
</SimpleFormIterator>
|
||||||
</ArrayInput>
|
</ArrayInput>
|
||||||
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
|
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
|
||||||
<SimpleFormIterator disableReordering>
|
<SimpleFormIterator disableReordering>
|
||||||
<TextInput source="auth_provider" validate={required()} />
|
<TextInput source="auth_provider" validate={required()} />
|
||||||
<TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
|
<TextInput
|
||||||
|
source="external_id"
|
||||||
|
label="resources.users.fields.id"
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
</SimpleFormIterator>
|
</SimpleFormIterator>
|
||||||
</ArrayInput>
|
</ArrayInput>
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
|
@ -189,26 +315,47 @@ const UserTitle = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserEdit = (props: EditProps) => {
|
export const UserEdit = props => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||||
<TabbedForm>
|
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
<FormTab
|
||||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
|
label={translate("resources.users.name", { smart_count: 1 })}
|
||||||
|
icon={<PersonPinIcon />}
|
||||||
|
>
|
||||||
|
<AvatarField
|
||||||
|
source="avatar_src"
|
||||||
|
sortable={false}
|
||||||
|
sx={{ height: "120px", width: "120px", float: "right" }}
|
||||||
|
/>
|
||||||
<TextInput source="id" disabled />
|
<TextInput source="id" disabled />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
<PasswordInput
|
||||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
source="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
helperText="resources.users.helper.password"
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
source="user_type"
|
||||||
|
choices={choices_type}
|
||||||
|
translateChoice={false}
|
||||||
|
resettable
|
||||||
|
/>
|
||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
<BooleanInput source="locked" />
|
<BooleanInput
|
||||||
<BooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
source="deactivated"
|
||||||
<BooleanInput source="erased" disabled />
|
helperText="resources.users.helper.deactivate"
|
||||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
/>
|
||||||
|
<DateField source="creation_ts_ms" showTime options={date_format} />
|
||||||
<TextField source="consent_version" />
|
<TextField source="consent_version" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab label="resources.users.threepid" icon={<ContactMailIcon />} path="threepid">
|
<FormTab
|
||||||
|
label="resources.users.threepid"
|
||||||
|
icon={<ContactMailIcon />}
|
||||||
|
path="threepid"
|
||||||
|
>
|
||||||
<ArrayInput source="threepids">
|
<ArrayInput source="threepids">
|
||||||
<SimpleFormIterator disableReordering>
|
<SimpleFormIterator disableReordering>
|
||||||
<SelectInput source="medium" choices={choices_medium} />
|
<SelectInput source="medium" choices={choices_medium} />
|
||||||
|
@ -217,34 +364,76 @@ export const UserEdit = (props: EditProps) => {
|
||||||
</ArrayInput>
|
</ArrayInput>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab label="synapseadmin.users.tabs.sso" icon={<AssignmentIndIcon />} path="sso">
|
<FormTab
|
||||||
|
label="synapseadmin.users.tabs.sso"
|
||||||
|
icon={<AssignmentIndIcon />}
|
||||||
|
path="sso"
|
||||||
|
>
|
||||||
<ArrayInput source="external_ids" label={false}>
|
<ArrayInput source="external_ids" label={false}>
|
||||||
<SimpleFormIterator disableReordering>
|
<SimpleFormIterator disableReordering>
|
||||||
<TextInput source="auth_provider" validate={required()} />
|
<TextInput source="auth_provider" validate={required()} />
|
||||||
<TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
|
<TextInput
|
||||||
|
source="external_id"
|
||||||
|
label="resources.users.fields.id"
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
</SimpleFormIterator>
|
</SimpleFormIterator>
|
||||||
</ArrayInput>
|
</ArrayInput>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
|
<FormTab
|
||||||
<ReferenceManyField reference="devices" target="user_id" label={false}>
|
label={translate("resources.devices.name", { smart_count: 2 })}
|
||||||
|
icon={<DevicesIcon />}
|
||||||
|
path="devices"
|
||||||
|
>
|
||||||
|
<ReferenceManyField
|
||||||
|
reference="devices"
|
||||||
|
target="user_id"
|
||||||
|
addLabel={false}
|
||||||
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<TextField source="device_id" sortable={false} />
|
<TextField source="device_id" sortable={false} />
|
||||||
<TextField source="display_name" sortable={false} />
|
<TextField source="display_name" sortable={false} />
|
||||||
<TextField source="last_seen_ip" sortable={false} />
|
<TextField source="last_seen_ip" sortable={false} />
|
||||||
<DateField source="last_seen_ts" showTime options={DATE_FORMAT} sortable={false} />
|
<DateField
|
||||||
|
source="last_seen_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
<DeviceRemoveButton />
|
<DeviceRemoveButton />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab label="resources.connections.name" icon={<SettingsInputComponentIcon />} path="connections">
|
<FormTab
|
||||||
<ReferenceField reference="connections" source="id" label={false} link={false}>
|
label="resources.connections.name"
|
||||||
<ArrayField source="devices[].sessions[0].connections" label="resources.connections.name">
|
icon={<SettingsInputComponentIcon />}
|
||||||
|
path="connections"
|
||||||
|
>
|
||||||
|
<ReferenceField
|
||||||
|
reference="connections"
|
||||||
|
source="id"
|
||||||
|
addLabel={false}
|
||||||
|
link={false}
|
||||||
|
>
|
||||||
|
<ArrayField
|
||||||
|
source="devices[].sessions[0].connections"
|
||||||
|
label="resources.connections.name"
|
||||||
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="ip" sortable={false} />
|
<TextField source="ip" sortable={false} />
|
||||||
<DateField source="last_seen" showTime options={DATE_FORMAT} sortable={false} />
|
<DateField
|
||||||
<TextField source="user_agent" sortable={false} style={{ width: "100%" }} />
|
source="last_seen"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
source="user_agent"
|
||||||
|
sortable={false}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</ArrayField>
|
</ArrayField>
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
|
@ -258,15 +447,19 @@ export const UserEdit = (props: EditProps) => {
|
||||||
<ReferenceManyField
|
<ReferenceManyField
|
||||||
reference="users_media"
|
reference="users_media"
|
||||||
target="user_id"
|
target="user_id"
|
||||||
label={false}
|
addLabel={false}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
perPage={50}
|
perPage={50}
|
||||||
sort={{ field: "created_ts", order: "DESC" }}
|
sort={{ field: "created_ts", order: "DESC" }}
|
||||||
>
|
>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }}>
|
||||||
<MediaIDField source="media_id" />
|
<MediaIDField source="media_id" />
|
||||||
<DateField source="created_ts" showTime options={DATE_FORMAT} />
|
<DateField source="created_ts" showTime options={date_format} />
|
||||||
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
<DateField
|
||||||
|
source="last_access_ts"
|
||||||
|
showTime
|
||||||
|
options={date_format}
|
||||||
|
/>
|
||||||
<NumberField source="media_length" />
|
<NumberField source="media_length" />
|
||||||
<TextField source="media_type" />
|
<TextField source="media_type" />
|
||||||
<TextField source="upload_name" />
|
<TextField source="upload_name" />
|
||||||
|
@ -278,10 +471,26 @@ export const UserEdit = (props: EditProps) => {
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms">
|
<FormTab
|
||||||
<ReferenceManyField reference="joined_rooms" target="user_id" label={false}>
|
label={translate("resources.rooms.name", { smart_count: 2 })}
|
||||||
<Datagrid style={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}>
|
icon={<ViewListIcon />}
|
||||||
<TextField source="id" sortable={false} label="resources.rooms.fields.room_id" />
|
path="rooms"
|
||||||
|
>
|
||||||
|
<ReferenceManyField
|
||||||
|
reference="joined_rooms"
|
||||||
|
target="user_id"
|
||||||
|
addLabel={false}
|
||||||
|
>
|
||||||
|
<Datagrid
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
|
||||||
|
bulkActionButtons={false}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
source="id"
|
||||||
|
sortable={false}
|
||||||
|
label="resources.rooms.fields.room_id"
|
||||||
|
/>
|
||||||
<ReferenceField
|
<ReferenceField
|
||||||
label="resources.rooms.fields.name"
|
label="resources.rooms.fields.name"
|
||||||
source="id"
|
source="id"
|
||||||
|
@ -300,7 +509,11 @@ export const UserEdit = (props: EditProps) => {
|
||||||
icon={<NotificationsIcon />}
|
icon={<NotificationsIcon />}
|
||||||
path="pushers"
|
path="pushers"
|
||||||
>
|
>
|
||||||
<ReferenceManyField reference="pushers" target="user_id" label={false}>
|
<ReferenceManyField
|
||||||
|
reference="pushers"
|
||||||
|
target="user_id"
|
||||||
|
addLabel={false}
|
||||||
|
>
|
||||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="kind" sortable={false} />
|
<TextField source="kind" sortable={false} />
|
||||||
<TextField source="app_display_name" sortable={false} />
|
<TextField source="app_display_name" sortable={false} />
|
||||||
|
@ -318,7 +531,7 @@ export const UserEdit = (props: EditProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource: ResourceProps = {
|
const resource = {
|
||||||
name: "users",
|
name: "users",
|
||||||
icon: UserIcon,
|
icon: UserIcon,
|
||||||
list: UserList,
|
list: UserList,
|
|
@ -1,8 +1,6 @@
|
||||||
import { formalGermanMessages } from "@haleos/ra-language-german";
|
import { formalGermanMessages } from "@haleos/ra-language-german";
|
||||||
|
|
||||||
import { SynapseTranslationMessages } from ".";
|
const de = {
|
||||||
|
|
||||||
const de: SynapseTranslationMessages = {
|
|
||||||
...formalGermanMessages,
|
...formalGermanMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
|
@ -46,9 +44,11 @@ const de: SynapseTranslationMessages = {
|
||||||
cards: {
|
cards: {
|
||||||
importstats: {
|
importstats: {
|
||||||
header: "Benutzer importieren",
|
header: "Benutzer importieren",
|
||||||
users_total: "%{smart_count} Benutzer in der CSV Datei |||| %{smart_count} Benutzer in der CSV Datei",
|
users_total:
|
||||||
|
"%{smart_count} Benutzer in der CSV Datei |||| %{smart_count} Benutzer in der CSV Datei",
|
||||||
guest_count: "%{smart_count} Gast |||| %{smart_count} Gäste",
|
guest_count: "%{smart_count} Gast |||| %{smart_count} Gäste",
|
||||||
admin_count: "%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
|
admin_count:
|
||||||
|
"%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
|
||||||
},
|
},
|
||||||
conflicts: {
|
conflicts: {
|
||||||
header: "Konfliktstrategie",
|
header: "Konfliktstrategie",
|
||||||
|
@ -60,7 +60,8 @@ const de: SynapseTranslationMessages = {
|
||||||
ids: {
|
ids: {
|
||||||
header: "IDs",
|
header: "IDs",
|
||||||
all_ids_present: "IDs in jedem Eintrag vorhanden",
|
all_ids_present: "IDs in jedem Eintrag vorhanden",
|
||||||
count_ids_present: "%{smart_count} Eintrag mit ID |||| %{smart_count} Einträge mit IDs",
|
count_ids_present:
|
||||||
|
"%{smart_count} Eintrag mit ID |||| %{smart_count} Einträge mit IDs",
|
||||||
mode: {
|
mode: {
|
||||||
ignore: "Ignoriere IDs der CSV-Datei und erstelle neue",
|
ignore: "Ignoriere IDs der CSV-Datei und erstelle neue",
|
||||||
update: "Aktualisiere existierende Benutzer",
|
update: "Aktualisiere existierende Benutzer",
|
||||||
|
@ -69,7 +70,8 @@ const de: SynapseTranslationMessages = {
|
||||||
passwords: {
|
passwords: {
|
||||||
header: "Passwörter",
|
header: "Passwörter",
|
||||||
all_passwords_present: "Passwörter in jedem Eintrag vorhanden",
|
all_passwords_present: "Passwörter in jedem Eintrag vorhanden",
|
||||||
count_passwords_present: "%{smart_count} Eintrag mit Passwort |||| %{smart_count} Einträge mit Passwörtern",
|
count_passwords_present:
|
||||||
|
"%{smart_count} Eintrag mit Passwort |||| %{smart_count} Einträge mit Passwörtern",
|
||||||
use_passwords: "Verwende Passwörter aus der CSV Datei",
|
use_passwords: "Verwende Passwörter aus der CSV Datei",
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
|
@ -83,11 +85,13 @@ const de: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
header: "Ergebnis",
|
header: "Ergebnis",
|
||||||
total: "%{smart_count} Eintrag insgesamt |||| %{smart_count} Einträge insgesamt",
|
total:
|
||||||
|
"%{smart_count} Eintrag insgesamt |||| %{smart_count} Einträge insgesamt",
|
||||||
successful: "%{smart_count} Einträge erfolgreich importiert",
|
successful: "%{smart_count} Einträge erfolgreich importiert",
|
||||||
skipped: "%{smart_count} Einträge übersprungen",
|
skipped: "%{smart_count} Einträge übersprungen",
|
||||||
download_skipped: "Übersprungene Einträge herunterladen",
|
download_skipped: "Übersprungene Einträge herunterladen",
|
||||||
with_error: "%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
|
with_error:
|
||||||
|
"%{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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -104,9 +108,7 @@ const de: SynapseTranslationMessages = {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
is_guest: "Gast",
|
is_guest: "Gast",
|
||||||
admin: "Server Administrator",
|
admin: "Server Administrator",
|
||||||
locked: "Gesperrt",
|
|
||||||
deactivated: "Deaktiviert",
|
deactivated: "Deaktiviert",
|
||||||
erased: "Gelöscht",
|
|
||||||
guests: "Zeige Gäste",
|
guests: "Zeige Gäste",
|
||||||
show_deactivated: "Zeige deaktivierte Benutzer",
|
show_deactivated: "Zeige deaktivierte Benutzer",
|
||||||
user_id: "Suche Benutzer",
|
user_id: "Suche Benutzer",
|
||||||
|
@ -123,8 +125,10 @@ const de: SynapseTranslationMessages = {
|
||||||
user_type: "Benutzertyp",
|
user_type: "Benutzertyp",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
password:
|
||||||
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
"Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
||||||
|
deactivate:
|
||||||
|
"Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
||||||
erase: "DSGVO konformes Löschen der Benutzerdaten",
|
erase: "DSGVO konformes Löschen der Benutzerdaten",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
|
@ -207,7 +211,6 @@ const de: SynapseTranslationMessages = {
|
||||||
info: {
|
info: {
|
||||||
mimetype: "Typ",
|
mimetype: "Typ",
|
||||||
},
|
},
|
||||||
url: "URL",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -356,7 +359,8 @@ const de: SynapseTranslationMessages = {
|
||||||
guest_can_join: "Gastbenutzer dürfen beitreten",
|
guest_can_join: "Gastbenutzer dürfen beitreten",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
title: "Raum aus Verzeichnis löschen |||| %{smart_count} Räume aus Verzeichnis löschen",
|
title:
|
||||||
|
"Raum aus Verzeichnis löschen |||| %{smart_count} Räume aus Verzeichnis löschen",
|
||||||
content:
|
content:
|
||||||
"Möchten Sie den Raum wirklich aus dem Raumverzeichnis löschen? |||| Möchten Sie die %{smart_count} Räume wirklich aus dem Raumverzeichnis löschen?",
|
"Möchten Sie den Raum wirklich aus dem Raumverzeichnis löschen? |||| Möchten Sie die %{smart_count} Räume wirklich aus dem Raumverzeichnis löschen?",
|
||||||
erase: "Lösche aus Verzeichnis",
|
erase: "Lösche aus Verzeichnis",
|
|
@ -1,8 +1,6 @@
|
||||||
import englishMessages from "ra-language-english";
|
import englishMessages from "ra-language-english";
|
||||||
|
|
||||||
import { SynapseTranslationMessages } from ".";
|
const en = {
|
||||||
|
|
||||||
const en: SynapseTranslationMessages = {
|
|
||||||
...englishMessages,
|
...englishMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
|
@ -20,7 +18,6 @@ const en: SynapseTranslationMessages = {
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Room details",
|
|
||||||
tabs: {
|
tabs: {
|
||||||
basic: "Basic",
|
basic: "Basic",
|
||||||
members: "Members",
|
members: "Members",
|
||||||
|
@ -35,8 +32,10 @@ const en: SynapseTranslationMessages = {
|
||||||
at_entry: "At entry %{entry}: %{message}",
|
at_entry: "At entry %{entry}: %{message}",
|
||||||
error: "Error",
|
error: "Error",
|
||||||
required_field: "Required field '%{field}' is not present",
|
required_field: "Required field '%{field}' is not present",
|
||||||
invalid_value: "Invalid value on line %{row}. '%{field}' field may only be 'true' or 'false'",
|
invalid_value:
|
||||||
unreasonably_big: "Refused to load unreasonably big file of %{size} megabytes",
|
"Invalid value on line %{row}. '%{field}' field may only be 'true' or 'false'",
|
||||||
|
unreasonably_big:
|
||||||
|
"Refused to load unreasonably big file of %{size} megabytes",
|
||||||
already_in_progress: "An import run is already in progress",
|
already_in_progress: "An import run is already in progress",
|
||||||
id_exits: "ID %{id} already present",
|
id_exits: "ID %{id} already present",
|
||||||
},
|
},
|
||||||
|
@ -45,7 +44,8 @@ const en: SynapseTranslationMessages = {
|
||||||
cards: {
|
cards: {
|
||||||
importstats: {
|
importstats: {
|
||||||
header: "Import users",
|
header: "Import users",
|
||||||
users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
|
users_total:
|
||||||
|
"%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
|
||||||
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
|
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
|
||||||
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
|
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,8 @@ const en: SynapseTranslationMessages = {
|
||||||
ids: {
|
ids: {
|
||||||
header: "IDs",
|
header: "IDs",
|
||||||
all_ids_present: "IDs present on every entry",
|
all_ids_present: "IDs present on every entry",
|
||||||
count_ids_present: "%{smart_count} entry with ID |||| %{smart_count} entries with IDs",
|
count_ids_present:
|
||||||
|
"%{smart_count} entry with ID |||| %{smart_count} entries with IDs",
|
||||||
mode: {
|
mode: {
|
||||||
ignore: "Ignore IDs in CSV and create new ones",
|
ignore: "Ignore IDs in CSV and create new ones",
|
||||||
update: "Update existing records",
|
update: "Update existing records",
|
||||||
|
@ -68,7 +69,8 @@ const en: SynapseTranslationMessages = {
|
||||||
passwords: {
|
passwords: {
|
||||||
header: "Passwords",
|
header: "Passwords",
|
||||||
all_passwords_present: "Passwords present on every entry",
|
all_passwords_present: "Passwords present on every entry",
|
||||||
count_passwords_present: "%{smart_count} entry with password |||| %{smart_count} entries with passwords",
|
count_passwords_present:
|
||||||
|
"%{smart_count} entry with password |||| %{smart_count} entries with passwords",
|
||||||
use_passwords: "Use passwords from CSV",
|
use_passwords: "Use passwords from CSV",
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
|
@ -82,11 +84,13 @@ const en: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
header: "Import results",
|
header: "Import results",
|
||||||
total: "%{smart_count} entry in total |||| %{smart_count} entries in total",
|
total:
|
||||||
|
"%{smart_count} entry in total |||| %{smart_count} entries in total",
|
||||||
successful: "%{smart_count} entries successfully imported",
|
successful: "%{smart_count} entries successfully imported",
|
||||||
skipped: "%{smart_count} entries skipped",
|
skipped: "%{smart_count} entries skipped",
|
||||||
download_skipped: "Download skipped records",
|
download_skipped: "Download skipped records",
|
||||||
with_error: "%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
with_error:
|
||||||
|
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
||||||
simulated_only: "Run was only simulated",
|
simulated_only: "Run was only simulated",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -103,9 +107,7 @@ const en: SynapseTranslationMessages = {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
is_guest: "Guest",
|
is_guest: "Guest",
|
||||||
admin: "Server Administrator",
|
admin: "Server Administrator",
|
||||||
locked: "Locked",
|
|
||||||
deactivated: "Deactivated",
|
deactivated: "Deactivated",
|
||||||
erased: "Erased",
|
|
||||||
guests: "Show guests",
|
guests: "Show guests",
|
||||||
show_deactivated: "Show deactivated users",
|
show_deactivated: "Show deactivated users",
|
||||||
user_id: "Search user",
|
user_id: "Search user",
|
||||||
|
@ -213,7 +215,8 @@ const en: SynapseTranslationMessages = {
|
||||||
action: {
|
action: {
|
||||||
erase: {
|
erase: {
|
||||||
title: "Delete reported event",
|
title: "Delete reported event",
|
||||||
content: "Are you sure you want to delete the reported event? This cannot be undone.",
|
content:
|
||||||
|
"Are you sure you want to delete the reported event? This cannot be undone.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -354,7 +357,8 @@ const en: SynapseTranslationMessages = {
|
||||||
guest_can_join: "guest users may join",
|
guest_can_join: "guest users may join",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
title: "Delete room from directory |||| Delete %{smart_count} rooms from directory",
|
title:
|
||||||
|
"Delete room from directory |||| Delete %{smart_count} rooms from directory",
|
||||||
content:
|
content:
|
||||||
"Are you sure you want to remove this room from directory? |||| Are you sure you want to remove these %{smart_count} rooms from directory?",
|
"Are you sure you want to remove this room from directory? |||| Are you sure you want to remove these %{smart_count} rooms from directory?",
|
||||||
erase: "Delete from room directory",
|
erase: "Delete from room directory",
|
|
@ -1,8 +1,6 @@
|
||||||
import farsiMessages from "ra-language-farsi";
|
import farsiMessages from "ra-language-farsi";
|
||||||
|
|
||||||
import { SynapseTranslationMessages } from ".";
|
const fa = {
|
||||||
|
|
||||||
const fa: SynapseTranslationMessages = {
|
|
||||||
...farsiMessages,
|
...farsiMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
|
@ -33,8 +31,10 @@ const fa: SynapseTranslationMessages = {
|
||||||
at_entry: "در هنگام ورود %{entry}: %{message}",
|
at_entry: "در هنگام ورود %{entry}: %{message}",
|
||||||
error: "Error",
|
error: "Error",
|
||||||
required_field: "فیلد الزامی '%{field}' وجود ندارد",
|
required_field: "فیلد الزامی '%{field}' وجود ندارد",
|
||||||
invalid_value: "خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
|
invalid_value:
|
||||||
unreasonably_big: "از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
|
"خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
|
||||||
|
unreasonably_big:
|
||||||
|
"از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
|
||||||
already_in_progress: "یک بارگذاری از قبل در حال انجام است",
|
already_in_progress: "یک بارگذاری از قبل در حال انجام است",
|
||||||
id_exits: "شناسه %{id} موجود است",
|
id_exits: "شناسه %{id} موجود است",
|
||||||
},
|
},
|
||||||
|
@ -43,7 +43,8 @@ const fa: SynapseTranslationMessages = {
|
||||||
cards: {
|
cards: {
|
||||||
importstats: {
|
importstats: {
|
||||||
header: "وارد کردن کاربران",
|
header: "وارد کردن کاربران",
|
||||||
users_total: "%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
|
users_total:
|
||||||
|
"%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
|
||||||
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
|
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
|
||||||
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
|
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
|
||||||
},
|
},
|
||||||
|
@ -57,7 +58,8 @@ const fa: SynapseTranslationMessages = {
|
||||||
ids: {
|
ids: {
|
||||||
header: "شناسنامه ها",
|
header: "شناسنامه ها",
|
||||||
all_ids_present: "شناسه های موجود در هر ورودی",
|
all_ids_present: "شناسه های موجود در هر ورودی",
|
||||||
count_ids_present: "%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
|
count_ids_present:
|
||||||
|
"%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
|
||||||
mode: {
|
mode: {
|
||||||
ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
|
ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
|
||||||
update: "سوابق موجود را به روز کنید",
|
update: "سوابق موجود را به روز کنید",
|
||||||
|
@ -66,7 +68,8 @@ const fa: SynapseTranslationMessages = {
|
||||||
passwords: {
|
passwords: {
|
||||||
header: "رمز عبور",
|
header: "رمز عبور",
|
||||||
all_passwords_present: "رمزهای عبور موجود در هر ورودی",
|
all_passwords_present: "رمزهای عبور موجود در هر ورودی",
|
||||||
count_passwords_present: "%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
|
count_passwords_present:
|
||||||
|
"%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
|
||||||
use_passwords: "از پسوردهای CSV استفاده کنید",
|
use_passwords: "از پسوردهای CSV استفاده کنید",
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
|
@ -84,7 +87,8 @@ const fa: SynapseTranslationMessages = {
|
||||||
successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
|
successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
|
||||||
skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
|
skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
|
||||||
download_skipped: "دانلود رکوردهای نادیده گرفته شده",
|
download_skipped: "دانلود رکوردهای نادیده گرفته شده",
|
||||||
with_error: "%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
|
with_error:
|
||||||
|
"%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
|
||||||
simulated_only: "اجرا فقط شبیه سازی شد",
|
simulated_only: "اجرا فقط شبیه سازی شد",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -222,7 +226,8 @@ const fa: SynapseTranslationMessages = {
|
||||||
action: {
|
action: {
|
||||||
erase: {
|
erase: {
|
||||||
title: "حذف کردن %{id}",
|
title: "حذف کردن %{id}",
|
||||||
content: 'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
|
content:
|
||||||
|
'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
|
||||||
success: "دستگاه با موفقیت حذف شد.",
|
success: "دستگاه با موفقیت حذف شد.",
|
||||||
failure: "خطایی رخ داده است.",
|
failure: "خطایی رخ داده است.",
|
||||||
},
|
},
|
||||||
|
@ -337,7 +342,8 @@ const fa: SynapseTranslationMessages = {
|
||||||
guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
|
guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
title: "اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
|
title:
|
||||||
|
"اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
|
||||||
content:
|
content:
|
||||||
"آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{smart_count} از راهنمای اتاق ها حذف کنید؟",
|
"آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{smart_count} از راهنمای اتاق ها حذف کنید؟",
|
||||||
erase: "حذف از فهرست اتاق",
|
erase: "حذف از فهرست اتاق",
|
|
@ -1,21 +1,21 @@
|
||||||
import frenchMessages from "ra-language-french";
|
import frenchMessages from "ra-language-french";
|
||||||
|
|
||||||
import { SynapseTranslationMessages } from ".";
|
const fr = {
|
||||||
|
|
||||||
const fr: SynapseTranslationMessages = {
|
|
||||||
...frenchMessages,
|
...frenchMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "URL du serveur d’accueil",
|
base_url: "URL du serveur d’accueil",
|
||||||
welcome: "Bienvenue sur Synapse-admin",
|
welcome: "Bienvenue sur Synapse-admin",
|
||||||
server_version: "Version du serveur Synapse",
|
server_version: "Version du serveur Synapse",
|
||||||
username_error: "Veuillez entrer un nom d'utilisateur complet : « @utilisateur:domaine »",
|
username_error:
|
||||||
|
"Veuillez entrer un nom d'utilisateur complet : « @utilisateur:domaine »",
|
||||||
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
|
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
|
||||||
url_error: "L'URL du serveur Matrix n'est pas valide",
|
url_error: "L'URL du serveur Matrix n'est pas valide",
|
||||||
sso_sign_in: "Se connecter avec l’authentification unique",
|
sso_sign_in: "Se connecter avec l’authentification unique",
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
invalid_user_id:
|
||||||
|
"Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
||||||
tabs: { sso: "Authentification unique" },
|
tabs: { sso: "Authentification unique" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@ -35,7 +35,8 @@ const fr: SynapseTranslationMessages = {
|
||||||
required_field: "Le champ requis « %{field} » est manquant",
|
required_field: "Le champ requis « %{field} » est manquant",
|
||||||
invalid_value:
|
invalid_value:
|
||||||
"Valeur non valide à la ligne %{row}. Le champ « %{field} » ne peut être que « true » ou « false »",
|
"Valeur non valide à la ligne %{row}. Le champ « %{field} » ne peut être que « true » ou « false »",
|
||||||
unreasonably_big: "Refus de charger un fichier trop volumineux de %{size} mégaoctets",
|
unreasonably_big:
|
||||||
|
"Refus de charger un fichier trop volumineux de %{size} mégaoctets",
|
||||||
already_in_progress: "Un import est déjà en cours",
|
already_in_progress: "Un import est déjà en cours",
|
||||||
id_exits: "L'identifiant %{id} déjà présent",
|
id_exits: "L'identifiant %{id} déjà présent",
|
||||||
},
|
},
|
||||||
|
@ -47,7 +48,8 @@ const fr: SynapseTranslationMessages = {
|
||||||
users_total:
|
users_total:
|
||||||
"%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
|
"%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
|
||||||
guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs",
|
guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs",
|
||||||
admin_count: "%{smart_count} administrateur |||| %{smart_count} administrateurs",
|
admin_count:
|
||||||
|
"%{smart_count} administrateur |||| %{smart_count} administrateurs",
|
||||||
},
|
},
|
||||||
conflicts: {
|
conflicts: {
|
||||||
header: "Stratégie de résolution des conflits",
|
header: "Stratégie de résolution des conflits",
|
||||||
|
@ -59,9 +61,11 @@ const fr: SynapseTranslationMessages = {
|
||||||
ids: {
|
ids: {
|
||||||
header: "Identifiants",
|
header: "Identifiants",
|
||||||
all_ids_present: "Identifiants présents pour chaque entrée",
|
all_ids_present: "Identifiants présents pour chaque entrée",
|
||||||
count_ids_present: "%{smart_count} entrée avec identifiant |||| %{smart_count} entrées avec identifiant",
|
count_ids_present:
|
||||||
|
"%{smart_count} entrée avec identifiant |||| %{smart_count} entrées avec identifiant",
|
||||||
mode: {
|
mode: {
|
||||||
ignore: "Ignorer les identifiants dans le ficher CSV et en créer de nouveaux",
|
ignore:
|
||||||
|
"Ignorer les identifiants dans le ficher CSV et en créer de nouveaux",
|
||||||
update: "Mettre à jour les enregistrements existants",
|
update: "Mettre à jour les enregistrements existants",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -83,11 +87,13 @@ const fr: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
header: "Résultats de l'import",
|
header: "Résultats de l'import",
|
||||||
total: "%{smart_count} entrée au total |||| %{smart_count} entrées au total",
|
total:
|
||||||
|
"%{smart_count} entrée au total |||| %{smart_count} entrées au total",
|
||||||
successful: "%{smart_count} entrées importées avec succès",
|
successful: "%{smart_count} entrées importées avec succès",
|
||||||
skipped: "%{smart_count} entrées ignorées",
|
skipped: "%{smart_count} entrées ignorées",
|
||||||
download_skipped: "Télécharger les entrées ignorées",
|
download_skipped: "Télécharger les entrées ignorées",
|
||||||
with_error: "%{smart_count} entrée avec des erreurs ||| %{smart_count} entrées avec des erreurs",
|
with_error:
|
||||||
|
"%{smart_count} entrée avec des erreurs ||| %{smart_count} entrées avec des erreurs",
|
||||||
simulated_only: "L'import était simulé",
|
simulated_only: "L'import était simulé",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -104,7 +110,6 @@ const fr: SynapseTranslationMessages = {
|
||||||
name: "Nom",
|
name: "Nom",
|
||||||
is_guest: "Visiteur",
|
is_guest: "Visiteur",
|
||||||
admin: "Administrateur du serveur",
|
admin: "Administrateur du serveur",
|
||||||
locked: "Verrouillé",
|
|
||||||
deactivated: "Désactivé",
|
deactivated: "Désactivé",
|
||||||
guests: "Afficher les visiteurs",
|
guests: "Afficher les visiteurs",
|
||||||
show_deactivated: "Afficher les utilisateurs désactivés",
|
show_deactivated: "Afficher les utilisateurs désactivés",
|
||||||
|
@ -121,7 +126,8 @@ const fr: SynapseTranslationMessages = {
|
||||||
auth_provider: "Fournisseur d'identité",
|
auth_provider: "Fournisseur d'identité",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
deactivate:
|
||||||
|
"Vous devrez fournir un mot de passe pour réactiver le compte.",
|
||||||
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
|
@ -335,11 +341,13 @@ const fr: SynapseTranslationMessages = {
|
||||||
room_directory: {
|
room_directory: {
|
||||||
name: "Répertoire des salons",
|
name: "Répertoire des salons",
|
||||||
fields: {
|
fields: {
|
||||||
world_readable: "Tout utilisateur peut avoir un aperçu du salon, sans en devenir membre",
|
world_readable:
|
||||||
|
"Tout utilisateur peut avoir un aperçu du salon, sans en devenir membre",
|
||||||
guest_can_join: "Les visiteurs peuvent rejoindre le salon",
|
guest_can_join: "Les visiteurs peuvent rejoindre le salon",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
title: "Supprimer un salon du répertoire |||| Supprimer %{smart_count} salons du répertoire",
|
title:
|
||||||
|
"Supprimer un salon du répertoire |||| Supprimer %{smart_count} salons du répertoire",
|
||||||
content:
|
content:
|
||||||
"Voulez-vous vraiment supprimer ce salon du répertoire ? |||| Voulez-vous vraiment supprimer ces %{smart_count} salons du répertoire ?",
|
"Voulez-vous vraiment supprimer ce salon du répertoire ? |||| Voulez-vous vraiment supprimer ces %{smart_count} salons du répertoire ?",
|
||||||
erase: "Supprimer du répertoire des salons",
|
erase: "Supprimer du répertoire des salons",
|
||||||
|
@ -360,7 +368,8 @@ const fr: SynapseTranslationMessages = {
|
||||||
length: "Longueur",
|
length: "Longueur",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
length: "Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
|
length:
|
||||||
|
"Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
391
src/i18n/index.d.ts
vendored
391
src/i18n/index.d.ts
vendored
|
@ -1,391 +0,0 @@
|
||||||
import { TranslationMessages } from "ra-core";
|
|
||||||
|
|
||||||
interface SynapseTranslationMessages extends TranslationMessages {
|
|
||||||
synapseadmin: {
|
|
||||||
auth: {
|
|
||||||
base_url: string;
|
|
||||||
welcome: string;
|
|
||||||
server_version: string;
|
|
||||||
supports_specs?: string; // TODO: fa, fr, it, zh
|
|
||||||
username_error: string;
|
|
||||||
protocol_error: string;
|
|
||||||
url_error: string;
|
|
||||||
sso_sign_in: string;
|
|
||||||
};
|
|
||||||
users: {
|
|
||||||
invalid_user_id: string;
|
|
||||||
tabs: { sso: string };
|
|
||||||
};
|
|
||||||
rooms: {
|
|
||||||
details?: string; // TODO: fa, fr, it, zh
|
|
||||||
tabs: {
|
|
||||||
basic: string;
|
|
||||||
members: string;
|
|
||||||
detail: string;
|
|
||||||
permission: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
reports: { tabs: { basic: string; detail: string } };
|
|
||||||
};
|
|
||||||
import_users: {
|
|
||||||
error: {
|
|
||||||
at_entry: string;
|
|
||||||
error: string;
|
|
||||||
required_field: string;
|
|
||||||
invalid_value: string;
|
|
||||||
unreasonably_big: string;
|
|
||||||
already_in_progress: string;
|
|
||||||
id_exits: string;
|
|
||||||
};
|
|
||||||
title: string;
|
|
||||||
goToPdf: string;
|
|
||||||
cards: {
|
|
||||||
importstats: {
|
|
||||||
header: string;
|
|
||||||
users_total: string;
|
|
||||||
guest_count: string;
|
|
||||||
admin_count: string;
|
|
||||||
};
|
|
||||||
conflicts: {
|
|
||||||
header: string;
|
|
||||||
mode: {
|
|
||||||
stop: string;
|
|
||||||
skip: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
ids: {
|
|
||||||
header: string;
|
|
||||||
all_ids_present: string;
|
|
||||||
count_ids_present: string;
|
|
||||||
mode: {
|
|
||||||
ignore: string;
|
|
||||||
update: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
passwords: {
|
|
||||||
header: string;
|
|
||||||
all_passwords_present: string;
|
|
||||||
count_passwords_present: string;
|
|
||||||
use_passwords: string;
|
|
||||||
};
|
|
||||||
upload: {
|
|
||||||
header: string;
|
|
||||||
explanation: string;
|
|
||||||
};
|
|
||||||
startImport: {
|
|
||||||
simulate_only: string;
|
|
||||||
run_import: string;
|
|
||||||
};
|
|
||||||
results: {
|
|
||||||
header: string;
|
|
||||||
total: string;
|
|
||||||
successful: string;
|
|
||||||
skipped: string;
|
|
||||||
download_skipped: string;
|
|
||||||
with_error: string;
|
|
||||||
simulated_only: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
resources: {
|
|
||||||
users: {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
msisdn: string;
|
|
||||||
threepid: string;
|
|
||||||
fields: {
|
|
||||||
avatar: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
is_guest: string;
|
|
||||||
admin: string;
|
|
||||||
locked?: string; // TODO: fa, zh
|
|
||||||
deactivated: string;
|
|
||||||
erased?: string; // TODO: fa, fr, it, zh
|
|
||||||
guests: string;
|
|
||||||
show_deactivated: string;
|
|
||||||
user_id: string;
|
|
||||||
displayname: string;
|
|
||||||
password: string;
|
|
||||||
avatar_url: string;
|
|
||||||
avatar_src: string;
|
|
||||||
medium: string;
|
|
||||||
threepids: string;
|
|
||||||
address: string;
|
|
||||||
creation_ts_ms: string;
|
|
||||||
consent_version: string;
|
|
||||||
auth_provider?: string;
|
|
||||||
user_type?: string;
|
|
||||||
};
|
|
||||||
helper: {
|
|
||||||
password?: string;
|
|
||||||
deactivate: string;
|
|
||||||
erase: string;
|
|
||||||
};
|
|
||||||
action: {
|
|
||||||
erase: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
rooms: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
room_id: string;
|
|
||||||
name: string;
|
|
||||||
canonical_alias: string;
|
|
||||||
joined_members: string;
|
|
||||||
joined_local_members: string;
|
|
||||||
joined_local_devices?: string;
|
|
||||||
state_events: string;
|
|
||||||
version: string;
|
|
||||||
is_encrypted: string;
|
|
||||||
encryption: string;
|
|
||||||
federatable: string;
|
|
||||||
public: string;
|
|
||||||
creator: string;
|
|
||||||
join_rules: string;
|
|
||||||
guest_access: string;
|
|
||||||
history_visibility: string;
|
|
||||||
topic?: string;
|
|
||||||
avatar?: string;
|
|
||||||
};
|
|
||||||
helper?: {
|
|
||||||
forward_extremities: string;
|
|
||||||
};
|
|
||||||
enums: {
|
|
||||||
join_rules: {
|
|
||||||
public: string;
|
|
||||||
knock: string;
|
|
||||||
invite: string;
|
|
||||||
private: string;
|
|
||||||
};
|
|
||||||
guest_access: {
|
|
||||||
can_join: string;
|
|
||||||
forbidden: string;
|
|
||||||
};
|
|
||||||
history_visibility: {
|
|
||||||
invited: string;
|
|
||||||
joined: string;
|
|
||||||
shared: string;
|
|
||||||
world_readable: string;
|
|
||||||
};
|
|
||||||
unencrypted: string;
|
|
||||||
};
|
|
||||||
action?: {
|
|
||||||
erase: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
reports: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
id: string;
|
|
||||||
received_ts: string;
|
|
||||||
user_id: string;
|
|
||||||
name: string;
|
|
||||||
score: string;
|
|
||||||
reason: string;
|
|
||||||
event_id: string;
|
|
||||||
event_json: {
|
|
||||||
origin: string;
|
|
||||||
origin_server_ts: string;
|
|
||||||
type: string;
|
|
||||||
content: {
|
|
||||||
msgtype: string;
|
|
||||||
body: string;
|
|
||||||
format: string;
|
|
||||||
formatted_body: string;
|
|
||||||
algorithm: string;
|
|
||||||
url?: string;
|
|
||||||
info?: {
|
|
||||||
mimetype: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
action?: {
|
|
||||||
erase: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
connections: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
last_seen: string;
|
|
||||||
ip: string;
|
|
||||||
user_agent: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
devices: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
device_id: string;
|
|
||||||
display_name: string;
|
|
||||||
last_seen_ts: string;
|
|
||||||
last_seen_ip: string;
|
|
||||||
};
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
success: string;
|
|
||||||
failure: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
users_media: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
media_id: string;
|
|
||||||
media_length: string;
|
|
||||||
media_type: string;
|
|
||||||
upload_name: string;
|
|
||||||
quarantined_by: string;
|
|
||||||
safe_from_quarantine: string;
|
|
||||||
created_ts: string;
|
|
||||||
last_access_ts: string;
|
|
||||||
};
|
|
||||||
action?: {
|
|
||||||
open: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
delete_media: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
before_ts: string;
|
|
||||||
size_gt: string;
|
|
||||||
keep_profiles: string;
|
|
||||||
};
|
|
||||||
action: {
|
|
||||||
send: string;
|
|
||||||
send_success: string;
|
|
||||||
send_failure: string;
|
|
||||||
};
|
|
||||||
helper: {
|
|
||||||
send: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
protect_media?: {
|
|
||||||
action: {
|
|
||||||
create: string;
|
|
||||||
delete: string;
|
|
||||||
none: string;
|
|
||||||
send_success: string;
|
|
||||||
send_failure: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
quarantine_media?: {
|
|
||||||
action: {
|
|
||||||
name: string;
|
|
||||||
create: string;
|
|
||||||
delete: string;
|
|
||||||
none: string;
|
|
||||||
send_success: string;
|
|
||||||
send_failure: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
pushers: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
app: string;
|
|
||||||
app_display_name: string;
|
|
||||||
app_id: string;
|
|
||||||
device_display_name: string;
|
|
||||||
kind: string;
|
|
||||||
lang: string;
|
|
||||||
profile_tag: string;
|
|
||||||
pushkey: string;
|
|
||||||
data: {
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
servernotices: {
|
|
||||||
name: string;
|
|
||||||
send: string;
|
|
||||||
fields: {
|
|
||||||
body: string;
|
|
||||||
};
|
|
||||||
action: {
|
|
||||||
send: string;
|
|
||||||
send_success: string;
|
|
||||||
send_failure: string;
|
|
||||||
};
|
|
||||||
helper: {
|
|
||||||
send: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
user_media_statistics: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
media_count: string;
|
|
||||||
media_length: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
forward_extremities?: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
id: string;
|
|
||||||
received_ts: string;
|
|
||||||
depth: string;
|
|
||||||
state_group: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
room_state?: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
type: string;
|
|
||||||
content: string;
|
|
||||||
origin_server_ts: string;
|
|
||||||
sender: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
room_directory?: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
world_readable: string;
|
|
||||||
guest_can_join: string;
|
|
||||||
};
|
|
||||||
action: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
erase: string;
|
|
||||||
create: string;
|
|
||||||
send_success: string;
|
|
||||||
send_failure: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
destinations?: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
destination: string;
|
|
||||||
failure_ts: string;
|
|
||||||
retry_last_ts: string;
|
|
||||||
retry_interval: string;
|
|
||||||
last_successful_stream_ordering: string;
|
|
||||||
stream_ordering: string;
|
|
||||||
};
|
|
||||||
action: {
|
|
||||||
reconnect: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
registration_tokens?: {
|
|
||||||
name: string;
|
|
||||||
fields: {
|
|
||||||
token: string;
|
|
||||||
valid: string;
|
|
||||||
uses_allowed: string;
|
|
||||||
pending: string;
|
|
||||||
completed: string;
|
|
||||||
expiry_time: string;
|
|
||||||
length: string;
|
|
||||||
};
|
|
||||||
helper: {
|
|
||||||
length: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,15 +1,14 @@
|
||||||
import italianMessages from "ra-language-italian";
|
import italianMessages from "ra-language-italian";
|
||||||
|
|
||||||
import { SynapseTranslationMessages } from ".";
|
const it = {
|
||||||
|
|
||||||
const it: SynapseTranslationMessages = {
|
|
||||||
...italianMessages,
|
...italianMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "URL dell'homeserver",
|
base_url: "URL dell'homeserver",
|
||||||
welcome: "Benvenuto in Synapse-admin",
|
welcome: "Benvenuto in Synapse-admin",
|
||||||
server_version: "Versione di Synapse",
|
server_version: "Versione di Synapse",
|
||||||
username_error: "Per favore inserisci un ID utente completo: '@utente:dominio'",
|
username_error:
|
||||||
|
"Per favore inserisci un ID utente completo: '@utente:dominio'",
|
||||||
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
||||||
url_error: "URL del server Matrix non valido",
|
url_error: "URL del server Matrix non valido",
|
||||||
sso_sign_in: "Accedi con SSO",
|
sso_sign_in: "Accedi con SSO",
|
||||||
|
@ -33,8 +32,10 @@ const it: SynapseTranslationMessages = {
|
||||||
at_entry: "Alla voce %{entry}: %{message}",
|
at_entry: "Alla voce %{entry}: %{message}",
|
||||||
error: "Errore",
|
error: "Errore",
|
||||||
required_field: "Il campo '%{field}' non è presente",
|
required_field: "Il campo '%{field}' non è presente",
|
||||||
invalid_value: "Valore non valido alla riga %{row}. '%{field}' Il campo può essere solo 'true' o 'false'",
|
invalid_value:
|
||||||
unreasonably_big: "Impossibile caricare un file così grosso (%{size} megabyte)",
|
"Valore non valido alla riga %{row}. '%{field}' Il campo può essere solo 'true' o 'false'",
|
||||||
|
unreasonably_big:
|
||||||
|
"Impossibile caricare un file così grosso (%{size} megabyte)",
|
||||||
already_in_progress: "Un import è attualmente già in caricamento",
|
already_in_progress: "Un import è attualmente già in caricamento",
|
||||||
id_exits: "L'ID %{id} è già presente",
|
id_exits: "L'ID %{id} è già presente",
|
||||||
},
|
},
|
||||||
|
@ -43,9 +44,11 @@ const it: SynapseTranslationMessages = {
|
||||||
cards: {
|
cards: {
|
||||||
importstats: {
|
importstats: {
|
||||||
header: "Importa utenti",
|
header: "Importa utenti",
|
||||||
users_total: "%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV",
|
users_total:
|
||||||
|
"%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV",
|
||||||
guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti",
|
guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti",
|
||||||
admin_count: "%{smart_count} amministratore |||| %{smart_count} amministratori",
|
admin_count:
|
||||||
|
"%{smart_count} amministratore |||| %{smart_count} amministratori",
|
||||||
},
|
},
|
||||||
conflicts: {
|
conflicts: {
|
||||||
header: "Strategia di conflitto",
|
header: "Strategia di conflitto",
|
||||||
|
@ -57,7 +60,8 @@ const it: SynapseTranslationMessages = {
|
||||||
ids: {
|
ids: {
|
||||||
header: "ID",
|
header: "ID",
|
||||||
all_ids_present: "ID presenti in ogni voce",
|
all_ids_present: "ID presenti in ogni voce",
|
||||||
count_ids_present: "%{smart_count} voce con ID |||| %{smart_count} voci con ID",
|
count_ids_present:
|
||||||
|
"%{smart_count} voce con ID |||| %{smart_count} voci con ID",
|
||||||
mode: {
|
mode: {
|
||||||
ignore: "Ignora gli ID nel file CSV e creane di nuovi",
|
ignore: "Ignora gli ID nel file CSV e creane di nuovi",
|
||||||
update: "Aggiorna le voci esistenti",
|
update: "Aggiorna le voci esistenti",
|
||||||
|
@ -66,7 +70,8 @@ const it: SynapseTranslationMessages = {
|
||||||
passwords: {
|
passwords: {
|
||||||
header: "Passwords",
|
header: "Passwords",
|
||||||
all_passwords_present: "Password presenti in ogni voce",
|
all_passwords_present: "Password presenti in ogni voce",
|
||||||
count_passwords_present: "%{smart_count} voce con password |||| %{smart_count} voci con password",
|
count_passwords_present:
|
||||||
|
"%{smart_count} voce con password |||| %{smart_count} voci con password",
|
||||||
use_passwords: "Usa le password dal file CSV",
|
use_passwords: "Usa le password dal file CSV",
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
|
@ -80,11 +85,13 @@ const it: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
header: "Importa i risultati",
|
header: "Importa i risultati",
|
||||||
total: "%{smart_count} voce in totale |||| %{smart_count} voci in totale",
|
total:
|
||||||
|
"%{smart_count} voce in totale |||| %{smart_count} voci in totale",
|
||||||
successful: "%{smart_count} voci importate con successo",
|
successful: "%{smart_count} voci importate con successo",
|
||||||
skipped: "%{smart_count} voci ignorate",
|
skipped: "%{smart_count} voci ignorate",
|
||||||
download_skipped: "Scarica le voci ignorate",
|
download_skipped: "Scarica le voci ignorate",
|
||||||
with_error: "%{smart_count} voce con errori ||| %{smart_count} voci con errori",
|
with_error:
|
||||||
|
"%{smart_count} voce con errori ||| %{smart_count} voci con errori",
|
||||||
simulated_only: "Il processo era stato solamente simulato",
|
simulated_only: "Il processo era stato solamente simulato",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -101,7 +108,6 @@ const it: SynapseTranslationMessages = {
|
||||||
name: "Nome",
|
name: "Nome",
|
||||||
is_guest: "Ospite",
|
is_guest: "Ospite",
|
||||||
admin: "Amministratore",
|
admin: "Amministratore",
|
||||||
locked: "Bloccato",
|
|
||||||
deactivated: "Disattivato",
|
deactivated: "Disattivato",
|
||||||
guests: "Mostra gli ospiti",
|
guests: "Mostra gli ospiti",
|
||||||
show_deactivated: "Mostra gli utenti disattivati",
|
show_deactivated: "Mostra gli utenti disattivati",
|
||||||
|
@ -119,7 +125,8 @@ const it: SynapseTranslationMessages = {
|
||||||
user_type: "Tipo d'utente",
|
user_type: "Tipo d'utente",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
password:
|
||||||
|
"Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
||||||
deactivate: "Devi fornire una password per riattivare l'account.",
|
deactivate: "Devi fornire una password per riattivare l'account.",
|
||||||
erase: "Constrassegna l'utente come cancellato dal GDPR",
|
erase: "Constrassegna l'utente come cancellato dal GDPR",
|
||||||
},
|
},
|
||||||
|
@ -338,7 +345,8 @@ const it: SynapseTranslationMessages = {
|
||||||
guest_can_join: "gli utenti ospite possono entrare",
|
guest_can_join: "gli utenti ospite possono entrare",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
|
title:
|
||||||
|
"Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
|
||||||
content:
|
content:
|
||||||
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
|
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
|
||||||
erase: "Rimuovi dall'elenco",
|
erase: "Rimuovi dall'elenco",
|
|
@ -1,8 +1,6 @@
|
||||||
import chineseMessages from "@haxqer/ra-language-chinese";
|
import chineseMessages from "@haxqer/ra-language-chinese";
|
||||||
|
|
||||||
import { SynapseTranslationMessages } from ".";
|
const zh = {
|
||||||
|
|
||||||
const zh: SynapseTranslationMessages = {
|
|
||||||
...chineseMessages,
|
...chineseMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
|
@ -15,7 +13,8 @@ const zh: SynapseTranslationMessages = {
|
||||||
sso_sign_in: "使用 SSO 登录",
|
sso_sign_in: "使用 SSO 登录",
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
invalid_user_id:
|
||||||
|
"必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@ -25,6 +24,11 @@ const zh: SynapseTranslationMessages = {
|
||||||
detail: "细节",
|
detail: "细节",
|
||||||
permission: "权限",
|
permission: "权限",
|
||||||
},
|
},
|
||||||
|
delete: {
|
||||||
|
title: "删除房间",
|
||||||
|
message:
|
||||||
|
"您确定要删除这个房间吗?该操作无法被撤销。这个房间里所有的消息和分享的媒体都将被从服务器上删除!",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
reports: { tabs: { basic: "基本", detail: "细节" } },
|
reports: { tabs: { basic: "基本", detail: "细节" } },
|
||||||
},
|
},
|
||||||
|
@ -33,7 +37,8 @@ const zh: SynapseTranslationMessages = {
|
||||||
at_entry: "在条目 %{entry}: %{message}",
|
at_entry: "在条目 %{entry}: %{message}",
|
||||||
error: "错误",
|
error: "错误",
|
||||||
required_field: "需要的值 '%{field}' 未被设置。",
|
required_field: "需要的值 '%{field}' 未被设置。",
|
||||||
invalid_value: "第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
|
invalid_value:
|
||||||
|
"第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
|
||||||
unreasonably_big: "拒绝加载过大的文件: %{size} MB",
|
unreasonably_big: "拒绝加载过大的文件: %{size} MB",
|
||||||
already_in_progress: "一个导入进程已经在运行中",
|
already_in_progress: "一个导入进程已经在运行中",
|
||||||
id_exits: "ID %{id} 已经存在",
|
id_exits: "ID %{id} 已经存在",
|
||||||
|
@ -43,7 +48,8 @@ const zh: SynapseTranslationMessages = {
|
||||||
cards: {
|
cards: {
|
||||||
importstats: {
|
importstats: {
|
||||||
header: "导入用户",
|
header: "导入用户",
|
||||||
users_total: "%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
|
users_total:
|
||||||
|
"%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
|
||||||
guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
|
guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
|
||||||
admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
|
admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
|
||||||
},
|
},
|
||||||
|
@ -57,7 +63,8 @@ const zh: SynapseTranslationMessages = {
|
||||||
ids: {
|
ids: {
|
||||||
header: "IDs",
|
header: "IDs",
|
||||||
all_ids_present: "每条记录的 ID",
|
all_ids_present: "每条记录的 ID",
|
||||||
count_ids_present: "%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
|
count_ids_present:
|
||||||
|
"%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
|
||||||
mode: {
|
mode: {
|
||||||
ignore: "忽略 CSV 中的 ID 并创建新的",
|
ignore: "忽略 CSV 中的 ID 并创建新的",
|
||||||
update: "更新已经存在的记录",
|
update: "更新已经存在的记录",
|
||||||
|
@ -66,7 +73,8 @@ const zh: SynapseTranslationMessages = {
|
||||||
passwords: {
|
passwords: {
|
||||||
header: "密码",
|
header: "密码",
|
||||||
all_passwords_present: "每条记录的密码",
|
all_passwords_present: "每条记录的密码",
|
||||||
count_passwords_present: "%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
|
count_passwords_present:
|
||||||
|
"%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
|
||||||
use_passwords: "使用 CSV 中标记的密码",
|
use_passwords: "使用 CSV 中标记的密码",
|
||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
|
@ -84,7 +92,8 @@ const zh: SynapseTranslationMessages = {
|
||||||
successful: "%{smart_count} 条记录导入成功",
|
successful: "%{smart_count} 条记录导入成功",
|
||||||
skipped: "跳过 %{smart_count} 条记录",
|
skipped: "跳过 %{smart_count} 条记录",
|
||||||
download_skipped: "下载跳过的记录",
|
download_skipped: "下载跳过的记录",
|
||||||
with_error: "%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
|
with_error:
|
||||||
|
"%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
|
||||||
simulated_only: "只是一次模拟运行",
|
simulated_only: "只是一次模拟运行",
|
||||||
},
|
},
|
||||||
},
|
},
|
|
@ -1,5 +1,4 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
|
@ -1 +0,0 @@
|
||||||
import "@testing-library/jest-dom";
|
|
3
src/setupTests.js
Normal file
3
src/setupTests.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import fetchMock from "jest-fetch-mock";
|
||||||
|
|
||||||
|
fetchMock.enableMocks();
|
|
@ -1,20 +1,10 @@
|
||||||
import { AuthProvider, Options, fetchUtils } from "react-admin";
|
import { fetchUtils } from "react-admin";
|
||||||
|
|
||||||
const authProvider: AuthProvider = {
|
const authProvider = {
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
login: async ({
|
login: async ({ base_url, username, password, loginToken }) => {
|
||||||
base_url,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
loginToken,
|
|
||||||
}: {
|
|
||||||
base_url: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
loginToken: string;
|
|
||||||
}) => {
|
|
||||||
console.log("login ");
|
console.log("login ");
|
||||||
const options: Options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
@ -55,10 +45,11 @@ const authProvider: AuthProvider = {
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
console.log("logout");
|
console.log("logout");
|
||||||
|
|
||||||
const logout_api_url = localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
|
const logout_api_url =
|
||||||
|
localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
|
||||||
const access_token = localStorage.getItem("access_token");
|
const access_token = localStorage.getItem("access_token");
|
||||||
|
|
||||||
const options: Options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
user: {
|
user: {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
|
@ -72,7 +63,7 @@ const authProvider: AuthProvider = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// called when the API returns an error
|
// called when the API returns an error
|
||||||
checkError: ({ status }: { status: number }) => {
|
checkError: ({ status }) => {
|
||||||
console.log("checkError " + status);
|
console.log("checkError " + status);
|
||||||
if (status === 401 || status === 403) {
|
if (status === 401 || status === 403) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
|
@ -83,7 +74,9 @@ const authProvider: AuthProvider = {
|
||||||
checkAuth: () => {
|
checkAuth: () => {
|
||||||
const access_token = localStorage.getItem("access_token");
|
const access_token = localStorage.getItem("access_token");
|
||||||
console.log("checkAuth " + access_token);
|
console.log("checkAuth " + access_token);
|
||||||
return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
|
return typeof access_token === "string"
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject();
|
||||||
},
|
},
|
||||||
// called when the user navigates to a new location, to check for permissions / roles
|
// called when the user navigates to a new location, to check for permissions / roles
|
||||||
getPermissions: () => Promise.resolve(),
|
getPermissions: () => Promise.resolve(),
|
|
@ -1,18 +1,14 @@
|
||||||
import fetchMock from "jest-fetch-mock";
|
|
||||||
|
|
||||||
import authProvider from "./authProvider";
|
import authProvider from "./authProvider";
|
||||||
|
|
||||||
fetchMock.enableMocks();
|
|
||||||
|
|
||||||
describe("authProvider", () => {
|
describe("authProvider", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.resetMocks();
|
fetch.resetMocks();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("login", () => {
|
describe("login", () => {
|
||||||
it("should successfully login with username and password", async () => {
|
it("should successfully login with username and password", async () => {
|
||||||
fetchMock.once(
|
fetch.once(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
home_server: "example.com",
|
home_server: "example.com",
|
||||||
user_id: "@user:example.com",
|
user_id: "@user:example.com",
|
||||||
|
@ -21,21 +17,24 @@ describe("authProvider", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ret: undefined = await authProvider.login({
|
const ret = await authProvider.login({
|
||||||
base_url: "http://example.com",
|
base_url: "http://example.com",
|
||||||
username: "@user:example.com",
|
username: "@user:example.com",
|
||||||
password: "secret",
|
password: "secret",
|
||||||
});
|
});
|
||||||
|
|
||||||
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: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
Accept: "application/json",
|
Accept: ["application/json"],
|
||||||
"Content-Type": "application/json",
|
"Content-Type": ["application/json"],
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
|
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
|
||||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
||||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
||||||
|
@ -44,7 +43,7 @@ describe("authProvider", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should successfully login with token", async () => {
|
it("should successfully login with token", async () => {
|
||||||
fetchMock.once(
|
fetch.once(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
home_server: "example.com",
|
home_server: "example.com",
|
||||||
user_id: "@user:example.com",
|
user_id: "@user:example.com",
|
||||||
|
@ -53,20 +52,23 @@ describe("authProvider", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ret: undefined = await authProvider.login({
|
const ret = await authProvider.login({
|
||||||
base_url: "https://example.com/",
|
base_url: "https://example.com/",
|
||||||
loginToken: "login_token",
|
loginToken: "login_token",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ret).toBe(undefined);
|
expect(ret).toBe(undefined);
|
||||||
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
|
expect(fetch).toBeCalledWith(
|
||||||
|
"https://example.com/_matrix/client/r0/login",
|
||||||
|
{
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
Accept: "application/json",
|
Accept: ["application/json"],
|
||||||
"Content-Type": "application/json",
|
"Content-Type": ["application/json"],
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
|
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
|
||||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
||||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
||||||
|
@ -77,14 +79,14 @@ describe("authProvider", () => {
|
||||||
it("should remove the access_token from localStorage", async () => {
|
it("should remove the access_token from localStorage", async () => {
|
||||||
localStorage.setItem("base_url", "example.com");
|
localStorage.setItem("base_url", "example.com");
|
||||||
localStorage.setItem("access_token", "foo");
|
localStorage.setItem("access_token", "foo");
|
||||||
fetchMock.mockResponse(JSON.stringify({}));
|
fetch.mockResponse(JSON.stringify({}));
|
||||||
|
|
||||||
await authProvider.logout(null);
|
await authProvider.logout();
|
||||||
|
|
||||||
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
|
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
Accept: "application/json",
|
Accept: ["application/json"],
|
||||||
Authorization: "Bearer foo",
|
Authorization: ["Bearer foo"],
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
user: { authenticated: true, token: "Bearer foo" },
|
user: { authenticated: true, token: "Bearer foo" },
|
||||||
|
@ -95,15 +97,21 @@ describe("authProvider", () => {
|
||||||
|
|
||||||
describe("checkError", () => {
|
describe("checkError", () => {
|
||||||
it("should resolve if error.status is not 401 or 403", async () => {
|
it("should resolve if error.status is not 401 or 403", async () => {
|
||||||
await expect(authProvider.checkError({ status: 200 })).resolves.toBeUndefined();
|
await expect(
|
||||||
|
authProvider.checkError({ status: 200 })
|
||||||
|
).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject if error.status is 401", async () => {
|
it("should reject if error.status is 401", async () => {
|
||||||
await expect(authProvider.checkError({ status: 401 })).rejects.toBeUndefined();
|
await expect(
|
||||||
|
authProvider.checkError({ status: 401 })
|
||||||
|
).rejects.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject if error.status is 403", async () => {
|
it("should reject if error.status is 403", async () => {
|
||||||
await expect(authProvider.checkError({ status: 403 })).rejects.toBeUndefined();
|
await expect(
|
||||||
|
authProvider.checkError({ status: 403 })
|
||||||
|
).rejects.toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -121,7 +129,7 @@ describe("authProvider", () => {
|
||||||
|
|
||||||
describe("getPermissions", () => {
|
describe("getPermissions", () => {
|
||||||
it("should do nothing", async () => {
|
it("should do nothing", async () => {
|
||||||
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
|
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,9 +1,8 @@
|
||||||
|
import { fetchUtils } from "react-admin";
|
||||||
import { stringify } from "query-string";
|
import { stringify } from "query-string";
|
||||||
|
|
||||||
import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
|
||||||
|
|
||||||
// Adds the access token to all requests
|
// Adds the access token to all requests
|
||||||
const jsonClient = (url: string, options: Options = {}) => {
|
const jsonClient = (url, options = {}) => {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
console.log("httpClient " + url);
|
console.log("httpClient " + url);
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
|
@ -15,10 +14,10 @@ const jsonClient = (url: string, options: Options = {}) => {
|
||||||
return fetchUtils.fetchJson(url, options);
|
return fetchUtils.fetchJson(url, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mxcUrlToHttp = (mxcUrl: string) => {
|
const mxcUrlToHttp = mxcUrl => {
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
||||||
const ret = re.exec(mxcUrl);
|
var ret = re.exec(mxcUrl);
|
||||||
console.log("mxcClient " + ret);
|
console.log("mxcClient " + ret);
|
||||||
if (ret == null) return null;
|
if (ret == null) return null;
|
||||||
const serverName = ret[1];
|
const serverName = ret[1];
|
||||||
|
@ -26,188 +25,13 @@ const mxcUrlToHttp = (mxcUrl: string) => {
|
||||||
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`;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Room {
|
|
||||||
room_id: string;
|
|
||||||
name?: string;
|
|
||||||
canonical_alias?: string;
|
|
||||||
avatar_url?: string;
|
|
||||||
joined_members: number;
|
|
||||||
joined_local_members: number;
|
|
||||||
version: number;
|
|
||||||
creator: string;
|
|
||||||
encryption?: string;
|
|
||||||
federatable: boolean;
|
|
||||||
public: boolean;
|
|
||||||
join_rules: "public" | "knock" | "invite" | "private";
|
|
||||||
guest_access?: "can_join" | "forbidden";
|
|
||||||
history_visibility: "invited" | "joined" | "shared" | "world_readable";
|
|
||||||
state_events: number;
|
|
||||||
room_type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RoomState {
|
|
||||||
age: number;
|
|
||||||
content: {
|
|
||||||
alias?: string;
|
|
||||||
};
|
|
||||||
event_id: string;
|
|
||||||
origin_server_ts: number;
|
|
||||||
room_id: string;
|
|
||||||
sender: string;
|
|
||||||
state_key: string;
|
|
||||||
type: string;
|
|
||||||
user_id: string;
|
|
||||||
unsigned: {
|
|
||||||
age?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ForwardExtremity {
|
|
||||||
event_id: string;
|
|
||||||
state_group: number;
|
|
||||||
depth: number;
|
|
||||||
received_ts: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventReport {
|
|
||||||
id: number;
|
|
||||||
received_ts: number;
|
|
||||||
room_id: string;
|
|
||||||
name: string;
|
|
||||||
event_id: string;
|
|
||||||
user_id: string;
|
|
||||||
reason?: string;
|
|
||||||
score?: number;
|
|
||||||
sender: string;
|
|
||||||
canonical_alias?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Threepid {
|
|
||||||
medium: string;
|
|
||||||
address: string;
|
|
||||||
added_at: number;
|
|
||||||
validated_at: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExternalId {
|
|
||||||
auth_provider: string;
|
|
||||||
external_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
name: string;
|
|
||||||
displayname?: string;
|
|
||||||
threepids: Threepid[];
|
|
||||||
avatar_url?: string;
|
|
||||||
is_guest: 0 | 1;
|
|
||||||
admin: 0 | 1;
|
|
||||||
deactivated: 0 | 1;
|
|
||||||
erased: boolean;
|
|
||||||
shadow_banned: 0 | 1;
|
|
||||||
creation_ts: number;
|
|
||||||
appservice_id?: string;
|
|
||||||
consent_server_notice_sent?: string;
|
|
||||||
consent_version?: string;
|
|
||||||
consent_ts?: number;
|
|
||||||
external_ids: ExternalId[];
|
|
||||||
user_type?: string;
|
|
||||||
locked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Device {
|
|
||||||
device_id: string;
|
|
||||||
display_name?: string;
|
|
||||||
last_seen_ip?: string;
|
|
||||||
last_seen_user_agent?: string;
|
|
||||||
last_seen_ts?: number;
|
|
||||||
user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Connection {
|
|
||||||
ip: string;
|
|
||||||
last_seen: number;
|
|
||||||
user_agent: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Whois {
|
|
||||||
user_id: string;
|
|
||||||
devices: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
sessions: {
|
|
||||||
connections: Connection[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Pusher {
|
|
||||||
app_display_name: string;
|
|
||||||
app_id: string;
|
|
||||||
data: {
|
|
||||||
url?: string;
|
|
||||||
format: string;
|
|
||||||
};
|
|
||||||
url: string;
|
|
||||||
format: string;
|
|
||||||
device_display_name: string;
|
|
||||||
profile_tag: string;
|
|
||||||
kind: string;
|
|
||||||
lang: string;
|
|
||||||
pushkey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserMedia {
|
|
||||||
created_ts: number;
|
|
||||||
last_access_ts?: number;
|
|
||||||
media_id: string;
|
|
||||||
media_length: number;
|
|
||||||
media_type: string;
|
|
||||||
quarantined_by?: string;
|
|
||||||
safe_from_quarantine: boolean;
|
|
||||||
upload_name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserMediaStatistic {
|
|
||||||
displayname: string;
|
|
||||||
media_count: number;
|
|
||||||
media_length: number;
|
|
||||||
user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegistrationToken {
|
|
||||||
token: string;
|
|
||||||
uses_allowed: number;
|
|
||||||
pending: number;
|
|
||||||
completed: number;
|
|
||||||
expiry_time?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RaServerNotice {
|
|
||||||
id: string;
|
|
||||||
body: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Destination {
|
|
||||||
destination: string;
|
|
||||||
retry_last_ts: number;
|
|
||||||
retry_interval: number;
|
|
||||||
failure_ts: number;
|
|
||||||
last_successful_stream_ordering?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DestinationRoom {
|
|
||||||
room_id: string;
|
|
||||||
stream_ordering: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
users: {
|
users: {
|
||||||
path: "/_synapse/admin/v2/users",
|
path: "/_synapse/admin/v2/users",
|
||||||
map: (u: User) => ({
|
map: u => ({
|
||||||
...u,
|
...u,
|
||||||
id: u.name,
|
id: u.name,
|
||||||
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
|
avatar_src: mxcUrlToHttp(u.avatar_url),
|
||||||
is_guest: !!u.is_guest,
|
is_guest: !!u.is_guest,
|
||||||
admin: !!u.admin,
|
admin: !!u.admin,
|
||||||
deactivated: !!u.deactivated,
|
deactivated: !!u.deactivated,
|
||||||
|
@ -216,20 +40,24 @@ const resourceMap = {
|
||||||
}),
|
}),
|
||||||
data: "users",
|
data: "users",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
create: (data: RaRecord) => ({
|
create: data => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${localStorage.getItem("home_server")}`,
|
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(
|
||||||
|
data.id
|
||||||
|
)}:${localStorage.getItem("home_server")}`,
|
||||||
body: data,
|
body: data,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(params.id)}`,
|
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
|
||||||
|
params.id
|
||||||
|
)}`,
|
||||||
body: { erase: true },
|
body: { erase: true },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
path: "/_synapse/admin/v1/rooms",
|
path: "/_synapse/admin/v1/rooms",
|
||||||
map: (r: Room) => ({
|
map: r => ({
|
||||||
...r,
|
...r,
|
||||||
id: r.room_id,
|
id: r.room_id,
|
||||||
alias: r.canonical_alias,
|
alias: r.canonical_alias,
|
||||||
|
@ -239,98 +67,121 @@ const resourceMap = {
|
||||||
public: !!r.public,
|
public: !!r.public,
|
||||||
}),
|
}),
|
||||||
data: "rooms",
|
data: "rooms",
|
||||||
total: json => json.total_rooms,
|
total: json => {
|
||||||
delete: (params: DeleteParams) => ({
|
return json.total_rooms;
|
||||||
|
},
|
||||||
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
|
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
|
||||||
body: { block: false },
|
body: { block: false },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
reports: {
|
reports: {
|
||||||
path: "/_synapse/admin/v1/event_reports",
|
path: "/_synapse/admin/v1/event_reports",
|
||||||
map: (er: EventReport) => ({ ...er }),
|
map: er => ({
|
||||||
|
...er,
|
||||||
|
id: er.id,
|
||||||
|
}),
|
||||||
data: "event_reports",
|
data: "event_reports",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
},
|
},
|
||||||
devices: {
|
devices: {
|
||||||
map: (d: Device) => ({
|
map: d => ({
|
||||||
...d,
|
...d,
|
||||||
id: d.device_id,
|
id: d.device_id,
|
||||||
}),
|
}),
|
||||||
data: "devices",
|
data: "devices",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
reference: (id: Identifier) => ({
|
return json.total;
|
||||||
|
},
|
||||||
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
|
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(params.previousData.user_id)}/devices/${params.id}`,
|
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
|
||||||
|
params.previousData.user_id
|
||||||
|
)}/devices/${params.id}`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
path: "/_synapse/admin/v1/whois",
|
path: "/_synapse/admin/v1/whois",
|
||||||
map: (c: Whois) => ({
|
map: c => ({
|
||||||
...c,
|
...c,
|
||||||
id: c.user_id,
|
id: c.user_id,
|
||||||
}),
|
}),
|
||||||
data: "connections",
|
data: "connections",
|
||||||
},
|
},
|
||||||
room_members: {
|
room_members: {
|
||||||
map: (m: string) => ({
|
map: m => ({
|
||||||
id: m,
|
id: m,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/rooms/${id}/members`,
|
endpoint: `/_synapse/admin/v1/rooms/${id}/members`,
|
||||||
}),
|
}),
|
||||||
data: "members",
|
data: "members",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
|
return json.total;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
room_state: {
|
room_state: {
|
||||||
map: (rs: RoomState) => ({
|
map: rs => ({
|
||||||
...rs,
|
...rs,
|
||||||
id: rs.event_id,
|
id: rs.event_id,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/rooms/${id}/state`,
|
endpoint: `/_synapse/admin/v1/rooms/${id}/state`,
|
||||||
}),
|
}),
|
||||||
data: "state",
|
data: "state",
|
||||||
total: json => json.state.length,
|
total: json => {
|
||||||
|
return json.state.length;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
pushers: {
|
pushers: {
|
||||||
map: (p: Pusher) => ({
|
map: p => ({
|
||||||
...p,
|
...p,
|
||||||
id: p.pushkey,
|
id: p.pushkey,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
|
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
|
||||||
}),
|
}),
|
||||||
data: "pushers",
|
data: "pushers",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
|
return json.total;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
joined_rooms: {
|
joined_rooms: {
|
||||||
map: (jr: string) => ({
|
map: jr => ({
|
||||||
id: jr,
|
id: jr,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/joined_rooms`,
|
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
|
||||||
|
id
|
||||||
|
)}/joined_rooms`,
|
||||||
}),
|
}),
|
||||||
data: "joined_rooms",
|
data: "joined_rooms",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
|
return json.total;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users_media: {
|
users_media: {
|
||||||
map: (um: UserMedia) => ({
|
map: um => ({
|
||||||
...um,
|
...um,
|
||||||
id: um.media_id,
|
id: um.media_id,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
|
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
|
||||||
}),
|
}),
|
||||||
data: "media",
|
data: "media",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
delete: (params: DeleteParams) => ({
|
return json.total;
|
||||||
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
|
},
|
||||||
|
delete: params => ({
|
||||||
|
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
|
||||||
|
"home_server"
|
||||||
|
)}/${params.id}`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
delete_media: {
|
delete_media: {
|
||||||
delete: (params: DeleteParams) => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
|
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
|
||||||
"home_server"
|
"home_server"
|
||||||
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
|
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
|
||||||
|
@ -340,30 +191,34 @@ const resourceMap = {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
protect_media: {
|
protect_media: {
|
||||||
map: (pm: UserMedia) => ({ id: pm.media_id }),
|
map: pm => ({ id: pm.media_id }),
|
||||||
create: (params: UserMedia) => ({
|
create: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
|
endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
|
endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
quarantine_media: {
|
quarantine_media: {
|
||||||
map: (qm: UserMedia) => ({ id: qm.media_id }),
|
map: qm => ({ id: qm.media_id }),
|
||||||
create: (params: UserMedia) => ({
|
create: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem("home_server")}/${params.media_id}`,
|
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem(
|
||||||
|
"home_server"
|
||||||
|
)}/${params.media_id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem("home_server")}/${params.id}`,
|
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
|
||||||
|
"home_server"
|
||||||
|
)}/${params.id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
servernotices: {
|
servernotices: {
|
||||||
map: (n: { event_id: string }) => ({ id: n.event_id }),
|
map: n => ({ id: n.event_id }),
|
||||||
create: (data: RaServerNotice) => ({
|
create: data => ({
|
||||||
endpoint: "/_synapse/admin/v1/send_server_notice",
|
endpoint: "/_synapse/admin/v1/send_server_notice",
|
||||||
body: {
|
body: {
|
||||||
user_id: data.id,
|
user_id: data.id,
|
||||||
|
@ -377,44 +232,50 @@ const resourceMap = {
|
||||||
},
|
},
|
||||||
user_media_statistics: {
|
user_media_statistics: {
|
||||||
path: "/_synapse/admin/v1/statistics/users/media",
|
path: "/_synapse/admin/v1/statistics/users/media",
|
||||||
map: (usms: UserMediaStatistic) => ({
|
map: usms => ({
|
||||||
...usms,
|
...usms,
|
||||||
id: usms.user_id,
|
id: usms.user_id,
|
||||||
}),
|
}),
|
||||||
data: "users",
|
data: "users",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
|
return json.total;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
forward_extremities: {
|
forward_extremities: {
|
||||||
map: (fe: ForwardExtremity) => ({
|
map: fe => ({
|
||||||
...fe,
|
...fe,
|
||||||
id: fe.event_id,
|
id: fe.event_id,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
|
endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
|
||||||
}),
|
}),
|
||||||
data: "results",
|
data: "results",
|
||||||
total: json => json.count,
|
total: json => {
|
||||||
delete: (params: DeleteParams) => ({
|
return json.count;
|
||||||
|
},
|
||||||
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
|
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: Room) => ({
|
map: rd => ({
|
||||||
...rd,
|
...rd,
|
||||||
id: rd.room_id,
|
id: rd.room_id,
|
||||||
public: !!rd.public,
|
public: !!rd.public,
|
||||||
guest_access: !!rd.guest_access,
|
guest_access: !!rd.guest_access,
|
||||||
avatar_src: rd.avatar_url ? mxcUrlToHttp(rd.avatar_url) : undefined,
|
avatar_src: mxcUrlToHttp(rd.avatar_url),
|
||||||
}),
|
}),
|
||||||
data: "chunk",
|
data: "chunk",
|
||||||
total: json => json.total_room_count_estimate,
|
total: json => {
|
||||||
create: (params: RaRecord) => ({
|
return json.total_room_count_estimate;
|
||||||
|
},
|
||||||
|
create: params => ({
|
||||||
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
|
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
|
||||||
body: { visibility: "public" },
|
body: { visibility: "public" },
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: params => ({
|
||||||
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
|
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
|
||||||
body: { visibility: "private" },
|
body: { visibility: "private" },
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
@ -422,49 +283,54 @@ const resourceMap = {
|
||||||
},
|
},
|
||||||
destinations: {
|
destinations: {
|
||||||
path: "/_synapse/admin/v1/federation/destinations",
|
path: "/_synapse/admin/v1/federation/destinations",
|
||||||
map: (dst: Destination) => ({
|
map: dst => ({
|
||||||
...dst,
|
...dst,
|
||||||
id: dst.destination,
|
id: dst.destination,
|
||||||
}),
|
}),
|
||||||
data: "destinations",
|
data: "destinations",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
|
return json.total;
|
||||||
|
},
|
||||||
delete: params => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
|
endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
destination_rooms: {
|
destination_rooms: {
|
||||||
map: (dstroom: DestinationRoom) => ({
|
map: dstroom => ({
|
||||||
...dstroom,
|
...dstroom,
|
||||||
id: dstroom.room_id,
|
id: dstroom.room_id,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
|
endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
|
||||||
}),
|
}),
|
||||||
data: "rooms",
|
data: "rooms",
|
||||||
total: json => json.total,
|
total: json => {
|
||||||
|
return json.total;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
registration_tokens: {
|
registration_tokens: {
|
||||||
path: "/_synapse/admin/v1/registration_tokens",
|
path: "/_synapse/admin/v1/registration_tokens",
|
||||||
map: (rt: RegistrationToken) => ({
|
map: rt => ({
|
||||||
...rt,
|
...rt,
|
||||||
id: rt.token,
|
id: rt.token,
|
||||||
}),
|
}),
|
||||||
data: "registration_tokens",
|
data: "registration_tokens",
|
||||||
total: json => json.registration_tokens.length,
|
total: json => {
|
||||||
create: (params: RaRecord) => ({
|
return json.registration_tokens.length;
|
||||||
|
},
|
||||||
|
create: params => ({
|
||||||
endpoint: "/_synapse/admin/v1/registration_tokens/new",
|
endpoint: "/_synapse/admin/v1/registration_tokens/new",
|
||||||
body: params,
|
body: params,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: params => ({
|
||||||
endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
|
endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
function filterNullValues(key, value) {
|
||||||
function filterNullValues(key: string, value: any) {
|
|
||||||
// Filtering out null properties
|
// Filtering out null properties
|
||||||
// to reset user_type from user, it must be null
|
// to reset user_type from user, it must be null
|
||||||
if (value === null && key !== "user_type") {
|
if (value === null && key !== "user_type") {
|
||||||
|
@ -473,7 +339,7 @@ function filterNullValues(key: string, value: any) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSearchOrder(order: "ASC" | "DESC") {
|
function getSearchOrder(order) {
|
||||||
if (order === "DESC") {
|
if (order === "DESC") {
|
||||||
return "b";
|
return "b";
|
||||||
} else {
|
} else {
|
||||||
|
@ -481,10 +347,18 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataProvider: DataProvider = {
|
const dataProvider = {
|
||||||
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,
|
||||||
|
search_term,
|
||||||
|
destination,
|
||||||
|
valid,
|
||||||
|
} = params.filter;
|
||||||
const { page, perPage } = params.pagination;
|
const { page, perPage } = params.pagination;
|
||||||
const { field, order } = params.sort;
|
const { field, order } = params.sort;
|
||||||
const from = (page - 1) * perPage;
|
const from = (page - 1) * perPage;
|
||||||
|
@ -502,7 +376,7 @@ const dataProvider: DataProvider = {
|
||||||
dir: getSearchOrder(order),
|
dir: getSearchOrder(order),
|
||||||
};
|
};
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
|
@ -519,24 +393,30 @@ const dataProvider: DataProvider = {
|
||||||
getOne: async (resource, params) => {
|
getOne: async (resource, params) => {
|
||||||
console.log("getOne " + resource);
|
console.log("getOne " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`);
|
const { json } = await jsonClient(
|
||||||
|
`${endpoint_url}/${encodeURIComponent(params.id)}`
|
||||||
|
);
|
||||||
return { data: res.map(json) };
|
return { data: res.map(json) };
|
||||||
},
|
},
|
||||||
|
|
||||||
getMany: async (resource, params) => {
|
getMany: async (resource, params) => {
|
||||||
console.log("getMany " + resource);
|
console.log("getMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const responses = await Promise.all(params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)));
|
const responses = await Promise.all(
|
||||||
|
params.ids.map(id =>
|
||||||
|
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
|
||||||
|
)
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
data: responses.map(({ json }) => res.map(json)),
|
data: responses.map(({ json }) => res.map(json)),
|
||||||
total: responses.length,
|
total: responses.length,
|
||||||
|
@ -556,11 +436,11 @@ const dataProvider: DataProvider = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const ref = res.reference(params.id);
|
const ref = res["reference"](params.id);
|
||||||
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
||||||
|
|
||||||
const { json } = await jsonClient(endpoint_url);
|
const { json } = await jsonClient(endpoint_url);
|
||||||
|
@ -573,31 +453,37 @@ const dataProvider: DataProvider = {
|
||||||
update: async (resource, params) => {
|
update: async (resource, params) => {
|
||||||
console.log("update " + resource);
|
console.log("update " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
|
const { json } = await jsonClient(
|
||||||
|
`${endpoint_url}/${encodeURIComponent(params.id)}`,
|
||||||
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return { data: res.map(json) };
|
return { data: res.map(json) };
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMany: async (resource, params) => {
|
updateMany: async (resource, params) => {
|
||||||
console.log("updateMany " + resource);
|
console.log("updateMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const responses = await Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`), {
|
params.ids.map(
|
||||||
|
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
|
||||||
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return { data: responses.map(({ json }) => json) };
|
return { data: responses.map(({ json }) => json) };
|
||||||
},
|
},
|
||||||
|
@ -605,12 +491,12 @@ const dataProvider: DataProvider = {
|
||||||
create: async (resource, params) => {
|
create: async (resource, params) => {
|
||||||
console.log("create " + resource);
|
console.log("create " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
if (!("create" in res)) return Promise.reject();
|
if (!("create" in res)) return Promise.reject();
|
||||||
|
|
||||||
const create = res.create(params.data);
|
const create = res["create"](params.data);
|
||||||
const endpoint_url = homeserver + create.endpoint;
|
const endpoint_url = homeserver + create.endpoint;
|
||||||
const { json } = await jsonClient(endpoint_url, {
|
const { json } = await jsonClient(endpoint_url, {
|
||||||
method: create.method,
|
method: create.method,
|
||||||
|
@ -619,18 +505,18 @@ const dataProvider: DataProvider = {
|
||||||
return { data: res.map(json) };
|
return { data: res.map(json) };
|
||||||
},
|
},
|
||||||
|
|
||||||
createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
|
createMany: async (resource, params) => {
|
||||||
console.log("createMany " + resource);
|
console.log("createMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
if (!("create" in res)) throw Error(`Create ${resource} is not allowed`);
|
if (!("create" in res)) return Promise.reject();
|
||||||
|
|
||||||
const responses = await Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(id => {
|
params.ids.map(id => {
|
||||||
params.data.id = id;
|
params.data.id = id;
|
||||||
const cre = res.create(params.data);
|
const cre = res["create"](params.data);
|
||||||
const endpoint_url = homeserver + cre.endpoint;
|
const endpoint_url = homeserver + cre.endpoint;
|
||||||
return jsonClient(endpoint_url, {
|
return jsonClient(endpoint_url, {
|
||||||
method: cre.method,
|
method: cre.method,
|
||||||
|
@ -644,12 +530,12 @@ const dataProvider: DataProvider = {
|
||||||
delete: async (resource, params) => {
|
delete: async (resource, params) => {
|
||||||
console.log("delete " + resource);
|
console.log("delete " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
if ("delete" in res) {
|
if ("delete" in res) {
|
||||||
const del = res.delete(params);
|
const del = res["delete"](params);
|
||||||
const endpoint_url = homeserver + del.endpoint;
|
const endpoint_url = homeserver + del.endpoint;
|
||||||
const { json } = await jsonClient(endpoint_url, {
|
const { json } = await jsonClient(endpoint_url, {
|
||||||
method: "method" in del ? del.method : "DELETE",
|
method: "method" in del ? del.method : "DELETE",
|
||||||
|
@ -669,14 +555,14 @@ const dataProvider: DataProvider = {
|
||||||
deleteMany: async (resource, params) => {
|
deleteMany: async (resource, params) => {
|
||||||
console.log("deleteMany " + resource);
|
console.log("deleteMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = localStorage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
if ("delete" in res) {
|
if ("delete" in res) {
|
||||||
const responses = await Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map(id => {
|
params.ids.map(id => {
|
||||||
const del = res.delete({ ...params, id: id });
|
const del = res["delete"]({ ...params, id: id });
|
||||||
const endpoint_url = homeserver + del.endpoint;
|
const endpoint_url = homeserver + del.endpoint;
|
||||||
return jsonClient(endpoint_url, {
|
return jsonClient(endpoint_url, {
|
||||||
method: "method" in del ? del.method : "DELETE",
|
method: "method" in del ? del.method : "DELETE",
|
||||||
|
@ -693,7 +579,7 @@ const dataProvider: DataProvider = {
|
||||||
params.ids.map(id =>
|
params.ids.map(id =>
|
||||||
jsonClient(`${endpoint_url}/${id}`, {
|
jsonClient(`${endpoint_url}/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
// body: JSON.stringify(params.data, filterNullValues), @FIXME
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
|
@ -1,11 +1,7 @@
|
||||||
import fetchMock from "jest-fetch-mock";
|
|
||||||
|
|
||||||
import dataProvider from "./dataProvider";
|
import dataProvider from "./dataProvider";
|
||||||
|
|
||||||
fetchMock.enableMocks();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.resetMocks();
|
fetch.resetMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("dataProvider", () => {
|
describe("dataProvider", () => {
|
||||||
|
@ -13,7 +9,7 @@ describe("dataProvider", () => {
|
||||||
localStorage.setItem("access_token", "access_token");
|
localStorage.setItem("access_token", "access_token");
|
||||||
|
|
||||||
it("fetches all users", async () => {
|
it("fetches all users", async () => {
|
||||||
fetchMock.mockResponseOnce(
|
fetch.mockResponseOnce(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
users: [
|
users: [
|
||||||
{
|
{
|
||||||
|
@ -46,13 +42,13 @@ describe("dataProvider", () => {
|
||||||
filter: { author_id: 12 },
|
filter: { author_id: 12 },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(users.data[0].id).toEqual("user_id1");
|
expect(users["data"][0]["id"]).toEqual("user_id1");
|
||||||
expect(users.total).toEqual(200);
|
expect(users["total"]).toEqual(200);
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fetches one user", async () => {
|
it("fetches one user", async () => {
|
||||||
fetchMock.mockResponseOnce(
|
fetch.mockResponseOnce(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name: "user_id1",
|
name: "user_id1",
|
||||||
password: "user_password",
|
password: "user_password",
|
||||||
|
@ -75,8 +71,8 @@ describe("dataProvider", () => {
|
||||||
|
|
||||||
const user = await dataProvider.getOne("users", { id: "user_id1" });
|
const user = await dataProvider.getOne("users", { id: "user_id1" });
|
||||||
|
|
||||||
expect(user.data.id).toEqual("user_id1");
|
expect(user["data"]["id"]).toEqual("user_id1");
|
||||||
expect(user.data.displayname).toEqual("User");
|
expect(user["data"]["displayname"]).toEqual("User");
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,11 +1,13 @@
|
||||||
import { fetchUtils } from "react-admin";
|
import { fetchUtils } from "react-admin";
|
||||||
|
|
||||||
export const splitMxid = mxid => {
|
export const splitMxid = mxid => {
|
||||||
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
const re =
|
||||||
|
/^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
||||||
return re.exec(mxid)?.groups;
|
return re.exec(mxid)?.groups;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidBaseUrl = baseUrl => /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
|
export const isValidBaseUrl = baseUrl =>
|
||||||
|
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the homeserver URL using the well-known lookup
|
* Resolve the homeserver URL using the well-known lookup
|
||||||
|
@ -56,27 +58,3 @@ export const getMediaUrl = media_id => {
|
||||||
const baseUrl = localStorage.getItem("base_url");
|
const baseUrl = localStorage.getItem("base_url");
|
||||||
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a random MXID for current homeserver
|
|
||||||
* @returns full MXID as string
|
|
||||||
*/
|
|
||||||
export function generateRandomMxId(): string {
|
|
||||||
const homeserver = localStorage.getItem("home_server");
|
|
||||||
const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
||||||
const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
|
|
||||||
.map(x => characters[x % characters.length])
|
|
||||||
.join("");
|
|
||||||
return `@${localpart}:${homeserver}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a random user password
|
|
||||||
* @returns a new random password as string
|
|
||||||
*/
|
|
||||||
export function generateRandomPassword(length = 20): string {
|
|
||||||
const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$";
|
|
||||||
return Array.from(crypto.getRandomValues(new Uint32Array(length)))
|
|
||||||
.map(x => characters[x % characters.length])
|
|
||||||
.join("");
|
|
||||||
}
|
|
31
src/synapse/synapse.test.js
Normal file
31
src/synapse/synapse.test.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { isValidBaseUrl, splitMxid } from "./synapse";
|
||||||
|
|
||||||
|
describe("splitMxid", () => {
|
||||||
|
it("splits valid MXIDs", () =>
|
||||||
|
expect(splitMxid("@name:domain.tld")).toEqual({
|
||||||
|
name: "name",
|
||||||
|
domain: "domain.tld",
|
||||||
|
}));
|
||||||
|
it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValidBaseUrl", () => {
|
||||||
|
it("accepts a http URL", () =>
|
||||||
|
expect(isValidBaseUrl("http://foo.bar")).toBeTruthy());
|
||||||
|
it("accepts a https URL", () =>
|
||||||
|
expect(isValidBaseUrl("https://foo.bar")).toBeTruthy());
|
||||||
|
it("accepts a valid URL with port", () =>
|
||||||
|
expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy());
|
||||||
|
it("rejects undefined base URLs", () =>
|
||||||
|
expect(isValidBaseUrl(undefined)).toBeFalsy());
|
||||||
|
it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy());
|
||||||
|
it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy());
|
||||||
|
it("rejects non-string base URLs", () =>
|
||||||
|
expect(isValidBaseUrl({})).toBeFalsy());
|
||||||
|
it("rejects base URLs without protocol", () =>
|
||||||
|
expect(isValidBaseUrl("foo.bar")).toBeFalsy());
|
||||||
|
it("rejects base URLs with path", () =>
|
||||||
|
expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy());
|
||||||
|
it("rejects invalid base URLs", () =>
|
||||||
|
expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy());
|
||||||
|
});
|
|
@ -1,23 +0,0 @@
|
||||||
import { isValidBaseUrl, splitMxid } from "./synapse";
|
|
||||||
|
|
||||||
describe("splitMxid", () => {
|
|
||||||
it("splits valid MXIDs", () =>
|
|
||||||
expect(splitMxid("@name:domain.tld")).toEqual({
|
|
||||||
name: "name",
|
|
||||||
domain: "domain.tld",
|
|
||||||
}));
|
|
||||||
it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined());
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isValidBaseUrl", () => {
|
|
||||||
it("accepts a http URL", () => expect(isValidBaseUrl("http://foo.bar")).toBeTruthy());
|
|
||||||
it("accepts a https URL", () => expect(isValidBaseUrl("https://foo.bar")).toBeTruthy());
|
|
||||||
it("accepts a valid URL with port", () => expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy());
|
|
||||||
it("rejects undefined base URLs", () => expect(isValidBaseUrl(undefined)).toBeFalsy());
|
|
||||||
it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy());
|
|
||||||
it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy());
|
|
||||||
it("rejects non-string base URLs", () => expect(isValidBaseUrl({})).toBeFalsy());
|
|
||||||
it("rejects base URLs without protocol", () => expect(isValidBaseUrl("foo.bar")).toBeFalsy());
|
|
||||||
it("rejects base URLs with path", () => expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy());
|
|
||||||
it("rejects invalid base URLs", () => expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy());
|
|
||||||
});
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["./**/*.ts", "./**/*.tsx"]
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
// prettier-ignore
|
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
/* Basic Options */
|
|
||||||
"target": "ESNext" /* Specify ECMAScript target version */,
|
|
||||||
"module": "ESNext" /* Specify module code generation */,
|
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"] /* Specify library files to be included in the compilation. */,
|
|
||||||
"allowJs": false /* Allow javascript files to be compiled. */,
|
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
|
||||||
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
|
||||||
"declaration": true /* Generates corresponding '.d.ts' file. */,
|
|
||||||
"declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
|
|
||||||
"sourceMap": true /* Generates corresponding '.map' file. */,
|
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
|
||||||
// "outDir": "./lib", /* Redirect output structure to the directory. */
|
|
||||||
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
|
||||||
// "composite": true, /* Enable project compilation */
|
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
|
||||||
"noEmit": true, /* Do not emit outputs. */
|
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
|
||||||
"isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
|
||||||
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
|
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
|
||||||
|
|
||||||
/* Additional Checks */
|
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
|
||||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
|
||||||
|
|
||||||
/* Module Resolution Options */
|
|
||||||
"moduleResolution": "Bundler" /* Specify module resolution strategy */,
|
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
|
||||||
"types": ["vite/client"], /* Type declaration files to be included in compilation. */
|
|
||||||
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
|
|
||||||
"esModuleInterop": false /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
|
|
||||||
/* Source Map Options */
|
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
|
||||||
|
|
||||||
/* Experimental Options */
|
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
|
||||||
"skipLibCheck": false
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"references": [{ "path": "./tsconfig.vite.json" }]
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node"
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { vitePluginVersionMark } from "vite-plugin-version-mark";
|
|
||||||
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { vitePluginVersionMark } from "vite-plugin-version-mark";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
Loading…
Reference in a new issue