Update eslint for typescript

Change-Id: I39ad44666fe958dd4f6c8f0d88b3dc960d7cb6c7
This commit is contained in:
Manuel Stahl 2024-04-22 15:36:13 +02:00
parent 72f5ab937e
commit 4761ea36bc
39 changed files with 781 additions and 2795 deletions

13
.editorconfig Normal file
View file

@ -0,0 +1,13 @@
# 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

View file

@ -17,5 +17,7 @@ jobs:
node-version: "18"
- name: Install dependencies
run: yarn --immutable
- name: Run checks
run: yarn lint
- name: Run tests
run: yarn test

View file

@ -1 +1,2 @@
.vscode
.yarn

View file

@ -1,11 +0,0 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid"
}

View file

@ -104,10 +104,7 @@ or to a list of homeservers:
```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"]
}
```
@ -166,5 +163,6 @@ services:
## Development
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
- Use `yarn test` to run all style, lint and unit tests
- Use `yarn lint` to run all style and linter checks
- Use `yarn test` to run all unit tests
- Use `yarn fix` to fix the coding style

View file

@ -12,6 +12,7 @@
},
"packageManager": "yarn@4.1.1",
"devDependencies": {
"@eslint/js": "^9.1.1",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.2",
@ -21,11 +22,16 @@
"@types/node": "^20.12.7",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.2.79",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unused-imports": "^3.1.0",
"eslint-plugin-yaml": "^0.5.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
@ -34,6 +40,7 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.7.1",
"vite": "^5.0.0",
"vite-plugin-version-mark": "^0.0.13"
},
@ -66,17 +73,79 @@
"scripts": {
"start": "vite serve",
"build": "vite build",
"fix:other": "yarn prettier --write",
"fix:code": "yarn test:lint --fix",
"fix": "yarn fix:code && yarn fix:other",
"prettier": "prettier \"**/*.{js,jsx,ts,tsx,json,md,scss,yaml,yml}\"",
"test:code": "jest",
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx,.ts,.jsx .",
"test:style": "yarn prettier --check",
"test": "yarn test:style && yarn test:lint && yarn test:code"
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx,.yml,.yaml .",
"fix": "yarn lint --fix",
"test": "yarn jest",
"test:watch": "yarn jest --watch"
},
"eslintConfig": {
"extends": "react-app"
"env": {
"browser": true
},
"plugins": [
"import",
"prettier",
"unused-imports",
"@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": {
"production": [

View file

@ -1,4 +1,5 @@
import { render, screen } from "@testing-library/react";
import App from "./App";
describe("App", () => {

View file

@ -1,28 +1,25 @@
import {
Admin,
CustomRoutes,
Resource,
resolveBrowserLocale,
} from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { merge } from "lodash";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
import users from "./components/users";
import rooms from "./components/rooms";
import userMediaStats from "./components/statistics";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
import { Route } from "react-router-dom";
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 registrationToken from "./components/RegistrationTokens";
import LoginPage from "./components/LoginPage";
import { ImportFeature } from "./components/ImportFeature";
import { Route } from "react-router-dom";
import rooms from "./components/rooms";
import userMediaStats from "./components/statistics";
import users from "./components/users";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr";
import chineseMessages from "./i18n/zh";
import italianMessages from "./i18n/it";
import chineseMessages from "./i18n/zh";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
@ -33,8 +30,7 @@ const messages = {
zh: chineseMessages,
};
const i18nProvider = polyglotI18nProvider(
locale =>
messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en,
locale => (messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en),
resolveBrowserLocale(),
[
{ locale: "en", name: "English" },

View file

@ -1,5 +1,6 @@
import { RecordContextProvider } from "react-admin";
import { render, screen } from "@testing-library/react";
import { RecordContextProvider } from "react-admin";
import AvatarField from "./AvatarField";
describe("AvatarField", () => {

View file

@ -1,4 +1,5 @@
import { get } from "lodash";
import { Avatar } from "@mui/material";
import { useRecordContext } from "react-admin";
@ -6,16 +7,7 @@ const AvatarField = ({ source, ...rest }) => {
const record = useRecordContext(rest);
const src = get(record, source)?.toString();
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;

View file

@ -1,3 +1,6 @@
import PageviewIcon from "@mui/icons-material/Pageview";
import ViewListIcon from "@mui/icons-material/ViewList";
import ReportIcon from "@mui/icons-material/Warning";
import {
Datagrid,
DateField,
@ -17,15 +20,11 @@ import {
useRecordContext,
useTranslate,
} from "react-admin";
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";
import { date_format } from "./date";
const ReportPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
import { DATE_FORMAT } from "./date";
import { MXCField } from "./media";
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
export const ReportShow = (props: ShowProps) => {
const translate = useTranslate();
@ -38,43 +37,21 @@ export const ReportShow = (props: ShowProps) => {
})}
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">
<TextField source="id" />
</ReferenceField>
<NumberField source="score" />
<TextField source="reason" />
<TextField source="name" />
<TextField
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="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" />
</ReferenceField>
</Tab>
<Tab
label="synapseadmin.reports.tabs.detail"
icon={<PageviewIcon />}
path="detail"
>
<DateField
source="event_json.origin_server_ts"
showTime
options={date_format}
sortable={true}
/>
<Tab 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">
<TextField source="id" />
</ReferenceField>
@ -89,10 +66,7 @@ export const ReportShow = (props: ShowProps) => {
<TextField source="event_json.content.format" />
<TextField source="event_json.content.formatted_body" />
<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>
</TabbedShowLayout>
</Show>
@ -115,19 +89,10 @@ const ReportShowActions = () => {
};
export const ReportList = (props: ListProps) => (
<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}>
<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="name" />
<TextField sortable={false} source="score" />

View file

@ -1,10 +1,6 @@
import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
import { ChangeEvent, useState } from "react";
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
import {
parse as parseCsv,
unparse as unparseCsv,
ParseResult,
} from "papaparse";
import {
Button,
Card,
@ -17,6 +13,8 @@ import {
NativeSelect,
} from "@mui/material";
import { DataProvider, useTranslate } from "ra-core";
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
import { generateRandomMxId, generateRandomPassword } from "../synapse/synapse";
const LOGGING = true;
@ -121,21 +119,12 @@ const FilePicker = () => {
}
};
const verifyCsv = (
{ data, meta, errors }: ParseResult<ImportLine>,
{ setValues, setStats, setError }
) => {
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
/* First, verify the presence of required fields */
const missingFields = expectedFields.filter(eF =>
meta.fields?.find(mF => eF === mF)
);
const missingFields = expectedFields.filter(eF => meta.fields?.find(mF => eF === mF));
if (missingFields.length > 0) {
setError(
translate("import_users.error.required_field", {
field: missingFields[0],
})
);
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
return false;
}
@ -157,7 +146,7 @@ const FilePicker = () => {
total: data.length,
};
var errorMessages = errors.map(e => e.message);
const errorMessages = errors.map(e => e.message);
data.forEach((line, idx) => {
if (line.user_type === undefined || line.user_type === "") {
stats.user_types.default++;
@ -305,10 +294,7 @@ const FilePicker = () => {
* We do a simple retry loop so that an accidental hit on an existing ID
* doesn't trip us up.
*/
if (LOGGING)
console.log(
"will check for existence of record " + JSON.stringify(userRecord)
);
if (LOGGING) console.log("will check for existence of record " + JSON.stringify(userRecord));
let retries = 0;
const submitRecord = (recordData: ImportLine) => {
return dataProvider.getOne("users", { id: recordData.id }).then(
@ -337,14 +323,7 @@ const FilePicker = () => {
}
},
async () => {
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) {
await dataProvider.create("users", { data: recordData });
@ -429,28 +408,11 @@ const FilePicker = () => {
const statsCards = stats &&
!importResults && [
<Container>
<CardHeader
title={translate("import_users.cards.importstats.header")}
/>
<CardHeader title={translate("import_users.cards.importstats.header")} />
<CardContent>
<div>
{translate(
"import_users.cards.importstats.users_total",
stats.total
)}
</div>
<div>
{translate(
"import_users.cards.importstats.guest_count",
stats.is_guest
)}
</div>
<div>
{translate(
"import_users.cards.importstats.admin_count",
stats.admin
)}
</div>
<div>{translate("import_users.cards.importstats.users_total", stats.total)}</div>
<div>{translate("import_users.cards.importstats.guest_count", stats.is_guest)}</div>
<div>{translate("import_users.cards.importstats.admin_count", stats.admin)}</div>
</CardContent>
</Container>,
<Container>
@ -463,19 +425,9 @@ const FilePicker = () => {
</div>
{stats.id > 0 ? (
<div>
<NativeSelect
onChange={onUseridModeChanged}
value={useridMode}
disabled={progress !== null}
>
<TranslatableOption
value="ignore"
text="import_users.cards.ids.mode.ignore"
/>
<TranslatableOption
value="update"
text="import_users.cards.ids.mode.update"
/>
<NativeSelect onChange={onUseridModeChanged} value={useridMode} disabled={progress !== null}>
<TranslatableOption value="ignore" text="import_users.cards.ids.mode.ignore" />
<TranslatableOption value="update" text="import_users.cards.ids.mode.update" />
</NativeSelect>
</div>
) : (
@ -489,20 +441,13 @@ const FilePicker = () => {
<div>
{stats.password === stats.total
? 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>
{stats.password > 0 ? (
<div>
<FormControlLabel
control={
<Checkbox
checked={passwordMode}
disabled={progress !== null}
onChange={onPasswordModeChange}
/>
<Checkbox checked={passwordMode} disabled={progress !== null} onChange={onPasswordModeChange} />
}
label={translate("import_users.cards.passwords.use_passwords")}
/>
@ -519,19 +464,9 @@ const FilePicker = () => {
<CardHeader title={translate("import_users.cards.conflicts.header")} />
<CardContent>
<div>
<NativeSelect
onChange={onConflictModeChanged}
value={conflictMode}
disabled={progress !== null}
>
<TranslatableOption
value="stop"
text="import_users.cards.conflicts.mode.stop"
/>
<TranslatableOption
value="skip"
text="import_users.cards.conflicts.mode.skip"
/>
<NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
<TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
<TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
</NativeSelect>
</div>
</CardContent>
@ -557,11 +492,7 @@ const FilePicker = () => {
<a href="./data/example.csv">example.csv</a>
<br />
<br />
<input
type="file"
onChange={onFileChange}
disabled={progress !== null}
/>
<input type="file" onChange={onFileChange} disabled={progress !== null} />
</CardContent>
</Container>
);
@ -570,22 +501,13 @@ const FilePicker = () => {
<CardContent>
<CardHeader title={translate("import_users.cards.results.header")} />
<div>
{translate(
"import_users.cards.results.total",
importResults.totalRecordCount
)}
{translate("import_users.cards.results.total", importResults.totalRecordCount)}
<br />
{translate(
"import_users.cards.results.successful",
importResults.succeededRecords.length
)}
{translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
<br />
{importResults.skippedRecords.length
? [
translate(
"import_users.cards.results.skipped",
importResults.skippedRecords.length
),
translate("import_users.cards.results.skipped", importResults.skippedRecords.length),
<div>
<button onClick={downloadSkippedRecords}>
{translate("import_users.cards.results.download_skipped")}
@ -595,19 +517,10 @@ const FilePicker = () => {
]
: ""}
{importResults.erroredRecords.length
? [
translate(
"import_users.cards.results.skipped",
importResults.erroredRecords.length
),
<br />,
]
? [translate("import_users.cards.results.skipped", importResults.erroredRecords.length), <br />]
: ""}
<br />
{importResults.wasDryRun && [
translate("import_users.cards.results.simulated_only"),
<br />,
]}
{importResults.wasDryRun && [translate("import_users.cards.results.simulated_only"), <br />]}
</div>
</CardContent>
);
@ -616,13 +529,7 @@ const FilePicker = () => {
!values || values.length === 0 || importResults ? undefined : (
<CardActions>
<FormControlLabel
control={
<Checkbox
checked={dryRun}
onChange={onDryRunModeChanged}
disabled={progress !== null}
/>
}
control={<Checkbox checked={dryRun} onChange={onDryRunModeChanged} disabled={progress !== null} />}
label={translate("import_users.cards.startImport.simulate_only")}
/>
<Button size="large" onClick={runImport} disabled={progress !== null}>
@ -646,10 +553,7 @@ const FilePicker = () => {
const cardContainer = <Card>{allCards}</Card>;
return [
<Title defaultTitle={translate("import_users.title")} />,
cardContainer,
];
return [<Title defaultTitle={translate("import_users.title")} />, cardContainer];
};
export const ImportFeature = FilePicker;

View file

@ -1,13 +1,13 @@
import polyglotI18nProvider from "ra-i18n-polyglot";
import { render, screen } from "@testing-library/react";
import { AdminContext } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import LoginPage from "./LoginPage";
import { AppContext } from "../AppContext";
import englishMessages from "../i18n/en";
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [
{ locale: "en", name: "English" },
]);
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
describe("LoginForm", () => {
it("renders with no restriction to homeserver", () => {
@ -30,9 +30,7 @@ describe("LoginForm", () => {
it("renders with single restricted homeserver", () => {
render(
<AppContext.Provider
value={{ restrictBaseUrl: "https://matrix.example.com" }}
>
<AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com" }}>
<AdminContext i18nProvider={i18nProvider}>
<LoginPage />
</AdminContext>
@ -54,10 +52,7 @@ describe("LoginForm", () => {
render(
<AppContext.Provider
value={{
restrictBaseUrl: [
"https://matrix.example.com",
"https://matrix.example.org",
],
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
}}
>
<AdminContext i18nProvider={i18nProvider}>

View file

@ -1,4 +1,8 @@
import { 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 {
Form,
FormDataConsumer,
@ -13,19 +17,6 @@ import {
useLocales,
} from "react-admin";
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 {
@ -103,9 +94,7 @@ const LoginPage = () => {
const [locale, setLocale] = useLocaleState();
const locales = useLocales();
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 loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
@ -113,11 +102,7 @@ const LoginPage = () => {
const ssoToken = loginToken[1];
console.log("SSO token is", ssoToken);
// 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");
localStorage.removeItem("sso_base_url");
if (baseUrl) {
@ -146,9 +131,7 @@ const LoginPage = () => {
const validateBaseUrl = value => {
if (!value.match(/^(http|https):\/\//)) {
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");
} else {
return undefined;
@ -189,10 +172,7 @@ const LoginPage = () => {
const domain = splitMxid(formData.username)?.domain;
if (domain) {
getWellKnownUrl(domain).then(url => {
if (
allowAnyBaseUrl ||
(allowMultipleBaseUrls && restrictBaseUrl.includes(url))
)
if (allowAnyBaseUrl || (allowMultipleBaseUrls && restrictBaseUrl.includes(url)))
form.setValue("base_url", url);
});
}
@ -205,28 +185,20 @@ const LoginPage = () => {
if (!isValidBaseUrl(formData.base_url)) return;
getServerVersion(formData.base_url)
.then(serverVersion =>
setServerVersion(
`${translate("synapseadmin.auth.server_version")} ${serverVersion}`
)
)
.then(serverVersion => setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`))
.catch(() => setServerVersion(""));
getSupportedFeatures(formData.base_url)
.then(features =>
setMatrixVersions(
`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
)
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`)
)
.catch(() => setMatrixVersions(""));
// Set SSO Url
getSupportedLoginFlows(formData.base_url)
.then(loginFlows => {
const supportPass =
loginFlows.find(f => f.type === "m.login.password") !== undefined;
const supportSSO =
loginFlows.find(f => f.type === "m.login.sso") !== undefined;
const supportPass = loginFlows.find(f => f.type === "m.login.password") !== undefined;
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
setSupportPassAuth(supportPass);
setSSOBaseUrl(supportSSO ? formData.base_url : "");
})
@ -287,11 +259,7 @@ const LoginPage = () => {
};
return (
<Form
defaultValues={{ base_url: base_url }}
onSubmit={handleSubmit}
mode="onTouched"
>
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
<FormBox>
<Card className="card">
<Box className="avatar">
@ -318,9 +286,7 @@ const LoginPage = () => {
</MenuItem>
))}
</Select>
<FormDataConsumer>
{formDataProps => <UserData {...formDataProps} />}
</FormDataConsumer>
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
<CardActions className="actions">
<Button
variant="contained"

View file

@ -1,3 +1,4 @@
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
import {
BooleanInput,
Create,
@ -21,8 +22,8 @@ import {
TextField,
Toolbar,
} from "react-admin";
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
import { date_format, dateFormatter, dateParser } from "./date";
import { DATE_FORMAT, dateFormatter, dateParser } from "./date";
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
const validateUsesAllowed = [number()];
@ -43,12 +44,7 @@ export const RegistrationTokenList = (props: ListProps) => (
<NumberField source="uses_allowed" sortable={false} />
<NumberField source="pending" 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>
</List>
);
@ -63,23 +59,14 @@ export const RegistrationTokenCreate = (props: CreateProps) => (
</Toolbar>
}
>
<TextInput
source="token"
autoComplete="off"
validate={validateToken}
resettable
/>
<TextInput source="token" autoComplete="off" validate={validateToken} resettable />
<NumberInput
source="length"
validate={validateLength}
helperText="resources.registration_tokens.helper.length"
step={1}
/>
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<NumberInput source="uses_allowed" validate={validateUsesAllowed} step={1} />
<DateTimeInput source="expiry_time" parse={dateParser} />
</SimpleForm>
</Create>
@ -91,16 +78,8 @@ export const RegistrationTokenEdit = (props: EditProps) => (
<TextInput source="token" disabled />
<NumberInput source="pending" disabled />
<NumberInput source="completed" disabled />
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<DateTimeInput
source="expiry_time"
parse={dateParser}
format={dateFormatter}
/>
<NumberInput source="uses_allowed" validate={validateUsesAllowed} step={1} />
<DateTimeInput source="expiry_time" parse={dateParser} format={dateFormatter} />
</SimpleForm>
</Edit>
);

View file

@ -1,3 +1,4 @@
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
import {
BooleanField,
BulkDeleteButton,
@ -25,12 +26,10 @@ import {
useUnselectAll,
} from "react-admin";
import { useMutation } from "react-query";
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
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) => {
const translate = useTranslate();
@ -53,9 +52,7 @@ export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
);
};
export const RoomDirectoryBulkUnpublishButton = (
props: BulkDeleteButtonProps
) => (
export const RoomDirectoryBulkUnpublishButton = (props: BulkDeleteButtonProps) => (
<BulkDeleteButton
{...props}
label="resources.room_directory.action.erase"
@ -93,12 +90,7 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
);
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 />
</Button>
);
@ -128,12 +120,7 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
};
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 />
</Button>
);
@ -147,11 +134,7 @@ const RoomDirectoryListActions = () => (
);
export const RoomDirectoryList = () => (
<List
pagination={<RoomDirectoryPagination />}
perPage={100}
actions={<RoomDirectoryListActions />}
>
<List pagination={<RoomDirectoryPagination />} perPage={100} actions={<RoomDirectoryListActions />}>
<DatagridConfigurable
rowClick={id => "/rooms/" + id + "/show"}
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
@ -163,41 +146,13 @@ export const RoomDirectoryList = () => (
sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar"
/>
<TextField
source="name"
sortable={false}
label="resources.rooms.fields.name"
/>
<TextField
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"
/>
<TextField source="name" sortable={false} label="resources.rooms.fields.name" />
<TextField 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>
</List>
);

View file

@ -1,4 +1,8 @@
import { 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 {
Button,
RaRecord,
@ -17,26 +21,13 @@ import {
useUnselectAll,
} from "react-admin";
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 translate = useTranslate();
const ServerNoticeToolbar = (
props: ToolbarProps & { pristine?: boolean }
) => (
const ServerNoticeToolbar = (props: ToolbarProps & { pristine?: boolean }) => (
<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}>
<IconCancel />
</Button>
@ -45,13 +36,9 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>
{translate("resources.servernotices.action.send")}
</DialogTitle>
<DialogTitle>{translate("resources.servernotices.action.send")}</DialogTitle>
<DialogContent>
<DialogContentText>
{translate("resources.servernotices.helper.send")}
</DialogContentText>
<DialogContentText>{translate("resources.servernotices.helper.send")}</DialogContentText>
<SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
<TextInput
source="body"
@ -96,18 +83,10 @@ export const ServerNoticeButton = () => {
return (
<>
<Button
label="resources.servernotices.send"
onClick={handleDialogOpen}
disabled={isLoading}
>
<Button label="resources.servernotices.send" onClick={handleDialogOpen} disabled={isLoading}>
<MessageIcon />
</Button>
<ServerNoticeDialog
open={open}
onClose={handleDialogClose}
onSubmit={handleSend}
/>
<ServerNoticeDialog open={open} onClose={handleDialogClose} onSubmit={handleSend} />
</>
);
};
@ -142,18 +121,10 @@ export const ServerNoticeBulkButton = () => {
return (
<>
<Button
label="resources.servernotices.send"
onClick={openDialog}
disabled={isLoading}
>
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
<MessageIcon />
</Button>
<ServerNoticeDialog
open={open}
onClose={closeDialog}
onSubmit={sendNotices}
/>
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
</>
);
};

View file

@ -1,4 +1,4 @@
export const date_format: Intl.DateTimeFormatOptions = {
export const DATE_FORMAT: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "2-digit",
day: "2-digit",
@ -12,9 +12,7 @@ export const dateParser = (v: string | number | Date): number => {
return d.getTime();
};
export const dateFormatter = (
v: string | number | Date | undefined | null
): string => {
export const dateFormatter = (v: string | number | Date | undefined | null): string => {
if (v === undefined || v === null) return "";
const d = new Date(v);

View file

@ -29,11 +29,9 @@ import {
useTranslate,
} from "react-admin";
import { date_format } from "./date";
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 destinationRowSx = (record: RaRecord) => ({
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
@ -72,11 +70,7 @@ export const DestinationReconnectButton = () => {
};
return (
<Button
label="resources.destinations.action.reconnect"
onClick={handleClick}
disabled={isLoading}
>
<Button label="resources.destinations.action.reconnect" onClick={handleClick} disabled={isLoading}>
<AutorenewIcon />
</Button>
);
@ -106,14 +100,10 @@ export const DestinationList = (props: ListProps) => {
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
>
<Datagrid
rowSx={destinationRowSx}
rowClick={id => `${id}/show/rooms`}
bulkActionButtons={false}
>
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
<DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
<DestinationReconnectButton />
@ -125,25 +115,17 @@ export const DestinationList = (props: ListProps) => {
export const DestinationShow = (props: ShowProps) => {
const translate = useTranslate();
return (
<Show
actions={<DestinationShowActions />}
title={<DestinationTitle />}
{...props}
>
<Show actions={<DestinationShowActions />} title={<DestinationTitle />} {...props}>
<TabbedShowLayout>
<Tab label="status" icon={<ViewListIcon />}>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
<DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
</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
reference="destination_rooms"
target="destination"
@ -151,14 +133,8 @@ export const DestinationShow = (props: ShowProps) => {
pagination={<DestinationPagination />}
perPage={50}
>
<Datagrid
style={{ width: "100%" }}
rowClick={id => `/rooms/${id}/show`}
>
<TextField
source="room_id"
label="resources.rooms.fields.room_id"
/>
<Datagrid style={{ width: "100%" }} rowClick={id => `/rooms/${id}/show`}>
<TextField source="room_id" label="resources.rooms.fields.room_id" />
<TextField source="stream_ordering" sortable={false} />
<ReferenceField
label="resources.rooms.fields.name"

View file

@ -1,8 +1,4 @@
import {
DeleteWithConfirmButton,
DeleteWithConfirmButtonProps,
useRecordContext,
} from "react-admin";
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
const record = useRecordContext();

View file

@ -1,5 +1,15 @@
import { useState } from "react";
import { get } from "lodash";
import { useState } from "react";
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 {
BooleanInput,
Button,
@ -18,22 +28,7 @@ import {
useTranslate,
} from "react-admin";
import { Link } from "react-router-dom";
import BlockIcon from "@mui/icons-material/Block";
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 { dateParser } from "./date";
import { getMediaUrl } from "../synapse/synapse";
@ -42,10 +37,7 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
const DeleteMediaToolbar = (props: ToolbarProps) => (
<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}>
<IconCancel />
</Button>
@ -54,13 +46,9 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>
{translate("resources.delete_media.action.send")}
</DialogTitle>
<DialogTitle>{translate("resources.delete_media.action.send")}</DialogTitle>
<DialogContent>
<DialogContentText>
{translate("resources.delete_media.helper.send")}
</DialogContentText>
<DialogContentText>{translate("resources.delete_media.helper.send")}</DialogContentText>
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
<DateTimeInput
fullWidth
@ -98,11 +86,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const deleteMedia = (values: {
before_ts: string;
size_gt: number;
keep_profiles: boolean;
}) => {
const deleteMedia = (values: { before_ts: string; size_gt: number; keep_profiles: boolean }) => {
deleteOne(
"delete_media",
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
@ -140,11 +124,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
>
<DeleteSweepIcon />
</Button>
<DeleteMediaDialog
open={open}
onClose={closeDialog}
onSubmit={deleteMedia}
/>
<DeleteMediaDialog open={open} onClose={closeDialog} onSubmit={deleteMedia} />
</>
);
};
@ -313,11 +293,7 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
})}
>
<div>
<Button
{...props}
onClick={handleRemoveQuarantaine}
disabled={isLoading}
>
<Button {...props} onClick={handleRemoveQuarantaine} disabled={isLoading}>
<BlockIcon color="error" />
</Button>
</div>

View file

@ -1,3 +1,14 @@
import EventIcon from "@mui/icons-material/Event";
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 {
BooleanField,
BulkDeleteButton,
@ -26,28 +37,16 @@ import {
useRecordContext,
useTranslate,
} 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 {
RoomDirectoryBulkUnpublishButton,
RoomDirectoryBulkPublishButton,
RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton,
} from "./RoomDirectory";
import { date_format } from "./date";
import { DATE_FORMAT } from "./date";
const RoomPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const RoomTitle = () => {
const record = useRecordContext();
@ -66,11 +65,7 @@ const RoomTitle = () => {
const RoomShowActions = () => {
const record = useRecordContext();
const publishButton = record.public ? (
<RoomDirectoryUnpublishButton />
) : (
<RoomDirectoryPublishButton />
);
const publishButton = record.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
// FIXME: refresh after (un)publish
return (
<TopToolbar>
@ -99,42 +94,19 @@ export const RoomShow = (props: ShowProps) => {
</ReferenceField>
</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_local_members" />
<TextField source="joined_local_devices" />
<TextField source="state_events" />
<TextField source="version" />
<TextField
source="encryption"
emptyText={translate("resources.rooms.enums.unencrypted")}
/>
<TextField source="encryption" emptyText={translate("resources.rooms.enums.unencrypted")} />
</Tab>
<Tab
label="synapseadmin.rooms.tabs.members"
icon={<UserIcon />}
path="members"
>
<ReferenceManyField
reference="room_members"
target="room_id"
label={false}
>
<Datagrid
style={{ width: "100%" }}
rowClick={id => "/users/" + id}
bulkActionButtons={false}
>
<TextField
source="id"
sortable={false}
label="resources.users.fields.id"
/>
<Tab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />} path="members">
<ReferenceManyField reference="room_members" target="room_id" label={false}>
<Datagrid style={{ width: "100%" }} rowClick={id => "/users/" + id} bulkActionButtons={false}>
<TextField source="id" sortable={false} label="resources.users.fields.id" />
<ReferenceField
label="resources.users.fields.displayname"
source="id"
@ -148,11 +120,7 @@ export const RoomShow = (props: ShowProps) => {
</ReferenceManyField>
</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="public" />
<SelectField
@ -203,41 +171,20 @@ export const RoomShow = (props: ShowProps) => {
/>
</Tab>
<Tab
label={translate("resources.room_state.name", { smart_count: 2 })}
icon={<EventIcon />}
path="state"
>
<ReferenceManyField
reference="room_state"
target="room_id"
label={false}
>
<Tab label={translate("resources.room_state.name", { smart_count: 2 })} icon={<EventIcon />} path="state">
<ReferenceManyField reference="room_state" target="room_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons={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} />
<ReferenceField
source="sender"
reference="users"
sortable={false}
>
<ReferenceField source="sender" reference="users" sortable={false}>
<TextField source="id" />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</Tab>
<Tab
label="resources.forward_extremities.name"
icon={<FastForwardIcon />}
path="forward_extremities"
>
<Tab label="resources.forward_extremities.name" icon={<FastForwardIcon />} path="forward_extremities">
<Box
sx={{
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
@ -246,19 +193,10 @@ export const RoomShow = (props: ShowProps) => {
>
{translate("resources.rooms.helper.forward_extremities")}
</Box>
<ReferenceManyField
reference="forward_extremities"
target="room_id"
label={false}
>
<ReferenceManyField reference="forward_extremities" target="room_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons={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} />
<TextField source="state_group" sortable={false} />
</Datagrid>
@ -304,12 +242,7 @@ export const RoomList = (props: ListProps) => {
<DatagridConfigurable
rowClick="show"
bulkActionButtons={<RoomBulkActionButtons />}
omit={[
"joined_local_members",
"state_events",
"version",
"federatable",
]}
omit={["joined_local_members", "state_events", "version", "federatable"]}
>
<BooleanField
source="is_encrypted"
@ -322,12 +255,7 @@ export const RoomList = (props: ListProps) => {
[`& [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_local_members" />
<TextField source="state_events" />

View file

@ -25,9 +25,7 @@ const ListActions = () => {
);
};
const UserMediaStatsPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const UserMediaStatsPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
@ -39,15 +37,9 @@ export const UserMediaStatsList = (props: ListProps) => (
pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }}
>
<Datagrid
rowClick={id => "/users/" + id + "/media"}
bulkActionButtons={false}
>
<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"
/>
<TextField source="displayname" label="resources.users.fields.displayname" />
<NumberField source="media_count" />
<NumberField source="media_length" />
</Datagrid>

View file

@ -2,11 +2,11 @@ import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import ContactMailIcon from "@mui/icons-material/ContactMail";
import DevicesIcon from "@mui/icons-material/Devices";
import GetAppIcon from "@mui/icons-material/GetApp";
import UserIcon from "@mui/icons-material/Group";
import NotificationsIcon from "@mui/icons-material/Notifications";
import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import UserIcon from "@mui/icons-material/Group";
import ViewListIcon from "@mui/icons-material/ViewList";
import {
ArrayInput,
@ -49,15 +49,12 @@ import {
useListContext,
} from "react-admin";
import { Link } from "react-router-dom";
import AvatarField from "./AvatarField";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DATE_FORMAT } from "./date";
import { DeviceRemoveButton } from "./devices";
import {
MediaIDField,
ProtectMediaButton,
QuarantineMediaButton,
} from "./media";
import { date_format } from "./date";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "./media";
const choices_medium = [
{ id: "email", name: "resources.users.email" },
@ -87,18 +84,12 @@ UserListActions.defaultProps = {
onUnselectItems: () => null,
};
const UserPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const userFilters = [
<SearchInput source="name" 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 = () => (
@ -122,22 +113,13 @@ export const UserList = (props: ListProps) => (
pagination={<UserPagination />}
>
<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="displayname" />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<BooleanField source="deactivated" />
<DateField
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
options={date_format}
/>
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
</Datagrid>
</List>
);
@ -146,11 +128,7 @@ export const UserList = (props: ListProps) => (
// here only local part of user_id
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
// 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)];
@ -177,36 +155,19 @@ export const UserCreate = (props: CreateProps) => (
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" validate={maxLength(256)} />
<PasswordInput
source="password"
autoComplete="new-password"
validate={maxLength(512)}
/>
<SelectInput
source="user_type"
choices={choices_type}
translateChoice={false}
resettable
/>
<PasswordInput source="password" autoComplete="new-password" validate={maxLength(512)} />
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
<BooleanInput source="admin" />
<ArrayInput source="threepids">
<SimpleFormIterator disableReordering>
<SelectInput
source="medium"
choices={choices_medium}
validate={required()}
/>
<SelectInput source="medium" choices={choices_medium} validate={required()} />
<TextInput source="address" validate={validateAddress} />
</SimpleFormIterator>
</ArrayInput>
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
<SimpleFormIterator disableReordering>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
validate={required()}
/>
<TextInput source="external_id" label="resources.users.fields.id" validate={required()} />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
@ -231,42 +192,19 @@ export const UserEdit = (props: EditProps) => {
return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm>
<FormTab
label={translate("resources.users.name", { smart_count: 1 })}
icon={<PersonPinIcon />}
>
<AvatarField
source="avatar_src"
sortable={false}
sx={{ height: "120px", width: "120px", float: "right" }}
/>
<FormTab 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="displayname" />
<PasswordInput
source="password"
autoComplete="new-password"
helperText="resources.users.helper.password"
/>
<SelectInput
source="user_type"
choices={choices_type}
translateChoice={false}
resettable
/>
<PasswordInput 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="deactivated"
helperText="resources.users.helper.deactivate"
/>
<DateField source="creation_ts_ms" showTime options={date_format} />
<BooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
<TextField source="consent_version" />
</FormTab>
<FormTab
label="resources.users.threepid"
icon={<ContactMailIcon />}
path="threepid"
>
<FormTab label="resources.users.threepid" icon={<ContactMailIcon />} path="threepid">
<ArrayInput source="threepids">
<SimpleFormIterator disableReordering>
<SelectInput source="medium" choices={choices_medium} />
@ -275,76 +213,34 @@ export const UserEdit = (props: EditProps) => {
</ArrayInput>
</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}>
<SimpleFormIterator disableReordering>
<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>
</ArrayInput>
</FormTab>
<FormTab
label={translate("resources.devices.name", { smart_count: 2 })}
icon={<DevicesIcon />}
path="devices"
>
<ReferenceManyField
reference="devices"
target="user_id"
label={false}
>
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
<ReferenceManyField reference="devices" target="user_id" label={false}>
<Datagrid style={{ width: "100%" }}>
<TextField source="device_id" sortable={false} />
<TextField source="display_name" 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 />
</Datagrid>
</ReferenceManyField>
</FormTab>
<FormTab
label="resources.connections.name"
icon={<SettingsInputComponentIcon />}
path="connections"
>
<ReferenceField
reference="connections"
source="id"
label={false}
link={false}
>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
>
<FormTab label="resources.connections.name" icon={<SettingsInputComponentIcon />} path="connections">
<ReferenceField reference="connections" source="id" label={false} link={false}>
<ArrayField source="devices[].sessions[0].connections" label="resources.connections.name">
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="ip" sortable={false} />
<DateField
source="last_seen"
showTime
options={date_format}
sortable={false}
/>
<TextField
source="user_agent"
sortable={false}
style={{ width: "100%" }}
/>
<DateField source="last_seen" showTime options={DATE_FORMAT} sortable={false} />
<TextField source="user_agent" sortable={false} style={{ width: "100%" }} />
</Datagrid>
</ArrayField>
</ReferenceField>
@ -365,12 +261,8 @@ export const UserEdit = (props: EditProps) => {
>
<Datagrid style={{ width: "100%" }}>
<MediaIDField source="media_id" />
<DateField source="created_ts" showTime options={date_format} />
<DateField
source="last_access_ts"
showTime
options={date_format}
/>
<DateField source="created_ts" showTime options={DATE_FORMAT} />
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
<NumberField source="media_length" />
<TextField source="media_type" />
<TextField source="upload_name" />
@ -382,26 +274,10 @@ export const UserEdit = (props: EditProps) => {
</ReferenceManyField>
</FormTab>
<FormTab
label={translate("resources.rooms.name", { smart_count: 2 })}
icon={<ViewListIcon />}
path="rooms"
>
<ReferenceManyField
reference="joined_rooms"
target="user_id"
label={false}
>
<Datagrid
style={{ width: "100%" }}
rowClick={id => "/rooms/" + id + "/show"}
bulkActionButtons={false}
>
<TextField
source="id"
sortable={false}
label="resources.rooms.fields.room_id"
/>
<FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms">
<ReferenceManyField reference="joined_rooms" target="user_id" label={false}>
<Datagrid style={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}>
<TextField source="id" sortable={false} label="resources.rooms.fields.room_id" />
<ReferenceField
label="resources.rooms.fields.name"
source="id"
@ -420,11 +296,7 @@ export const UserEdit = (props: EditProps) => {
icon={<NotificationsIcon />}
path="pushers"
>
<ReferenceManyField
reference="pushers"
target="user_id"
label={false}
>
<ReferenceManyField reference="pushers" target="user_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="kind" sortable={false} />
<TextField source="app_display_name" sortable={false} />

View file

@ -1,4 +1,5 @@
import { formalGermanMessages } from "@haleos/ra-language-german";
import { SynapseTranslationMessages } from ".";
const de: SynapseTranslationMessages = {
@ -45,11 +46,9 @@ const de: SynapseTranslationMessages = {
cards: {
importstats: {
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",
admin_count:
"%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
admin_count: "%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
},
conflicts: {
header: "Konfliktstrategie",
@ -61,8 +60,7 @@ const de: SynapseTranslationMessages = {
ids: {
header: "IDs",
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: {
ignore: "Ignoriere IDs der CSV-Datei und erstelle neue",
update: "Aktualisiere existierende Benutzer",
@ -71,8 +69,7 @@ const de: SynapseTranslationMessages = {
passwords: {
header: "Passwörter",
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",
},
upload: {
@ -86,13 +83,11 @@ const de: SynapseTranslationMessages = {
},
results: {
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",
skipped: "%{smart_count} Einträge übersprungen",
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",
},
},
@ -126,10 +121,8 @@ const de: SynapseTranslationMessages = {
user_type: "Benutzertyp",
},
helper: {
password:
"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.",
password: "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",
},
action: {
@ -360,8 +353,7 @@ const de: SynapseTranslationMessages = {
guest_can_join: "Gastbenutzer dürfen beitreten",
},
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:
"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",

View file

@ -1,4 +1,5 @@
import englishMessages from "ra-language-english";
import { SynapseTranslationMessages } from ".";
const en: SynapseTranslationMessages = {
@ -34,10 +35,8 @@ const en: SynapseTranslationMessages = {
at_entry: "At entry %{entry}: %{message}",
error: "Error",
required_field: "Required field '%{field}' is not present",
invalid_value:
"Invalid value on line %{row}. '%{field}' field may only be 'true' or 'false'",
unreasonably_big:
"Refused to load unreasonably big file of %{size} megabytes",
invalid_value: "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",
id_exits: "ID %{id} already present",
},
@ -46,8 +45,7 @@ const en: SynapseTranslationMessages = {
cards: {
importstats: {
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",
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
},
@ -61,8 +59,7 @@ const en: SynapseTranslationMessages = {
ids: {
header: "IDs",
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: {
ignore: "Ignore IDs in CSV and create new ones",
update: "Update existing records",
@ -71,8 +68,7 @@ const en: SynapseTranslationMessages = {
passwords: {
header: "Passwords",
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",
},
upload: {
@ -86,13 +82,11 @@ const en: SynapseTranslationMessages = {
},
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",
skipped: "%{smart_count} entries skipped",
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",
},
},
@ -217,8 +211,7 @@ const en: SynapseTranslationMessages = {
action: {
erase: {
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.",
},
},
},
@ -359,8 +352,7 @@ const en: SynapseTranslationMessages = {
guest_can_join: "guest users may join",
},
action: {
title:
"Delete room from directory |||| Delete %{smart_count} rooms from directory",
title: "Delete room from directory |||| Delete %{smart_count} rooms from directory",
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?",
erase: "Delete from room directory",

View file

@ -1,4 +1,5 @@
import farsiMessages from "ra-language-farsi";
import { SynapseTranslationMessages } from ".";
const fa: SynapseTranslationMessages = {
@ -32,10 +33,8 @@ const fa: SynapseTranslationMessages = {
at_entry: "در هنگام ورود %{entry}: %{message}",
error: "Error",
required_field: "فیلد الزامی '%{field}' وجود ندارد",
invalid_value:
"خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
unreasonably_big:
"از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
invalid_value: "خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
unreasonably_big: "از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
already_in_progress: "یک بارگذاری از قبل در حال انجام است",
id_exits: "شناسه %{id} موجود است",
},
@ -44,8 +43,7 @@ const fa: SynapseTranslationMessages = {
cards: {
importstats: {
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",
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
},
@ -59,8 +57,7 @@ const fa: SynapseTranslationMessages = {
ids: {
header: "شناسنامه ها",
all_ids_present: "شناسه های موجود در هر ورودی",
count_ids_present:
"%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
count_ids_present: "%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
mode: {
ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
update: "سوابق موجود را به روز کنید",
@ -69,8 +66,7 @@ const fa: SynapseTranslationMessages = {
passwords: {
header: "رمز عبور",
all_passwords_present: "رمزهای عبور موجود در هر ورودی",
count_passwords_present:
"%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
count_passwords_present: "%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
use_passwords: "از پسوردهای CSV استفاده کنید",
},
upload: {
@ -88,8 +84,7 @@ const fa: SynapseTranslationMessages = {
successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
download_skipped: "دانلود رکوردهای نادیده گرفته شده",
with_error:
"%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
with_error: "%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
simulated_only: "اجرا فقط شبیه سازی شد",
},
},
@ -227,8 +222,7 @@ const fa: SynapseTranslationMessages = {
action: {
erase: {
title: "حذف کردن %{id}",
content:
'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
content: 'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
success: "دستگاه با موفقیت حذف شد.",
failure: "خطایی رخ داده است.",
},
@ -343,8 +337,7 @@ const fa: SynapseTranslationMessages = {
guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
},
action: {
title:
"اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
title: "اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
content:
"آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{smart_count} از راهنمای اتاق ها حذف کنید؟",
erase: "حذف از فهرست اتاق",

View file

@ -1,4 +1,5 @@
import frenchMessages from "ra-language-french";
import { SynapseTranslationMessages } from ".";
const fr: SynapseTranslationMessages = {
@ -8,15 +9,13 @@ const fr: SynapseTranslationMessages = {
base_url: "URL du serveur daccueil",
welcome: "Bienvenue sur Synapse-admin",
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:// »",
url_error: "L'URL du serveur Matrix n'est pas valide",
sso_sign_in: "Se connecter avec lauthentification unique",
},
users: {
invalid_user_id:
"Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.",
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.",
tabs: { sso: "Authentification unique" },
},
rooms: {
@ -36,8 +35,7 @@ const fr: SynapseTranslationMessages = {
required_field: "Le champ requis « %{field} » est manquant",
invalid_value:
"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",
id_exits: "L'identifiant %{id} déjà présent",
},
@ -49,8 +47,7 @@ const fr: SynapseTranslationMessages = {
users_total:
"%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
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: {
header: "Stratégie de résolution des conflits",
@ -62,11 +59,9 @@ const fr: SynapseTranslationMessages = {
ids: {
header: "Identifiants",
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: {
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",
},
},
@ -88,13 +83,11 @@ const fr: SynapseTranslationMessages = {
},
results: {
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",
skipped: "%{smart_count} 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é",
},
},
@ -127,8 +120,7 @@ const fr: SynapseTranslationMessages = {
auth_provider: "Fournisseur d'identité",
},
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",
},
action: {
@ -342,13 +334,11 @@ const fr: SynapseTranslationMessages = {
room_directory: {
name: "Répertoire des salons",
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",
},
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:
"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",
@ -369,8 +359,7 @@ const fr: SynapseTranslationMessages = {
length: "Longueur",
},
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é",
},
},
},

View file

@ -1,4 +1,5 @@
import italianMessages from "ra-language-italian";
import { SynapseTranslationMessages } from ".";
const it: SynapseTranslationMessages = {
@ -8,8 +9,7 @@ const it: SynapseTranslationMessages = {
base_url: "URL dell'homeserver",
welcome: "Benvenuto in Synapse-admin",
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://'",
url_error: "URL del server Matrix non valido",
sso_sign_in: "Accedi con SSO",
@ -33,10 +33,8 @@ const it: SynapseTranslationMessages = {
at_entry: "Alla voce %{entry}: %{message}",
error: "Errore",
required_field: "Il campo '%{field}' non è presente",
invalid_value:
"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)",
invalid_value: "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",
id_exits: "L'ID %{id} è già presente",
},
@ -45,11 +43,9 @@ const it: SynapseTranslationMessages = {
cards: {
importstats: {
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",
admin_count:
"%{smart_count} amministratore |||| %{smart_count} amministratori",
admin_count: "%{smart_count} amministratore |||| %{smart_count} amministratori",
},
conflicts: {
header: "Strategia di conflitto",
@ -61,8 +57,7 @@ const it: SynapseTranslationMessages = {
ids: {
header: "ID",
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: {
ignore: "Ignora gli ID nel file CSV e creane di nuovi",
update: "Aggiorna le voci esistenti",
@ -71,8 +66,7 @@ const it: SynapseTranslationMessages = {
passwords: {
header: "Passwords",
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",
},
upload: {
@ -86,13 +80,11 @@ const it: SynapseTranslationMessages = {
},
results: {
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",
skipped: "%{smart_count} 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",
},
},
@ -126,8 +118,7 @@ const it: SynapseTranslationMessages = {
user_type: "Tipo d'utente",
},
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.",
erase: "Constrassegna l'utente come cancellato dal GDPR",
},
@ -346,8 +337,7 @@ const it: SynapseTranslationMessages = {
guest_can_join: "gli utenti ospite possono entrare",
},
action: {
title:
"Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
content:
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
erase: "Rimuovi dall'elenco",

View file

@ -1,4 +1,5 @@
import chineseMessages from "@haxqer/ra-language-chinese";
import { SynapseTranslationMessages } from ".";
const zh: SynapseTranslationMessages = {
@ -14,8 +15,7 @@ const zh: SynapseTranslationMessages = {
sso_sign_in: "使用 SSO 登录",
},
users: {
invalid_user_id:
"必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
tabs: { sso: "SSO" },
},
rooms: {
@ -33,8 +33,7 @@ const zh: SynapseTranslationMessages = {
at_entry: "在条目 %{entry}: %{message}",
error: "错误",
required_field: "需要的值 '%{field}' 未被设置。",
invalid_value:
"第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
invalid_value: "第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
unreasonably_big: "拒绝加载过大的文件: %{size} MB",
already_in_progress: "一个导入进程已经在运行中",
id_exits: "ID %{id} 已经存在",
@ -44,8 +43,7 @@ const zh: SynapseTranslationMessages = {
cards: {
importstats: {
header: "导入用户",
users_total:
"%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
users_total: "%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
},
@ -59,8 +57,7 @@ const zh: SynapseTranslationMessages = {
ids: {
header: "IDs",
all_ids_present: "每条记录的 ID",
count_ids_present:
"%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
count_ids_present: "%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
mode: {
ignore: "忽略 CSV 中的 ID 并创建新的",
update: "更新已经存在的记录",
@ -69,8 +66,7 @@ const zh: SynapseTranslationMessages = {
passwords: {
header: "密码",
all_passwords_present: "每条记录的密码",
count_passwords_present:
"%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
count_passwords_present: "%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
use_passwords: "使用 CSV 中标记的密码",
},
upload: {
@ -88,8 +84,7 @@ const zh: SynapseTranslationMessages = {
successful: "%{smart_count} 条记录导入成功",
skipped: "跳过 %{smart_count} 条记录",
download_skipped: "下载跳过的记录",
with_error:
"%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
with_error: "%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
simulated_only: "只是一次模拟运行",
},
},

View file

@ -1,4 +1,5 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

View file

@ -1,4 +1,5 @@
import fetchMock from "jest-fetch-mock";
import authProvider from "./authProvider";
fetchMock.enableMocks();
@ -27,17 +28,14 @@ describe("authProvider", () => {
});
expect(ret).toBe(undefined);
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"}',
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
method: "POST",
}
);
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"}',
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
method: "POST",
});
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
@ -61,17 +59,14 @@ describe("authProvider", () => {
});
expect(ret).toBe(undefined);
expect(fetch).toHaveBeenCalledWith(
"https://example.com/_matrix/client/r0/login",
{
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
method: "POST",
}
);
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
method: "POST",
});
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
@ -100,21 +95,15 @@ describe("authProvider", () => {
describe("checkError", () => {
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 () => {
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 () => {
await expect(
authProvider.checkError({ status: 403 })
).rejects.toBeUndefined();
await expect(authProvider.checkError({ status: 403 })).rejects.toBeUndefined();
});
});

View file

@ -55,8 +55,7 @@ const authProvider: AuthProvider = {
logout: async () => {
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 options: Options = {
@ -84,9 +83,7 @@ const authProvider: AuthProvider = {
checkAuth: () => {
const access_token = localStorage.getItem("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
getPermissions: () => Promise.resolve(),

View file

@ -1,4 +1,5 @@
import fetchMock from "jest-fetch-mock";
import dataProvider from "./dataProvider";
fetchMock.enableMocks();
@ -45,8 +46,8 @@ describe("dataProvider", () => {
filter: { author_id: 12 },
});
expect(users["data"][0]["id"]).toEqual("user_id1");
expect(users["total"]).toEqual(200);
expect(users.data[0].id).toEqual("user_id1");
expect(users.total).toEqual(200);
expect(fetch).toHaveBeenCalledTimes(1);
});
@ -74,8 +75,8 @@ describe("dataProvider", () => {
const user = await dataProvider.getOne("users", { id: "user_id1" });
expect(user["data"]["id"]).toEqual("user_id1");
expect(user["data"]["displayname"]).toEqual("User");
expect(user.data.id).toEqual("user_id1");
expect(user.data.displayname).toEqual("User");
expect(fetch).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,13 +1,7 @@
import {
DataProvider,
DeleteParams,
Identifier,
Options,
RaRecord,
fetchUtils,
} from "react-admin";
import { stringify } from "query-string";
import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
// Adds the access token to all requests
const jsonClient = (url: string, options: Options = {}) => {
const token = localStorage.getItem("access_token");
@ -223,16 +217,12 @@ const resourceMap = {
data: "users",
total: json => json.total,
create: (data: RaRecord) => ({
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,
method: "PUT",
}),
delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
params.id
)}`,
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(params.id)}`,
body: { erase: true },
method: "POST",
}),
@ -272,9 +262,7 @@ const resourceMap = {
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
}),
delete: (params: DeleteParams) => ({
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: {
@ -322,9 +310,7 @@ const resourceMap = {
id: jr,
}),
reference: (id: Identifier) => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
id
)}/joined_rooms`,
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/joined_rooms`,
}),
data: "joined_rooms",
total: json => json.total,
@ -340,9 +326,7 @@ const resourceMap = {
data: "media",
total: json => json.total,
delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
"home_server"
)}/${params.id}`,
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
}),
},
delete_media: {
@ -369,15 +353,11 @@ const resourceMap = {
quarantine_media: {
map: (qm: UserMedia) => ({ id: qm.media_id }),
create: (params: UserMedia) => ({
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",
}),
delete: (params: DeleteParams) => ({
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",
}),
},
@ -504,15 +484,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
const dataProvider: DataProvider = {
getList: async (resource, params) => {
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 { field, order } = params.sort;
const from = (page - 1) * perPage;
@ -530,8 +502,7 @@ const dataProvider: DataProvider = {
dir: getSearchOrder(order),
};
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@ -548,32 +519,24 @@ const dataProvider: DataProvider = {
getOne: async (resource, params) => {
console.log("getOne " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
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) };
},
getMany: async (resource, params) => {
console.log("getMany " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homerserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
const res = resourceMap[resource];
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 {
data: responses.map(({ json }) => res.map(json)),
total: responses.length,
@ -593,12 +556,11 @@ const dataProvider: DataProvider = {
};
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
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 { json } = await jsonClient(endpoint_url);
@ -611,39 +573,31 @@ const dataProvider: DataProvider = {
update: async (resource, params) => {
console.log("update " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
const { json } = await jsonClient(
`${endpoint_url}/${encodeURIComponent(params.id)}`,
{
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
}
);
const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
});
return { data: res.map(json) };
},
updateMany: async (resource, params) => {
console.log("updateMany " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
const responses = await Promise.all(
params.ids.map(
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
{
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
}
)
params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`), {
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
})
);
return { data: responses.map(({ json }) => json) };
},
@ -651,13 +605,12 @@ const dataProvider: DataProvider = {
create: async (resource, params) => {
console.log("create " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
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 { json } = await jsonClient(endpoint_url, {
method: create.method,
@ -666,14 +619,10 @@ const dataProvider: DataProvider = {
return { data: res.map(json) };
},
createMany: async (
resource: string,
params: { ids: Identifier[]; data: RaRecord }
) => {
createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
console.log("createMany " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
if (!("create" in res)) throw Error(`Create ${resource} is not allowed`);
@ -681,7 +630,7 @@ const dataProvider: DataProvider = {
const responses = await Promise.all(
params.ids.map(id => {
params.data.id = id;
const cre = res["create"](params.data);
const cre = res.create(params.data);
const endpoint_url = homeserver + cre.endpoint;
return jsonClient(endpoint_url, {
method: cre.method,
@ -695,13 +644,12 @@ const dataProvider: DataProvider = {
delete: async (resource, params) => {
console.log("delete " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
if ("delete" in res) {
const del = res["delete"](params);
const del = res.delete(params);
const endpoint_url = homeserver + del.endpoint;
const { json } = await jsonClient(endpoint_url, {
method: "method" in del ? del.method : "DELETE",
@ -721,15 +669,14 @@ const dataProvider: DataProvider = {
deleteMany: async (resource, params) => {
console.log("deleteMany " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap))
throw Error("Homeserver not set");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
if ("delete" in res) {
const responses = await Promise.all(
params.ids.map(id => {
const del = res["delete"]({ ...params, id: id });
const del = res.delete({ ...params, id: id });
const endpoint_url = homeserver + del.endpoint;
return jsonClient(endpoint_url, {
method: "method" in del ? del.method : "DELETE",

View file

@ -10,22 +10,14 @@ describe("splitMxid", () => {
});
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("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());
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());
});

View file

@ -1,13 +1,11 @@
import { fetchUtils } from "react-admin";
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;
};
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
@ -77,8 +75,7 @@ export function generateRandomMxId(): string {
* @returns a new random password as string
*/
export function generateRandomPassword(length = 20): string {
const characters =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$";
const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$";
return Array.from(crypto.getRandomValues(new Uint32Array(length)))
.map(x => characters[x % characters.length])
.join("");

View file

@ -1,7 +1,8 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { vitePluginVersionMark } from "vite-plugin-version-mark";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
react(),

2067
yarn.lock

File diff suppressed because it is too large Load diff