remove unused eslint plugin, run eslint --fix, rollback node memory workaround in ci

This commit is contained in:
Aine 2025-04-05 21:37:31 +03:00
parent 738685c599
commit bac962c127
No known key found for this signature in database
GPG Key ID: 34969C908CCA2804
62 changed files with 1782 additions and 1502 deletions

View File

@ -24,8 +24,6 @@ jobs:
node-version: lts/* node-version: lts/*
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
env:
NODE_OPTIONS: --max-old-space-size=8192
run: yarn install --immutable --network-timeout=300000 run: yarn install --immutable --network-timeout=300000
- name: Build - name: Build
run: yarn build --base=${{ env.base_path }} run: yarn build --base=${{ env.base_path }}

View File

@ -10,22 +10,22 @@ const config: JestConfigWithTsJest = {
extensionsToTreatAsEsm: [".ts", ".tsx"], extensionsToTreatAsEsm: [".ts", ".tsx"],
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"], setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
transform: { transform: {
'^.+\\.tsx?$': [ "^.+\\.tsx?$": [
'ts-jest', "ts-jest",
{ {
diagnostics: { diagnostics: {
ignoreCodes: [1343] ignoreCodes: [1343],
}, },
astTransformers: { astTransformers: {
before: [ before: [
{ {
path: 'ts-jest-mock-import-meta', path: "ts-jest-mock-import-meta",
options: { metaObjectReplacement: { env: { BASE_URL: "/" } } } options: { metaObjectReplacement: { env: { BASE_URL: "/" } } },
} },
] ],
} },
} },
] ],
} },
}; };
export default config; export default config;

View File

@ -30,7 +30,6 @@
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.5", "eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-yaml": "^1.1.2",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
@ -75,7 +74,7 @@
"scripts": { "scripts": {
"start": "vite serve", "start": "vite serve",
"build": "vite build", "build": "vite build",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ignore-path .gitignore --ignore-pattern testdata/ --ext .ts,.tsx,.yml,.yaml .", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ignore-path .gitignore --ignore-pattern testdata/ --ext .ts,.tsx .",
"fix": "yarn lint --fix", "fix": "yarn lint --fix",
"test": "yarn jest", "test": "yarn jest",
"test:watch": "yarn jest --watch" "test:watch": "yarn jest --watch"
@ -88,15 +87,13 @@
"import", "import",
"prettier", "prettier",
"unused-imports", "unused-imports",
"@typescript-eslint", "@typescript-eslint"
"yaml"
], ],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic", "plugin:@typescript-eslint/stylistic",
"plugin:import/typescript", "plugin:import/typescript"
"plugin:yaml/legacy"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
@ -135,8 +132,7 @@
] ]
] ]
} }
], ]
"unused-imports/no-unused-imports-ts": 2
} }
}, },
"prettier": { "prettier": {

View File

@ -1,6 +1,6 @@
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import fetchMock from "jest-fetch-mock"; import fetchMock from "jest-fetch-mock";
import { BrowserRouter } from "react-router-dom";
fetchMock.enableMocks(); fetchMock.enableMocks();
jest.mock("./synapse/authProvider", () => ({ jest.mock("./synapse/authProvider", () => ({

View File

@ -1,11 +1,12 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { merge } from "lodash"; import { merge } from "lodash";
import polyglotI18nProvider from "ra-i18n-polyglot"; import polyglotI18nProvider from "ra-i18n-polyglot";
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin"; import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import AdminLayout from "./components/AdminLayout"; import AdminLayout from "./components/AdminLayout";
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
import UserImport from "./components/user-import/UserImport"; import UserImport from "./components/user-import/UserImport";
import germanMessages from "./i18n/de"; import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en"; import englishMessages from "./i18n/en";
@ -23,9 +24,6 @@ import userMediaStats from "./resources/user_media_statistics";
import users from "./resources/users"; import users from "./resources/users";
import authProvider from "./synapse/authProvider"; import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider"; import dataProvider from "./synapse/dataProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
// TODO: Can we use lazy loading together with browser locale? // TODO: Can we use lazy loading together with browser locale?
const messages = { const messages = {

View File

@ -1,12 +1,25 @@
import { CheckForApplicationUpdate, AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu, useStore } from "react-admin";
import { LoginMethod } from "../pages/LoginPage";
import { useEffect, useState, Suspense } from "react"; import { useEffect, useState, Suspense } from "react";
import { Icons, DefaultIcon } from "../utils/icons"; import {
import { MenuItem, GetConfig, ClearConfig } from "../utils/config"; CheckForApplicationUpdate,
AppBar,
TitlePortal,
InspectorButton,
Confirm,
Layout,
Logout,
Menu,
useLogout,
UserMenu,
useStore,
} from "react-admin";
import Footer from "./Footer"; import Footer from "./Footer";
import ServerStatusBadge from "./etke.cc/ServerStatusBadge"; import { LoginMethod } from "../pages/LoginPage";
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
import { Icons, DefaultIcon } from "../utils/icons";
import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge"; import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge";
import { ServerProcessResponse, ServerStatusResponse } from "../synapse/dataProvider"; import { ServerProcessResponse, ServerStatusResponse } from "../synapse/dataProvider";
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
import { ServerStatusStyledBadge } from "./etke.cc/ServerStatusBadge"; import { ServerStatusStyledBadge } from "./etke.cc/ServerStatusBadge";
const AdminUserMenu = () => { const AdminUserMenu = () => {
@ -50,15 +63,17 @@ const AdminUserMenu = () => {
}; };
const AdminAppBar = () => { const AdminAppBar = () => {
return (<AppBar userMenu={<AdminUserMenu />}> return (
<AppBar userMenu={<AdminUserMenu />}>
<TitlePortal /> <TitlePortal />
<ServerStatusBadge /> <ServerStatusBadge />
<ServerNotificationsBadge /> <ServerNotificationsBadge />
<InspectorButton /> <InspectorButton />
</AppBar>); </AppBar>
);
}; };
const AdminMenu = (props) => { const AdminMenu = props => {
const [menu, setMenu] = useState([] as MenuItem[]); const [menu, setMenu] = useState([] as MenuItem[]);
const [serverStatusEnabled, setServerStatusEnabled] = useState(false); const [serverStatusEnabled, setServerStatusEnabled] = useState(false);
useEffect(() => { useEffect(() => {
@ -67,22 +82,36 @@ const AdminMenu = (props) => {
setServerStatusEnabled(true); setServerStatusEnabled(true);
} }
}, []); }, []);
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { success: false, ok: false, host: "", results: [] }); command: "",
locked_at: "",
});
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
success: false,
ok: false,
host: "",
results: [],
});
return ( return (
<Menu {...props}> <Menu {...props}>
{serverStatusEnabled && <Menu.Item to="/server_status" leftIcon={ {serverStatusEnabled && (
<Menu.Item
to="/server_status"
leftIcon={
<ServerStatusStyledBadge <ServerStatusStyledBadge
inSidebar={true} inSidebar={true}
command={serverProcess.command} command={serverProcess.command}
locked_at={serverProcess.locked_at} locked_at={serverProcess.locked_at}
isOkay={serverStatus.ok} /> isOkay={serverStatus.ok}
} />
primaryText="Server Status" />
} }
primaryText="Server Status"
/>
)}
<Menu.ResourceItems /> <Menu.ResourceItems />
{menu && menu.map((item, index) => { {menu &&
menu.map((item, index) => {
const { url, icon, label } = item; const { url, icon, label } = item;
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined; const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
@ -103,21 +132,27 @@ const AdminMenu = (props) => {
}; };
export const AdminLayout = ({ children }) => { export const AdminLayout = ({ children }) => {
return <> return (
<Layout appBar={AdminAppBar} menu={AdminMenu} sx={{ <>
['& .RaLayout-appFrame']: { <Layout
minHeight: '90vh', appBar={AdminAppBar}
height: '90vh', menu={AdminMenu}
sx={{
["& .RaLayout-appFrame"]: {
minHeight: "90vh",
height: "90vh",
}, },
['& .RaLayout-content']: { ["& .RaLayout-content"]: {
marginBottom: '3rem', marginBottom: "3rem",
}, },
}}> }}
>
{children} {children}
<CheckForApplicationUpdate /> <CheckForApplicationUpdate />
</Layout> </Layout>
<Footer /> <Footer />
</> </>
);
}; };
export default AdminLayout; export default AdminLayout;

View File

@ -1,6 +1,7 @@
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import { RecordContextProvider } from "react-admin";
import { act } from "react"; import { act } from "react";
import { RecordContextProvider } from "react-admin";
import AvatarField from "./AvatarField"; import AvatarField from "./AvatarField";
describe("AvatarField", () => { describe("AvatarField", () => {
@ -8,7 +9,7 @@ describe("AvatarField", () => {
// Mock fetch // Mock fetch
global.fetch = jest.fn(() => global.fetch = jest.fn(() =>
Promise.resolve({ Promise.resolve({
blob: () => Promise.resolve(new Blob(["mock image data"], { type: 'image/jpeg' })), blob: () => Promise.resolve(new Blob(["mock image data"], { type: "image/jpeg" })),
}) })
) as jest.Mock; ) as jest.Mock;

View File

@ -1,7 +1,8 @@
import { get } from "lodash";
import { Avatar, AvatarProps, Badge, Tooltip } from "@mui/material"; import { Avatar, AvatarProps, Badge, Tooltip } from "@mui/material";
import { FieldProps, useRecordContext, useTranslate } from "react-admin"; import { get } from "lodash";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { FieldProps, useRecordContext, useTranslate } from "react-admin";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia"; import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { isMXID, isASManaged } from "../utils/mxid"; import { isMXID, isASManaged } from "../utils/mxid";
@ -96,12 +97,15 @@ const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
{letter} {letter}
</Avatar> </Avatar>
</Badge> </Badge>
</Tooltip>); </Tooltip>
);
} }
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}>
{letter} {letter}
</Avatar>); </Avatar>
);
}; };
export default AvatarField; export default AvatarField;

View File

@ -1,9 +1,21 @@
import ActionCheck from "@mui/icons-material/CheckCircle";
import ActionDelete from "@mui/icons-material/Delete";
import AlertError from "@mui/icons-material/ErrorOutline";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin"; import {
import ActionDelete from "@mui/icons-material/Delete"; SimpleForm,
import ActionCheck from "@mui/icons-material/CheckCircle"; BooleanInput,
import AlertError from "@mui/icons-material/ErrorOutline"; useTranslate,
RaRecord,
useNotify,
useRedirect,
useDelete,
NotificationType,
useDeleteMany,
Identifier,
useUnselectAll,
} from "react-admin";
interface DeleteRoomButtonProps { interface DeleteRoomButtonProps {
selectedIds: Identifier[]; selectedIds: Identifier[];
@ -13,7 +25,7 @@ interface DeleteRoomButtonProps {
const resourceName = "rooms"; const resourceName = "rooms";
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => { const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = props => {
const translate = useTranslate(); const translate = useTranslate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [block, setBlock] = useState(false); const [block, setBlock] = useState(false);
@ -28,7 +40,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
const handleDelete = (values: {block: boolean}) => { const handleDelete = (values: { block: boolean }) => {
deleteMany( deleteMany(
resourceName, resourceName,
{ ids: recordIds, meta: values }, { ids: recordIds, meta: values },
@ -39,8 +51,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
unselectAll(); unselectAll();
redirect("/rooms"); redirect("/rooms");
}, },
onError: (error) => onError: error => notify("resources.rooms.action.erase.failure", { type: "error" as NotificationType }),
notify("resources.rooms.action.erase.failure", { type: 'error' as NotificationType }),
} }
); );
}; };

View File

@ -1,9 +1,21 @@
import ActionCheck from "@mui/icons-material/CheckCircle";
import ActionDelete from "@mui/icons-material/Delete";
import AlertError from "@mui/icons-material/ErrorOutline";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin"; import {
import ActionDelete from "@mui/icons-material/Delete"; SimpleForm,
import ActionCheck from "@mui/icons-material/CheckCircle"; BooleanInput,
import AlertError from "@mui/icons-material/ErrorOutline"; useTranslate,
RaRecord,
useNotify,
useRedirect,
useDelete,
NotificationType,
useDeleteMany,
Identifier,
useUnselectAll,
} from "react-admin";
interface DeleteUserButtonProps { interface DeleteUserButtonProps {
selectedIds: Identifier[]; selectedIds: Identifier[];
@ -13,7 +25,7 @@ interface DeleteUserButtonProps {
const resourceName = "users"; const resourceName = "users";
const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => { const DeleteUserButton: React.FC<DeleteUserButtonProps> = props => {
const translate = useTranslate(); const translate = useTranslate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [deleteMedia, setDeleteMedia] = useState(false); const [deleteMedia, setDeleteMedia] = useState(false);
@ -29,7 +41,7 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
const handleDelete = (values: {deleteMedia: boolean, redactEvents: boolean}) => { const handleDelete = (values: { deleteMedia: boolean; redactEvents: boolean }) => {
deleteMany( deleteMany(
resourceName, resourceName,
{ ids: recordIds, meta: values }, { ids: recordIds, meta: values },
@ -39,14 +51,13 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
messageArgs: { messageArgs: {
smart_count: recordIds.length, smart_count: recordIds.length,
}, },
type: 'info' as NotificationType, type: "info" as NotificationType,
}); });
handleDialogClose(); handleDialogClose();
unselectAll(); unselectAll();
redirect("/users"); redirect("/users");
}, },
onError: (error) => onError: error => notify("ra.notification.data_provider_error", { type: "error" as NotificationType }),
notify("ra.notification.data_provider_error", { type: 'error' as NotificationType }),
} }
); );
}; };

View File

@ -1,4 +1,5 @@
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin"; import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
import { isASManaged } from "../utils/mxid"; import { isASManaged } from "../utils/mxid";
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => { export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {

View File

@ -1,15 +1,20 @@
import { Stack, Switch, Typography } from "@mui/material";
import { useState, useEffect } from "react";
import { useRecordContext } from "react-admin"; import { useRecordContext } from "react-admin";
import { useNotify } from "react-admin"; import { useNotify } from "react-admin";
import { useDataProvider } from "react-admin"; import { useDataProvider } from "react-admin";
import { useState, useEffect } from "react";
import { Stack, Switch, Typography } from "@mui/material";
import { ExperimentalFeaturesModel, SynapseDataProvider } from "../synapse/dataProvider"; import { ExperimentalFeaturesModel, SynapseDataProvider } from "../synapse/dataProvider";
const experimentalFeaturesMap = { const experimentalFeaturesMap = {
msc3881: "enable remotely toggling push notifications for another client", msc3881: "enable remotely toggling push notifications for another client",
msc3575: "enable experimental sliding sync support", msc3575: "enable experimental sliding sync support",
}; };
const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boolean, updateFeature: (feature_name: string, feature_value: boolean) => void}) => { const ExperimentalFeatureRow = (props: {
featureKey: string;
featureValue: boolean;
updateFeature: (feature_name: string, feature_value: boolean) => void;
}) => {
const featureKey = props.featureKey; const featureKey = props.featureKey;
const featureValue = props.featureValue; const featureValue = props.featureValue;
const featureDescription = experimentalFeaturesMap[featureKey] ?? ""; const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
@ -20,7 +25,8 @@ const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boole
props.updateFeature(featureKey, event.target.checked); props.updateFeature(featureKey, event.target.checked);
}; };
return <Stack return (
<Stack
direction="row" direction="row"
spacing={2} spacing={2}
alignItems="start" alignItems="start"
@ -34,20 +40,18 @@ const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boole
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
fontWeight: "medium", fontWeight: "medium",
color: "text.primary" color: "text.primary",
}} }}
> >
{featureKey} {featureKey}
</Typography> </Typography>
<Typography <Typography variant="body2" color="text.secondary">
variant="body2"
color="text.secondary"
>
{featureDescription} {featureDescription}
</Typography> </Typography>
</Stack> </Stack>
</Stack> </Stack>
} );
};
export const ExperimentalFeaturesList = () => { export const ExperimentalFeaturesList = () => {
const record = useRecordContext(); const record = useRecordContext();
@ -62,13 +66,13 @@ export const ExperimentalFeaturesList = () => {
const fetchFeatures = async () => { const fetchFeatures = async () => {
const features = await dataProvider.getFeatures(record.id); const features = await dataProvider.getFeatures(record.id);
setFeatures(features); setFeatures(features);
} };
fetchFeatures(); fetchFeatures();
}, []); }, []);
const updateFeature = async (feature_name: string, feature_value: boolean) => { const updateFeature = async (feature_name: string, feature_value: boolean) => {
const updatedFeatures = {...features, [feature_name]: feature_value} as ExperimentalFeaturesModel; const updatedFeatures = { ...features, [feature_name]: feature_value } as ExperimentalFeaturesModel;
setFeatures(updatedFeatures); setFeatures(updatedFeatures);
const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures); const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures);
notify("ra.notification.updated", { notify("ra.notification.updated", {
@ -77,21 +81,20 @@ export const ExperimentalFeaturesList = () => {
}); });
}; };
return <> return (
<Stack <>
direction="column" <Stack direction="column" spacing={1}>
spacing={1} {Object.keys(features).map((featureKey: string) => (
>
{Object.keys(features).map((featureKey: string) =>
<ExperimentalFeatureRow <ExperimentalFeatureRow
key={featureKey} key={featureKey}
featureKey={featureKey} featureKey={featureKey}
featureValue={features[featureKey]} featureValue={features[featureKey]}
updateFeature={updateFeature} updateFeature={updateFeature}
/> />
)} ))}
</Stack> </Stack>
</> </>
} );
};
export default ExperimentalFeaturesList; export default ExperimentalFeaturesList;

View File

@ -1,6 +1,6 @@
import { Avatar, Box, Link, Typography } from "@mui/material"; import { Avatar, Box, Link, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useEffect, useState } from "react";
const Footer = () => { const Footer = () => {
const [version, setVersion] = useState<string | null>(null); const [version, setVersion] = useState<string | null>(null);
@ -13,35 +13,45 @@ const Footer = () => {
} }
}, []); }, []);
return (<Box return (
<Box
component="footer" component="footer"
sx={{ sx={{
position: 'fixed', position: "fixed",
zIndex: 100, zIndex: 100,
bottom: 0, bottom: 0,
width: '100%', width: "100%",
bgcolor: theme.palette.background.default, bgcolor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
borderTop: '1px solid', borderTop: "1px solid",
borderColor: theme.palette.divider, borderColor: theme.palette.divider,
fontSize: '0.89rem', fontSize: "0.89rem",
display: 'flex', display: "flex",
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
justifyContent: 'start', justifyContent: "start",
p: 1, p: 1,
gap: '10px' gap: "10px",
}}> }}
<Avatar src="./images/logo.webp" sx={{ width: "1rem", height: "1rem", display: "inline-block", verticalAlign: "sub" }} /> >
<Avatar
src="./images/logo.webp"
sx={{ width: "1rem", height: "1rem", display: "inline-block", verticalAlign: "sub" }}
/>
<Link href="https://github.com/etkecc/synapse-admin" target="_blank"> <Link href="https://github.com/etkecc/synapse-admin" target="_blank">
Synapse Admin {version} Synapse Admin {version}
</Link> </Link>
by by
<Link href="https://etke.cc/?utm_source=synapse-admin&utm_medium=footer&utm_campaign=synapse-admin" target="_blank"> <Link
href="https://etke.cc/?utm_source=synapse-admin&utm_medium=footer&utm_campaign=synapse-admin"
target="_blank"
>
etke.cc etke.cc
</Link> </Link>
(originally developed by Awesome Technologies Innovationslabor GmbH). (originally developed by Awesome Technologies Innovationslabor GmbH).
<Link sx={{ fontWeight: 'bold' }} href="https://matrix.to/#/#synapse-admin:etke.cc" target="_blank">#synapse-admin:etke.cc</Link> <Link sx={{ fontWeight: "bold" }} href="https://matrix.to/#/#synapse-admin:etke.cc" target="_blank">
#synapse-admin:etke.cc
</Link>
</Box> </Box>
); );
}; };

View File

@ -1,5 +1,5 @@
import { styled } from "@mui/material/styles";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { styled } from "@mui/material/styles";
const LoginFormBox = styled(Box)(({ theme }) => ({ const LoginFormBox = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
@ -8,7 +8,7 @@ const LoginFormBox = styled(Box)(({ theme }) => ({
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)", background: "url(./images/floating-cogs.svg)",
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.background.default : theme.palette.background.paper, backgroundColor: theme.palette.mode === "dark" ? theme.palette.background.default : theme.palette.background.paper,
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundSize: "cover", backgroundSize: "cover",
@ -53,7 +53,6 @@ const LoginFormBox = styled(Box)(({ theme }) => ({
marginBottom: "1rem", marginBottom: "1rem",
marginLeft: "0.5rem", marginLeft: "0.5rem",
}, },
} }));
));
export default LoginFormBox; export default LoginFormBox;

View File

@ -1,8 +1,8 @@
import { useState } from "react";
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@mui/icons-material/Cancel";
import MessageIcon from "@mui/icons-material/Message"; import MessageIcon from "@mui/icons-material/Message";
import { Dialog, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { Dialog, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { import {
Button, Button,
RaRecord, RaRecord,
@ -20,7 +20,6 @@ import {
useTranslate, useTranslate,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "@tanstack/react-query";
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => { const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate(); const translate = useTranslate();
@ -104,7 +103,7 @@ export const ServerNoticeBulkButton = () => {
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
const { mutate: sendNotices, isPending } = useMutation({ const { mutate: sendNotices, isPending } = useMutation({
mutationFn: (data) => mutationFn: data =>
dataProvider.createMany("servernotices", { dataProvider.createMany("servernotices", {
ids: selectedIds, ids: selectedIds,
data: data, data: data,

View File

@ -1,7 +1,8 @@
import { useDataProvider, useRecordContext, useTranslate } from "react-admin"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { useEffect, useState } from "react";
import { Typography, Box, Stack, Accordion, AccordionSummary, AccordionDetails } from "@mui/material"; import { Typography, Box, Stack, Accordion, AccordionSummary, AccordionDetails } from "@mui/material";
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import { useEffect, useState } from "react";
import { useDataProvider, useRecordContext, useTranslate } from "react-admin";
import { SynapseDataProvider } from "../synapse/dataProvider"; import { SynapseDataProvider } from "../synapse/dataProvider";
const UserAccountData = () => { const UserAccountData = () => {
@ -25,24 +26,25 @@ const UserAccountData = () => {
}, []); }, []);
if (Object.keys(globalAccountData).length === 0 && Object.keys(roomsAccountData).length === 0) { if (Object.keys(globalAccountData).length === 0 && Object.keys(roomsAccountData).length === 0) {
return <Typography variant="body2">{translate('ra.navigation.no_results', { return (
resource: 'Account Data', <Typography variant="body2">
_: 'No results found.', {translate("ra.navigation.no_results", {
})}</Typography>; resource: "Account Data",
_: "No results found.",
})}
</Typography>
);
} }
return <> return (
<Stack <>
direction="column" <Stack direction="column" spacing={2} width="100%">
spacing={2} <Typography variant="h6">{translate("resources.users.account_data.title")}</Typography>
width="100%"
>
<Typography variant="h6">{translate('resources.users.account_data.title')}</Typography>
<Typography variant="body1"> <Typography variant="body1">
<Box> <Box>
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ArrowDownwardIcon />}> <AccordionSummary expandIcon={<ArrowDownwardIcon />}>
<Typography variant="h6">{translate('resources.users.account_data.global')}</Typography> <Typography variant="h6">{translate("resources.users.account_data.global")}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(globalAccountData, null, 4)}</Box> <Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(globalAccountData, null, 4)}</Box>
@ -50,7 +52,7 @@ const UserAccountData = () => {
</Accordion> </Accordion>
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ArrowDownwardIcon />}> <AccordionSummary expandIcon={<ArrowDownwardIcon />}>
<Typography variant="h6">{translate('resources.users.account_data.rooms')}</Typography> <Typography variant="h6">{translate("resources.users.account_data.rooms")}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box> <Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box>
@ -60,6 +62,7 @@ const UserAccountData = () => {
</Typography> </Typography>
</Stack> </Stack>
</> </>
} );
};
export default UserAccountData; export default UserAccountData;

View File

@ -1,10 +1,18 @@
import { Stack, Typography } from "@mui/material"; import { Stack, Typography } from "@mui/material";
import { TextField } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin"; import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin";
import { TextField } from "@mui/material";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value: any, updateRateLimit: (limit: string, value: any) => void }) => { const RateLimitRow = ({
limit,
value,
updateRateLimit,
}: {
limit: string;
value: any;
updateRateLimit: (limit: string, value: any) => void;
}) => {
const translate = useTranslate(); const translate = useTranslate();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -16,7 +24,8 @@ const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value:
updateRateLimit(limit, value); updateRateLimit(limit, value);
}; };
return <Stack return (
<Stack
spacing={1} spacing={1}
alignItems="start" alignItems="start"
sx={{ sx={{
@ -36,15 +45,13 @@ const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value:
label={translate(`resources.users.limits.${limit}`)} label={translate(`resources.users.limits.${limit}`)}
/> />
<Stack> <Stack>
<Typography <Typography variant="body2" color="text.secondary">
variant="body2"
color="text.secondary"
>
{translate(`resources.users.limits.${limit}_text`)} {translate(`resources.users.limits.${limit}_text`)}
</Typography> </Typography>
</Stack> </Stack>
</Stack> </Stack>
} );
};
const UserRateLimits = () => { const UserRateLimits = () => {
const translate = useTranslate(); const translate = useTranslate();
@ -67,31 +74,26 @@ const UserRateLimits = () => {
if (Object.keys(rateLimits).length > 0) { if (Object.keys(rateLimits).length > 0) {
setRateLimits(rateLimits); setRateLimits(rateLimits);
} }
} };
fetchRateLimits(); fetchRateLimits();
}, []); }, []);
const updateRateLimit = async (limit: string, value: any) => { const updateRateLimit = async (limit: string, value: any) => {
let updatedRateLimits = { ...rateLimits, [limit]: value }; const updatedRateLimits = { ...rateLimits, [limit]: value };
setRateLimits(updatedRateLimits); setRateLimits(updatedRateLimits);
form.setValue(`rates.${limit}`, value, { shouldDirty: true }); form.setValue(`rates.${limit}`, value, { shouldDirty: true });
}; };
return <> return (
<Stack <>
direction="column" <Stack direction="column">
> {Object.keys(rateLimits).map((limit: string) => (
{Object.keys(rateLimits).map((limit: string) => <RateLimitRow key={limit} limit={limit} value={rateLimits[limit]} updateRateLimit={updateRateLimit} />
<RateLimitRow ))}
key={limit}
limit={limit}
value={rateLimits[limit]}
updateRateLimit={updateRateLimit}
/>
)}
</Stack> </Stack>
</> </>
);
}; };
export default UserRateLimits; export default UserRateLimits;

View File

@ -1,25 +1,39 @@
import { PlayArrow, CheckCircle } from "@mui/icons-material";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Alert,
TextField,
Box,
} from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin"; import { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin";
import { useAppContext } from "../../Context"; import { Link } from "react-router-dom";
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Alert, TextField, Box } from "@mui/material";
import { PlayArrow, CheckCircle } from "@mui/icons-material";
import { Icons } from "../../utils/icons";
import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider";
import { Link } from 'react-router-dom';
import { useAppContext } from "../../Context";
import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider";
import { Icons } from "../../utils/icons";
const renderIcon = (icon: string) => { const renderIcon = (icon: string) => {
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined; const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null; return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null;
} };
const ServerCommandsPanel = () => { const ServerCommandsPanel = () => {
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
const createPath = useCreatePath(); const createPath = useCreatePath();
const [ isLoading, setLoading ] = useState(true); const [isLoading, setLoading] = useState(true);
const [serverCommands, setServerCommands] = useState<{ [key: string]: ServerCommand }>({}); const [serverCommands, setServerCommands] = useState<Record<string, ServerCommand>>({});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== ""); const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== "");
const [commandResult, setCommandResult] = useState<any[]>([]); const [commandResult, setCommandResult] = useState<any[]>([]);
@ -36,7 +50,7 @@ const ServerCommandsPanel = () => {
setServerCommands(serverCommands); setServerCommands(serverCommands);
} }
setLoading(false); setLoading(false);
} };
fetchIsAdmin(); fetchIsAdmin();
}, []); }, []);
@ -47,10 +61,10 @@ const ServerCommandsPanel = () => {
}, [serverProcess]); }, [serverProcess]);
const setCommandAdditionalArgs = (command: string, additionalArgs: string) => { const setCommandAdditionalArgs = (command: string, additionalArgs: string) => {
const updatedServerCommands = {...serverCommands}; const updatedServerCommands = { ...serverCommands };
updatedServerCommands[command].additionalArgs = additionalArgs; updatedServerCommands[command].additionalArgs = additionalArgs;
setServerCommands(updatedServerCommands); setServerCommands(updatedServerCommands);
} };
const runCommand = async (command: string) => { const runCommand = async (command: string) => {
setCommandResult([]); setCommandResult([]);
@ -92,7 +106,8 @@ const ServerCommandsPanel = () => {
results.push(<Box>{commandScheduledText}</Box>); results.push(<Box>{commandScheduledText}</Box>);
results.push( results.push(
<Box> <Box>
Expect your result in the <Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon. Expect your result in the{" "}
<Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon.
</Box> </Box>
); );
@ -100,7 +115,7 @@ const ServerCommandsPanel = () => {
}; };
const resetCommandArgs = (command: string) => { const resetCommandArgs = (command: string) => {
const updatedServerCommands = {...serverCommands}; const updatedServerCommands = { ...serverCommands };
updatedServerCommands[command].additionalArgs = ""; updatedServerCommands[command].additionalArgs = "";
setServerCommands(updatedServerCommands); setServerCommands(updatedServerCommands);
}; };
@ -114,14 +129,15 @@ const ServerCommandsPanel = () => {
serverProcess["locked_at"] = new Date().toISOString(); serverProcess["locked_at"] = new Date().toISOString();
} }
setServerProcess({...serverProcess}); setServerProcess({ ...serverProcess });
}; };
if (isLoading) { if (isLoading) {
return <Loading /> return <Loading />;
} }
return (<> return (
<>
<h2>Server Commands</h2> <h2>Server Commands</h2>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table"> <Table sx={{ minWidth: 450 }} size="small" aria-label="simple table">
@ -134,10 +150,7 @@ const ServerCommandsPanel = () => {
</TableHead> </TableHead>
<TableBody> <TableBody>
{Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => ( {Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => (
<TableRow <TableRow key={command} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
key={command}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell scope="row"> <TableCell scope="row">
<Box> <Box>
{renderIcon(icon)} {renderIcon(icon)}
@ -146,22 +159,28 @@ const ServerCommandsPanel = () => {
</TableCell> </TableCell>
<TableCell>{description}</TableCell> <TableCell>{description}</TableCell>
<TableCell> <TableCell>
{args && <TextField {args && (
<TextField
size="small" size="small"
variant="standard" variant="standard"
onChange={(e) => { onChange={e => {
setCommandAdditionalArgs(command, e.target.value); setCommandAdditionalArgs(command, e.target.value);
}} }}
value={additionalArgs} value={additionalArgs}
/>} />
)}
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
color="primary" color="primary"
label="Run" label="Run"
startIcon={<PlayArrow />} startIcon={<PlayArrow />}
onClick={() => { runCommand(command); }} onClick={() => {
disabled={commandIsRunning || (args && typeof additionalArgs === 'string' && additionalArgs.length === 0)} runCommand(command);
}}
disabled={
commandIsRunning || (args && typeof additionalArgs === "string" && additionalArgs.length === 0)
}
></Button> ></Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -170,13 +189,15 @@ const ServerCommandsPanel = () => {
</Table> </Table>
</TableContainer> </TableContainer>
{commandResult.length > 0 && <Alert icon={<CheckCircle fontSize="inherit" />} severity="success"> {commandResult.length > 0 && (
<Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
{commandResult.map((result, index) => ( {commandResult.map((result, index) => (
<div key={index}>{result}</div> <div key={index}>{result}</div>
))} ))}
</Alert>} </Alert>
)}
</> </>
) );
}; };
export default ServerCommandsPanel; export default ServerCommandsPanel;

View File

@ -1,9 +1,26 @@
import { Badge, useTheme, Button, Paper, Popper, ClickAwayListener, Box, List, ListItem, ListItemText, Typography, ListSubheader, IconButton, Divider, Tooltip } from "@mui/material";
import NotificationsIcon from '@mui/icons-material/Notifications';
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import NotificationsIcon from "@mui/icons-material/Notifications";
import {
Badge,
useTheme,
Button,
Paper,
Popper,
ClickAwayListener,
Box,
List,
ListItem,
ListItemText,
Typography,
ListSubheader,
IconButton,
Divider,
Tooltip,
} from "@mui/material";
import { Fragment, useEffect, useState } from "react";
import { useDataProvider, useStore } from "react-admin"; import { useDataProvider, useStore } from "react-admin";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { Fragment, useEffect, useState } from "react";
import { useAppContext } from "../../Context"; import { useAppContext } from "../../Context";
import { ServerNotificationsResponse, ServerProcessResponse } from "../../synapse/dataProvider"; import { ServerNotificationsResponse, ServerProcessResponse } from "../../synapse/dataProvider";
import { getTimeSince } from "../../utils/date"; import { getTimeSince } from "../../utils/date";
@ -12,8 +29,14 @@ import { getTimeSince } from "../../utils/date";
const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000; const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;
const useServerNotifications = () => { const useServerNotifications = () => {
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { notifications: [], success: false }); const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", {
const [ serverProcess, setServerProcess ] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); notifications: [],
success: false,
});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
@ -21,13 +44,16 @@ const useServerNotifications = () => {
const { notifications, success } = serverNotifications; const { notifications, success } = serverNotifications;
const fetchNotifications = async () => { const fetchNotifications = async () => {
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin, command !== ""); const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(
etkeccAdmin,
command !== ""
);
const serverNotifications = [...notificationsResponse.notifications]; const serverNotifications = [...notificationsResponse.notifications];
serverNotifications.reverse(); serverNotifications.reverse();
setServerNotifications({ setServerNotifications({
...notificationsResponse, ...notificationsResponse,
notifications: serverNotifications, notifications: serverNotifications,
success: notificationsResponse.success success: notificationsResponse.success,
}); });
}; };
@ -60,7 +86,7 @@ const useServerNotifications = () => {
if (serverNotificationsInterval) { if (serverNotificationsInterval) {
clearInterval(serverNotificationsInterval); clearInterval(serverNotificationsInterval);
} }
} };
}, [etkeccAdmin, command, locked_at]); }, [etkeccAdmin, command, locked_at]);
return { success, notifications, deleteServerNotifications }; return { success, notifications, deleteServerNotifications };
@ -89,7 +115,7 @@ export const ServerNotificationsBadge = () => {
}; };
const handleClearAllNotifications = async () => { const handleClearAllNotifications = async () => {
deleteServerNotifications() deleteServerNotifications();
handleClose(); handleClose();
}; };
@ -100,20 +126,21 @@ export const ServerNotificationsBadge = () => {
return ( return (
<Box> <Box>
<IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}> <IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}>
<Tooltip title={notifications && notifications.length > 0 ? `${notifications.length} new notifications` : `No notifications yet`}> <Tooltip
{notifications && notifications.length > 0 && ( title={
notifications && notifications.length > 0
? `${notifications.length} new notifications`
: `No notifications yet`
}
>
{(notifications && notifications.length > 0 && (
<Badge badgeContent={notifications.length} color="error"> <Badge badgeContent={notifications.length} color="error">
<NotificationsIcon /> <NotificationsIcon />
</Badge> </Badge>
) || <NotificationsIcon />} )) || <NotificationsIcon />}
</Tooltip> </Tooltip>
</IconButton> </IconButton>
<Popper <Popper open={open} anchorEl={anchorEl} placement="bottom-end" style={{ zIndex: 1300 }}>
open={open}
anchorEl={anchorEl}
placement="bottom-end"
style={{ zIndex: 1300 }}
>
<ClickAwayListener onClickAway={handleClose}> <ClickAwayListener onClickAway={handleClose}>
<Paper <Paper
elevation={3} elevation={3}
@ -125,12 +152,14 @@ export const ServerNotificationsBadge = () => {
minWidth: "300px", minWidth: "300px",
maxWidth: { maxWidth: {
xs: "100vw", // Full width on mobile xs: "100vw", // Full width on mobile
sm: "400px" // Fixed width on desktop sm: "400px", // Fixed width on desktop
} },
}} }}
> >
{(!notifications || notifications.length === 0) ? ( {!notifications || notifications.length === 0 ? (
<Typography sx={{ p: 1 }} variant="body2">No new notifications</Typography> <Typography sx={{ p: 1 }} variant="body2">
No new notifications
</Typography>
) : ( ) : (
<List sx={{ p: 0 }} dense={true}> <List sx={{ p: 0 }} dense={true}>
<ListSubheader <ListSubheader
@ -142,11 +171,17 @@ export const ServerNotificationsBadge = () => {
}} }}
> >
<Typography variant="h6">Notifications</Typography> <Typography variant="h6">Notifications</Typography>
<Box sx={{ cursor: "pointer", color: theme.palette.primary.main }} onClick={() => handleSeeAllNotifications()}>See all notifications</Box> <Box
sx={{ cursor: "pointer", color: theme.palette.primary.main }}
onClick={() => handleSeeAllNotifications()}
>
See all notifications
</Box>
</ListSubheader> </ListSubheader>
<Divider /> <Divider />
{notifications.map((notification, index) => { {notifications.map((notification, index) => {
return (<Fragment key={notification.event_id ? notification.event_id + index : index }> return (
<Fragment key={notification.event_id ? notification.event_id + index : index}>
<ListItem <ListItem
onClick={() => handleSeeAllNotifications()} onClick={() => handleSeeAllNotifications()}
sx={{ sx={{
@ -156,8 +191,8 @@ export const ServerNotificationsBadge = () => {
overflow: "hidden", overflow: "hidden",
"&:hover": { "&:hover": {
backgroundColor: "action.hover", backgroundColor: "action.hover",
cursor: "pointer" cursor: "pointer",
} },
}} }}
> >
<ListItemText <ListItemText
@ -174,13 +209,16 @@ export const ServerNotificationsBadge = () => {
/> />
<ListItemText <ListItemText
primary={ primary={
<Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>{getTimeSince(notification.sent_at) + " ago"}</Typography> <Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>
{getTimeSince(notification.sent_at) + " ago"}
</Typography>
} }
/> />
</ListItem> </ListItem>
<Divider /> <Divider />
</Fragment> </Fragment>
)})} );
})}
<ListItem> <ListItem>
<Button <Button
key="clear-all-notifications" key="clear-all-notifications"
@ -190,7 +228,7 @@ export const ServerNotificationsBadge = () => {
sx={{ sx={{
pl: 0, pl: 0,
pt: 1, pt: 1,
verticalAlign: "middle" verticalAlign: "middle",
}} }}
> >
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> <DeleteIcon fontSize="small" sx={{ mr: 1 }} />

View File

@ -1,11 +1,12 @@
import { Box, Typography, Paper, Button } from "@mui/material"
import { Stack } from "@mui/material"
import { useStore } from "react-admin"
import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider"
import { useAppContext } from "../../Context";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import { getTimeSince } from "../../utils/date"; import { Box, Typography, Paper, Button } from "@mui/material";
import { Stack } from "@mui/material";
import { Tooltip } from "@mui/material"; import { Tooltip } from "@mui/material";
import { useStore } from "react-admin";
import { useAppContext } from "../../Context";
import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider";
import { getTimeSince } from "../../utils/date";
const DisplayTime = ({ date }: { date: string }) => { const DisplayTime = ({ date }: { date: string }) => {
const dateFromDateString = new Date(date.replace(" ", "T") + "Z"); const dateFromDateString = new Date(date.replace(" ", "T") + "Z");
@ -26,13 +27,17 @@ const ServerNotificationsPage = () => {
<Stack spacing={1} direction="row" alignItems="center"> <Stack spacing={1} direction="row" alignItems="center">
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}>
<Typography variant="h4">Server Notifications</Typography> <Typography variant="h4">Server Notifications</Typography>
<Button variant="text" color="error" onClick={async () => { <Button
variant="text"
color="error"
onClick={async () => {
await dataProvider.deleteServerNotifications(etkeccAdmin); await dataProvider.deleteServerNotifications(etkeccAdmin);
setServerNotifications({ setServerNotifications({
notifications: [], notifications: [],
success: true, success: true,
}); });
}}> }}
>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear
</Button> </Button>
</Box> </Box>

View File

@ -1,45 +1,47 @@
import MonitorHeartIcon from "@mui/icons-material/MonitorHeart";
import { Avatar, Badge, Box, Theme, Tooltip } from "@mui/material"; import { Avatar, Badge, Box, Theme, Tooltip } from "@mui/material";
import { useEffect } from "react";
import { useAppContext } from "../../Context";
import { Button, useDataProvider, useStore } from "react-admin";
import { styled } from '@mui/material/styles';
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
import { BadgeProps } from "@mui/material/Badge"; import { BadgeProps } from "@mui/material/Badge";
import { useNavigate } from "react-router"; import { styled } from "@mui/material/styles";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useEffect } from "react";
import { Button, useDataProvider, useStore } from "react-admin";
import { useNavigate } from "react-router";
import { useAppContext } from "../../Context";
import { ServerProcessResponse, ServerStatusResponse } from "../../synapse/dataProvider"; import { ServerProcessResponse, ServerStatusResponse } from "../../synapse/dataProvider";
interface StyledBadgeProps extends BadgeProps { interface StyledBadgeProps extends BadgeProps {
backgroundColor: string; backgroundColor: string;
badgeColor: string badgeColor: string;
theme?: Theme; theme?: Theme;
} }
const StyledBadge = styled(Badge, { shouldForwardProp: (prop) => !['badgeColor', 'backgroundColor'].includes(prop as string) })<StyledBadgeProps> const StyledBadge = styled(Badge, {
(({ theme, backgroundColor, badgeColor }) => ({ shouldForwardProp: prop => !["badgeColor", "backgroundColor"].includes(prop as string),
'& .MuiBadge-badge': { })<StyledBadgeProps>(({ theme, backgroundColor, badgeColor }) => ({
"& .MuiBadge-badge": {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
color: badgeColor, color: badgeColor,
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`, boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
'&::after': { "&::after": {
position: 'absolute', position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: "100%",
height: '100%', height: "100%",
borderRadius: '50%', borderRadius: "50%",
animation: 'ripple 2.5s infinite ease-in-out', animation: "ripple 2.5s infinite ease-in-out",
border: '1px solid currentColor', border: "1px solid currentColor",
content: '""', content: '""',
}, },
}, },
'@keyframes ripple': { "@keyframes ripple": {
'0%': { "0%": {
transform: 'scale(.8)', transform: "scale(.8)",
opacity: 1, opacity: 1,
}, },
'100%': { "100%": {
transform: 'scale(2.4)', transform: "scale(2.4)",
opacity: 0, opacity: 0,
}, },
}, },
@ -51,8 +53,16 @@ const SERVER_STATUS_INTERVAL_TIME = 5 * 60 * 1000;
const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000; const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000;
const useServerStatus = () => { const useServerStatus = () => {
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { ok: false, success: false, host: "", results: [] }); const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); ok: false,
success: false,
host: "",
results: [],
});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
@ -90,26 +100,32 @@ const useServerStatus = () => {
if (serverStatusInterval) { if (serverStatusInterval) {
clearInterval(serverStatusInterval); clearInterval(serverStatusInterval);
} }
} };
}, [etkeccAdmin, command]); }, [etkeccAdmin, command]);
return { isOkay, successCheck }; return { isOkay, successCheck };
}; };
const useCurrentServerProcess = () => { const useCurrentServerProcess = () => {
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { etkeccAdmin } = useAppContext(); const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const checkServerRunningProcess = async () => { const checkServerRunningProcess = async () => {
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin, command !== ""); const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(
etkeccAdmin,
command !== ""
);
setServerProcess({ setServerProcess({
...serverProcess, ...serverProcess,
command: serverProcess.command, command: serverProcess.command,
locked_at: serverProcess.locked_at locked_at: serverProcess.locked_at,
}); });
} };
useEffect(() => { useEffect(() => {
let serverCheckInterval: NodeJS.Timeout | null = null; let serverCheckInterval: NodeJS.Timeout | null = null;
@ -131,13 +147,23 @@ const useCurrentServerProcess = () => {
if (serverCheckInterval) { if (serverCheckInterval) {
clearInterval(serverCheckInterval); clearInterval(serverCheckInterval);
} }
} };
}, [etkeccAdmin, command]); }, [etkeccAdmin, command]);
return { command, locked_at }; return { command, locked_at };
}; };
export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar = false }: { command: string, locked_at: string, isOkay: boolean, inSidebar: boolean }) => { export const ServerStatusStyledBadge = ({
command,
locked_at,
isOkay,
inSidebar = false,
}: {
command: string;
locked_at: string;
isOkay: boolean;
inSidebar: boolean;
}) => {
const theme = useTheme(); const theme = useTheme();
let badgeBackgroundColor = isOkay ? theme.palette.success.light : theme.palette.error.main; let badgeBackgroundColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
let badgeColor = isOkay ? theme.palette.success.light : theme.palette.error.main; let badgeColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
@ -151,9 +177,10 @@ export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar
avatarBackgroundColor = theme.palette.grey[600]; avatarBackgroundColor = theme.palette.grey[600];
} }
return <StyledBadge return (
<StyledBadge
overlap="circular" overlap="circular"
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
variant="dot" variant="dot"
backgroundColor={badgeBackgroundColor} backgroundColor={badgeBackgroundColor}
badgeColor={badgeColor} badgeColor={badgeColor}
@ -162,6 +189,7 @@ export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar
<MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} /> <MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} />
</Avatar> </Avatar>
</StyledBadge> </StyledBadge>
);
}; };
const ServerStatusBadge = () => { const ServerStatusBadge = () => {
@ -183,13 +211,20 @@ const ServerStatusBadge = () => {
tooltipText = `Running: ${command}; ${tooltipText}`; tooltipText = `Running: ${command}; ${tooltipText}`;
} }
return <Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 }}}> return (
<Button onClick={handleServerStatusClick} size="medium" sx={{ minWidth: "auto", ".MuiButton-startIcon": { m: 0 } }}>
<Tooltip title={tooltipText} sx={{ cursor: "pointer" }}> <Tooltip title={tooltipText} sx={{ cursor: "pointer" }}>
<Box> <Box>
<ServerStatusStyledBadge inSidebar={false} command={command || ""} locked_at={locked_at || ""} isOkay={isOkay} /> <ServerStatusStyledBadge
inSidebar={false}
command={command || ""}
locked_at={locked_at || ""}
isOkay={isOkay}
/>
</Box> </Box>
</Tooltip> </Tooltip>
</Button> </Button>
);
}; };
export default ServerStatusBadge; export default ServerStatusBadge;

View File

@ -1,13 +1,22 @@
import { useStore } from "ra-core"; import CheckIcon from "@mui/icons-material/Check";
import { Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import EngineeringIcon from '@mui/icons-material/Engineering'; import EngineeringIcon from "@mui/icons-material/Engineering";
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider"; import { Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
import { useStore } from "ra-core";
import ServerCommandsPanel from "./ServerCommandsPanel"; import ServerCommandsPanel from "./ServerCommandsPanel";
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
import { getTimeSince } from "../../utils/date"; import { getTimeSince } from "../../utils/date";
const StatusChip = ({ isOkay, size = "medium", command }: { isOkay: boolean, size?: "small" | "medium", command?: string }) => { const StatusChip = ({
isOkay,
size = "medium",
command,
}: {
isOkay: boolean;
size?: "small" | "medium";
command?: string;
}) => {
let label = "OK"; let label = "OK";
let icon = <CheckIcon />; let icon = <CheckIcon />;
let color: ChipProps["color"] = "success"; let color: ChipProps["color"] = "success";
@ -23,9 +32,7 @@ const StatusChip = ({ isOkay, size = "medium", command }: { isOkay: boolean, siz
icon = <EngineeringIcon />; icon = <EngineeringIcon />;
} }
return ( return <Chip icon={icon} label={label} color={color} variant="outlined" size={size} />;
<Chip icon={icon} label={label} color={color} variant="outlined" size={size} />
);
}; };
const ServerComponentText = ({ text }: { text: string }) => { const ServerComponentText = ({ text }: { text: string }) => {
@ -39,14 +46,17 @@ const ServerStatusPage = () => {
host: "", host: "",
results: [], results: [],
}); });
const [ serverProcess, setServerProcess ] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" }); const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess; const { command, locked_at } = serverProcess;
const successCheck = serverStatus.success; const successCheck = serverStatus.success;
const isOkay = serverStatus.ok; const isOkay = serverStatus.ok;
const host = serverStatus.host; const host = serverStatus.host;
const results = serverStatus.results; const results = serverStatus.results;
let groupedResults: Record<string, ServerStatusComponent[]> = {}; const groupedResults: Record<string, ServerStatusComponent[]> = {};
for (const result of results) { for (const result of results) {
if (!groupedResults[result.category]) { if (!groupedResults[result.category]) {
groupedResults[result.category] = []; groupedResults[result.category] = [];
@ -59,9 +69,7 @@ const ServerStatusPage = () => {
<Paper elevation={3} sx={{ p: 3, mt: 3 }}> <Paper elevation={3} sx={{ p: 3, mt: 3 }}>
<Stack direction="row" spacing={2} alignItems="center"> <Stack direction="row" spacing={2} alignItems="center">
<CloseIcon color="error" /> <CloseIcon color="error" />
<Typography color="error"> <Typography color="error">Unable to fetch server status. Please try again later.</Typography>
Unable to fetch server status. Please try again later.
</Typography>
</Stack> </Stack>
</Paper> </Paper>
); );
@ -83,11 +91,13 @@ const ServerStatusPage = () => {
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Typography variant="h5">Currently running:</Typography> <Typography variant="h5">Currently running:</Typography>
<Typography variant="h5" color="text.secondary"> <Typography variant="h5" color="text.secondary">
<Link href={"https://etke.cc/help/extras/scheduler/#"+command} target="_blank"> <Link href={"https://etke.cc/help/extras/scheduler/#" + command} target="_blank">
{command} {command}
</Link> </Link>
<Tooltip title={locked_at.toString()}> <Tooltip title={locked_at.toString()}>
<Typography component="span" color="text.secondary" sx={{ display: "inline-block", ml: 1 }}>(started {getTimeSince(locked_at)} ago)</Typography> <Typography component="span" color="text.secondary" sx={{ display: "inline-block", ml: 1 }}>
(started {getTimeSince(locked_at)} ago)
</Typography>
</Tooltip> </Tooltip>
</Typography> </Typography>
</Box> </Box>
@ -117,8 +127,10 @@ const ServerStatusPage = () => {
<ServerComponentText text={result.label.text} /> <ServerComponentText text={result.label.text} />
)} )}
</Box> </Box>
{result.reason && <Typography color="text.secondary" dangerouslySetInnerHTML={{ __html: result.reason }}/>} {result.reason && (
{(!result.ok && result.help) && ( <Typography color="text.secondary" dangerouslySetInnerHTML={{ __html: result.reason }} />
)}
{!result.ok && result.help && (
<Link href={result.help} target="_blank" rel="noopener noreferrer" sx={{ mt: 1 }}> <Link href={result.help} target="_blank" rel="noopener noreferrer" sx={{ mt: 1 }}>
Learn more Learn more
</Link> </Link>

View File

@ -1,17 +1,26 @@
import { get } from "lodash";
import { useState } from "react";
import BlockIcon from "@mui/icons-material/Block"; import BlockIcon from "@mui/icons-material/Block";
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@mui/icons-material/Cancel";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep"; import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
import DownloadIcon from "@mui/icons-material/Download";
import DownloadingIcon from "@mui/icons-material/Downloading";
import FileOpenIcon from "@mui/icons-material/FileOpen"; import FileOpenIcon from "@mui/icons-material/FileOpen";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen"; import LockOpenIcon from "@mui/icons-material/LockOpen";
import DownloadIcon from '@mui/icons-material/Download'; import {
import DownloadingIcon from '@mui/icons-material/Downloading'; Grid2 as Grid,
import { Grid2 as Grid, Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip, Link } from "@mui/material"; Box,
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
Tooltip,
Link,
} from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles"; import { alpha, useTheme } from "@mui/material/styles";
import { useMutation } from "@tanstack/react-query";
import { get } from "lodash";
import { useState } from "react";
import { import {
BooleanInput, BooleanInput,
Button, Button,
@ -30,12 +39,11 @@ import {
useRefresh, useRefresh,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import { useMutation } from "@tanstack/react-query";
import { dateParser } from "../utils/date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider"; import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia"; import { dateParser } from "../utils/date";
import decodeURLComponent from "../utils/decodeURLComponent"; import decodeURLComponent from "../utils/decodeURLComponent";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => { const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate(); const translate = useTranslate();
@ -55,24 +63,9 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
<DialogContent> <DialogContent>
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText> <DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}> <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
<DateTimeInput <DateTimeInput source="before_ts" label="delete_media.fields.before_ts" defaultValue={0} parse={dateParser} />
source="before_ts" <NumberInput source="size_gt" label="delete_media.fields.size_gt" defaultValue={0} min={0} step={1024} />
label="delete_media.fields.before_ts" <BooleanInput source="keep_profiles" label="delete_media.fields.keep_profiles" defaultValue={true} />
defaultValue={0}
parse={dateParser}
/>
<NumberInput
source="size_gt"
label="delete_media.fields.size_gt"
defaultValue={0}
min={0}
step={1024}
/>
<BooleanInput
source="keep_profiles"
label="delete_media.fields.keep_profiles"
defaultValue={true}
/>
</SimpleForm> </SimpleForm>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@ -410,7 +403,7 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
document.body.appendChild(anchorElement); document.body.appendChild(anchorElement);
anchorElement.click(); anchorElement.click();
document.body.removeChild(anchorElement); document.body.removeChild(anchorElement);
setTimeout(() => URL.revokeObjectURL(blobURL), 10);; setTimeout(() => URL.revokeObjectURL(blobURL), 10);
}; };
const handleFile = async (preview: boolean) => { const handleFile = async (preview: boolean) => {
@ -491,7 +484,7 @@ export const MediaIDField = ({ source }) => {
mxcURL = `mxc://${homeserver}/${mediaID}`; mxcURL = `mxc://${homeserver}/${mediaID}`;
} }
return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type}/>; return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type} />;
}; };
export const ReportMediaContent = ({ source }) => { export const ReportMediaContent = ({ source }) => {
@ -510,5 +503,5 @@ export const ReportMediaContent = ({ source }) => {
uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString()); uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString());
} }
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type}/>; return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type} />;
}; };

View File

@ -1,18 +1,28 @@
import { NativeSelect, Paper } from "@mui/material"; import { NativeSelect, Paper } from "@mui/material";
import { CardContent, CardHeader, Container } from "@mui/material"; import { CardContent, CardHeader, Container } from "@mui/material";
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import { ParsedStats, Progress } from "./types";
import { ChangeEventHandler } from "react"; import { ChangeEventHandler } from "react";
const TranslatableOption = ({ value, text }: { value: string, text: string }) => { import { ParsedStats, Progress } from "./types";
const TranslatableOption = ({ value, text }: { value: string; text: string }) => {
const translate = useTranslate(); const translate = useTranslate();
return <option value={value}>{translate(text)}</option>; return <option value={value}>{translate(text)}</option>;
} };
const ConflictModeCard = ({ stats, importResults, onConflictModeChanged, conflictMode, progress }: const ConflictModeCard = ({
{ stats: ParsedStats | null, importResults: any, onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>, conflictMode: string, progress: Progress }) => { stats,
importResults,
onConflictModeChanged,
conflictMode,
progress,
}: {
stats: ParsedStats | null;
importResults: any;
onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>;
conflictMode: string;
progress: Progress;
}) => {
const translate = useTranslate(); const translate = useTranslate();
if (!stats || importResults) { if (!stats || importResults) {
@ -35,6 +45,6 @@ const ConflictModeCard = ({ stats, importResults, onConflictModeChanged, conflic
</Paper> </Paper>
</Container> </Container>
); );
} };
export default ConflictModeCard; export default ConflictModeCard;

View File

@ -1,11 +1,4 @@
import { import { Container, Paper, CardHeader, CardContent, Stack, Typography } from "@mui/material";
Container,
Paper,
CardHeader,
CardContent,
Stack,
Typography,
} from "@mui/material";
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
const ErrorsCard = ({ errors }: { errors: string[] }) => { const ErrorsCard = ({ errors }: { errors: string[] }) => {
@ -23,7 +16,7 @@ const ErrorsCard = ({ errors }: { errors: string[] }) => {
sx={{ sx={{
borderBottom: 1, borderBottom: 1,
borderColor: "error.main", borderColor: "error.main",
color: "error.main" color: "error.main",
}} }}
/> />
<CardContent> <CardContent>

View File

@ -1,10 +1,29 @@
import { Alert, Box, CardContent, CardHeader, Container, List, ListItem, ListItemText, Paper, Stack, Typography } from "@mui/material"
import { Button, Link, useTranslate } from "react-admin";
import { ImportResult } from "./types";
import DownloadIcon from "@mui/icons-material/Download";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import DownloadIcon from "@mui/icons-material/Download";
import {
Alert,
Box,
CardContent,
CardHeader,
Container,
List,
ListItem,
ListItemText,
Paper,
Stack,
Typography,
} from "@mui/material";
import { Button, Link, useTranslate } from "react-admin";
const ResultsCard = ({ importResults, downloadSkippedRecords }: { importResults: ImportResult | null, downloadSkippedRecords: () => void }) => { import { ImportResult } from "./types";
const ResultsCard = ({
importResults,
downloadSkippedRecords,
}: {
importResults: ImportResult | null;
downloadSkippedRecords: () => void;
}) => {
const translate = useTranslate(); const translate = useTranslate();
if (!importResults) { if (!importResults) {
@ -30,7 +49,7 @@ const ResultsCard = ({ importResults, downloadSkippedRecords }: { importResults:
{translate("import_users.cards.results.successful", importResults.succeededRecords.length)} {translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
</Typography> </Typography>
<List dense> <List dense>
{importResults.succeededRecords.map((record) => ( {importResults.succeededRecords.map(record => (
<ListItem key={record.id}> <ListItem key={record.id}>
<ListItemText primary={record.displayname} /> <ListItemText primary={record.displayname} />
</ListItem> </ListItem>
@ -47,8 +66,7 @@ const ResultsCard = ({ importResults, downloadSkippedRecords }: { importResults:
onClick={downloadSkippedRecords} onClick={downloadSkippedRecords}
sx={{ mt: 2 }} sx={{ mt: 2 }}
label={translate("import_users.cards.results.download_skipped")} label={translate("import_users.cards.results.download_skipped")}
> ></Button>
</Button>
</Box> </Box>
)} )}
{importResults.erroredRecords.length > 0 && ( {importResults.erroredRecords.length > 0 && (
@ -66,7 +84,9 @@ const ResultsCard = ({ importResults, downloadSkippedRecords }: { importResults:
</CardContent> </CardContent>
</Paper> </Paper>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Link to="/users"><Button variant="outlined" startIcon={<ArrowBackIcon />} label={translate("ra.action.back")} /></Link> <Link to="/users">
<Button variant="outlined" startIcon={<ArrowBackIcon />} label={translate("ra.action.back")} />
</Link>
</Box> </Box>
</Container> </Container>
); );

View File

@ -1,14 +1,25 @@
import { Button, Checkbox, Paper, Container } from "@mui/material"; import { Button, Checkbox, Paper, Container } from "@mui/material";
import { CardActions, FormControlLabel } from "@mui/material"; import { CardActions, FormControlLabel } from "@mui/material";
import { Progress, ImportLine, ImportResult } from "./types";
import { ChangeEventHandler } from "react";
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react";
const StartImportCard = ( import { Progress, ImportLine, ImportResult } from "./types";
{ csvData, importResults, progress, dryRun, onDryRunModeChanged, runImport }:
{ csvData: ImportLine[], importResults: ImportResult | null, progress: Progress, dryRun: boolean, onDryRunModeChanged: ChangeEventHandler<HTMLInputElement>, runImport: () => void } const StartImportCard = ({
) => { csvData,
importResults,
progress,
dryRun,
onDryRunModeChanged,
runImport,
}: {
csvData: ImportLine[];
importResults: ImportResult | null;
progress: Progress;
dryRun: boolean;
onDryRunModeChanged: ChangeEventHandler<HTMLInputElement>;
runImport: () => void;
}) => {
const translate = useTranslate(); const translate = useTranslate();
if (!csvData || csvData.length === 0 || importResults) { if (!csvData || csvData.length === 0 || importResults) {
return null; return null;
@ -34,6 +45,6 @@ const StartImportCard = (
</Paper> </Paper>
</Container> </Container>
); );
} };
export default StartImportCard; export default StartImportCard;

View File

@ -4,9 +4,26 @@ import { FormControlLabel } from "@mui/material";
import { Checkbox } from "@mui/material"; import { Checkbox } from "@mui/material";
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react"; import { ChangeEventHandler } from "react";
import { ParsedStats, Progress } from "./types"; import { ParsedStats, Progress } from "./types";
const StatsCard = ({ stats, progress, importResults, useridMode, passwordMode, onUseridModeChanged, onPasswordModeChange }: { stats: ParsedStats | null, progress: Progress, importResults: any, useridMode: string, passwordMode: boolean, onUseridModeChanged: ChangeEventHandler<HTMLSelectElement>, onPasswordModeChange: ChangeEventHandler<HTMLInputElement> }) => { const StatsCard = ({
stats,
progress,
importResults,
useridMode,
passwordMode,
onUseridModeChanged,
onPasswordModeChange,
}: {
stats: ParsedStats | null;
progress: Progress;
importResults: any;
useridMode: string;
passwordMode: boolean;
onUseridModeChanged: ChangeEventHandler<HTMLSelectElement>;
onPasswordModeChange: ChangeEventHandler<HTMLInputElement>;
}) => {
const translate = useTranslate(); const translate = useTranslate();
if (!stats) { if (!stats) {

View File

@ -1,10 +1,18 @@
import { CardHeader, CardContent, Container, Link, Stack, Typography, Paper } from "@mui/material"; import { CardHeader, CardContent, Container, Link, Stack, Typography, Paper } from "@mui/material";
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react"; import { ChangeEventHandler } from "react";
import { Progress } from "./types"; import { Progress } from "./types";
const UploadCard = ({ importResults, onFileChange, progress }: { importResults: any, onFileChange: ChangeEventHandler<HTMLInputElement>, progress: Progress }) => { const UploadCard = ({
importResults,
onFileChange,
progress,
}: {
importResults: any;
onFileChange: ChangeEventHandler<HTMLInputElement>;
progress: Progress;
}) => {
const translate = useTranslate(); const translate = useTranslate();
if (importResults) { if (importResults) {
return null; return null;

View File

@ -1,16 +1,14 @@
import { import { Stack } from "@mui/material";
Stack,
} from "@mui/material";
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import { Title } from "react-admin"; import { Title } from "react-admin";
import ConflictModeCard from "./ConflictModeCard";
import ErrorsCard from "./ErrorsCard";
import ResultsCard from "./ResultsCard";
import StartImportCard from "./StartImportCard";
import StatsCard from "./StatsCard";
import UploadCard from "./UploadCard"; import UploadCard from "./UploadCard";
import useImportFile from "./useImportFile"; import useImportFile from "./useImportFile";
import ErrorsCard from "./ErrorsCard";
import ConflictModeCard from "./ConflictModeCard";
import StatsCard from "./StatsCard";
import StartImportCard from "./StartImportCard";
import ResultsCard from "./ResultsCard";
const UserImport = () => { const UserImport = () => {
const { const {
@ -29,7 +27,7 @@ const UserImport = () => {
onConflictModeChanged, onConflictModeChanged,
onPasswordModeChange, onPasswordModeChange,
onUseridModeChanged, onUseridModeChanged,
downloadSkippedRecords downloadSkippedRecords,
} = useImportFile(); } = useImportFile();
const translate = useTranslate(); const translate = useTranslate();
@ -39,13 +37,33 @@ const UserImport = () => {
<Title defaultTitle={translate("import_users.title")} /> <Title defaultTitle={translate("import_users.title")} />
<UploadCard importResults={importResults} onFileChange={onFileChange} progress={progress} /> <UploadCard importResults={importResults} onFileChange={onFileChange} progress={progress} />
<ErrorsCard errors={errors} /> <ErrorsCard errors={errors} />
<ConflictModeCard stats={stats} importResults={importResults} conflictMode={conflictMode} onConflictModeChanged={onConflictModeChanged} progress={progress} /> <ConflictModeCard
<StatsCard stats={stats} progress={progress} importResults={importResults} passwordMode={passwordMode} useridMode={useridMode} onPasswordModeChange={onPasswordModeChange} onUseridModeChanged={onUseridModeChanged} /> stats={stats}
<StartImportCard csvData={csvData} importResults={importResults} progress={progress} dryRun={dryRun} onDryRunModeChanged={onDryRunModeChanged} runImport={runImport} /> importResults={importResults}
conflictMode={conflictMode}
onConflictModeChanged={onConflictModeChanged}
progress={progress}
/>
<StatsCard
stats={stats}
progress={progress}
importResults={importResults}
passwordMode={passwordMode}
useridMode={useridMode}
onPasswordModeChange={onPasswordModeChange}
onUseridModeChanged={onUseridModeChanged}
/>
<StartImportCard
csvData={csvData}
importResults={importResults}
progress={progress}
dryRun={dryRun}
onDryRunModeChanged={onDryRunModeChanged}
runImport={runImport}
/>
<ResultsCard importResults={importResults} downloadSkippedRecords={downloadSkippedRecords} /> <ResultsCard importResults={importResults} downloadSkippedRecords={downloadSkippedRecords} />
</Stack> </Stack>
); );
}; };
export default UserImport; export default UserImport;

View File

@ -14,7 +14,7 @@ export interface ImportLine {
} }
export interface ParsedStats { export interface ParsedStats {
user_types: { [key: string]: number }; user_types: Record<string, number>;
is_guest: number; is_guest: number;
admin: number; admin: number;
deactivated: number; deactivated: number;

View File

@ -1,11 +1,12 @@
import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import { useTranslate, useNotify, HttpError } from "react-admin"; import { useTranslate, useNotify, HttpError } from "react-admin";
import { parse as parseCsv, unparse as unparseCsv, ParseResult } from "papaparse";
import { ImportLine, ParsedStats, Progress, ImportResult, ChangeStats } from "./types";
import dataProvider from "../../synapse/dataProvider"; import dataProvider from "../../synapse/dataProvider";
import { returnMXID } from "../../utils/mxid"; import { returnMXID } from "../../utils/mxid";
import { generateRandomPassword } from "../../utils/password";
import { generateRandomMXID } from "../../utils/mxid"; import { generateRandomMXID } from "../../utils/mxid";
import { ImportLine, ParsedStats, Progress, ImportResult, ChangeStats } from "./types"; import { generateRandomPassword } from "../../utils/password";
const LOGGING = true; const LOGGING = true;
@ -77,7 +78,6 @@ const useImportFile = () => {
return false; return false;
} }
/* Collect some stats to prevent sneaky csv files from adding admin /* Collect some stats to prevent sneaky csv files from adding admin
users or something. users or something.
*/ */
@ -327,7 +327,7 @@ const useImportFile = () => {
translate("import_users.error.at_entry", { translate("import_users.error.at_entry", {
entry: entriesDone + 1, entry: entriesDone + 1,
message: e instanceof Error ? e.message : String(e), message: e instanceof Error ? e.message : String(e),
}) }),
]); ]);
setProgress(null); setProgress(null);
} }
@ -370,8 +370,8 @@ const useImportFile = () => {
onPasswordModeChange, onPasswordModeChange,
onUseridModeChanged, onUseridModeChanged,
onFileChange, onFileChange,
downloadSkippedRecords downloadSkippedRecords,
} };
} };
export default useImportFile; export default useImportFile;

View File

@ -29,15 +29,15 @@ const fixedGermanMessages = {
}, },
message: { message: {
...formalGermanMessages.ra.message, ...formalGermanMessages.ra.message,
access_denied: access_denied: "Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
"Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
authentication_error: authentication_error:
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.", "Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
select_all_limit_reached: "Es gibt zu viele Elemente, um sie alle auszuwählen. Es wurden nur die ersten %{max} Elemente ausgewählt.", select_all_limit_reached:
"Es gibt zu viele Elemente, um sie alle auszuwählen. Es wurden nur die ersten %{max} Elemente ausgewählt.",
placeholder_data_warning: "Netzwerkproblem: Datenaktualisierung fehlgeschlagen.", placeholder_data_warning: "Netzwerkproblem: Datenaktualisierung fehlgeschlagen.",
}, },
}, },
} };
const de: SynapseTranslationMessages = { const de: SynapseTranslationMessages = {
...fixedGermanMessages, ...fixedGermanMessages,
@ -55,7 +55,8 @@ const de: SynapseTranslationMessages = {
access_token: "Zugriffstoken", access_token: "Zugriffstoken",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.", title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.",
content: "Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?", content:
"Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?",
confirm: "Sitzung beenden", confirm: "Sitzung beenden",
cancel: "Nur vom Admin-Panel abmelden", cancel: "Nur vom Admin-Panel abmelden",
}, },
@ -205,9 +206,11 @@ const de: SynapseTranslationMessages = {
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.", password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.", create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.",
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.", deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
suspend: "Ein gesperrter Benutzer kann sich nicht mehr anmelden und wird in den schreibgeschützten Modus versetzt.", suspend:
"Ein gesperrter Benutzer kann sich nicht mehr anmelden und wird in den schreibgeschützten Modus versetzt.",
erase: "DSGVO konformes Löschen der Benutzerdaten.", erase: "DSGVO konformes Löschen der Benutzerdaten.",
erase_text: "Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.", erase_text:
"Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.", erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.", modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
username_available: "Benutzername verfügbar", username_available: "Benutzername verfügbar",
@ -227,7 +230,8 @@ const de: SynapseTranslationMessages = {
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)", redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
generate_password: "Passwort generieren", generate_password: "Passwort generieren",
overwrite_title: "Warnung!", overwrite_title: "Warnung!",
overwrite_content: "Dieser Benutzername ist bereits vergeben. Sind Sie sicher, dass Sie den vorhandenen Benutzer überschreiben möchten?", overwrite_content:
"Dieser Benutzername ist bereits vergeben. Sind Sie sicher, dass Sie den vorhandenen Benutzer überschreiben möchten?",
overwrite_cancel: "Abbrechen", overwrite_cancel: "Abbrechen",
overwrite_confirm: "Überschreiben", overwrite_confirm: "Überschreiben",
}, },
@ -241,7 +245,7 @@ const de: SynapseTranslationMessages = {
title: "Kontodaten", title: "Kontodaten",
global: "Globale", global: "Globale",
rooms: "Räume", rooms: "Räume",
} },
}, },
rooms: { rooms: {
name: "Raum |||| Räume", name: "Raum |||| Räume",
@ -304,10 +308,11 @@ const de: SynapseTranslationMessages = {
assign_admin: "Raumadministrator zuweisen", assign_admin: "Raumadministrator zuweisen",
title: "Raumadministrator zu %{roomName} zuweisen", title: "Raumadministrator zu %{roomName} zuweisen",
confirm: "Raumadministrator zuweisen", confirm: "Raumadministrator zuweisen",
content: "Geben Sie die vollständige MXID des Benutzers an, der als Administrator gesetzt werden soll.\nWarnung: Damit dies funktioniert, muss der Raum mindestens ein lokales Mitglied als Administrator haben.", content:
"Geben Sie die vollständige MXID des Benutzers an, der als Administrator gesetzt werden soll.\nWarnung: Damit dies funktioniert, muss der Raum mindestens ein lokales Mitglied als Administrator haben.",
success: "Der/die Benutzer wurde/n als Raumadministrator gesetzt.", success: "Der/die Benutzer wurde/n als Raumadministrator gesetzt.",
failure: "Der/die Benutzer konnte/n nicht als Raumadministrator gesetzt werden. %{errMsg}", failure: "Der/die Benutzer konnte/n nicht als Raumadministrator gesetzt werden. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@ -468,8 +473,8 @@ const de: SynapseTranslationMessages = {
info: "Dies ist eine Liste der Medien, die in den Raum hochgeladen wurden. Es ist nicht möglich, Medien zu löschen, die in externen Medien-Repositories hochgeladen wurden.", info: "Dies ist eine Liste der Medien, die in den Raum hochgeladen wurden. Es ist nicht möglich, Medien zu löschen, die in externen Medien-Repositories hochgeladen wurden.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "Raumverzeichnis", name: "Raumverzeichnis",

View File

@ -18,7 +18,8 @@ const en: SynapseTranslationMessages = {
access_token: "Access token", access_token: "Access token",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "You are using an existing Matrix access token.", title: "You are using an existing Matrix access token.",
content: "Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?", content:
"Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?",
confirm: "Destroy session", confirm: "Destroy session",
cancel: "Just logout from admin panel", cancel: "Just logout from admin panel",
}, },
@ -40,7 +41,7 @@ const en: SynapseTranslationMessages = {
detail: "Details", detail: "Details",
permission: "Permissions", permission: "Permissions",
media: "Media", media: "Media",
} },
}, },
reports: { tabs: { basic: "Basic", detail: "Details" } }, reports: { tabs: { basic: "Basic", detail: "Details" } },
}, },
@ -174,7 +175,8 @@ const en: SynapseTranslationMessages = {
deactivate: "You must provide a password to re-activate an account.", deactivate: "You must provide a password to re-activate an account.",
suspend: "Suspending user means they are put into a read-only mode.", suspend: "Suspending user means they are put into a read-only mode.",
erase: "Mark the user as GDPR-erased", erase: "Mark the user as GDPR-erased",
erase_text: "This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.", erase_text:
"This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
erase_admin_error: "Deleting own user is not allowed.", erase_admin_error: "Deleting own user is not allowed.",
modify_managed_user_error: "Modifying a system-managed user is not allowed.", modify_managed_user_error: "Modifying a system-managed user is not allowed.",
username_available: "Username is available", username_available: "Username is available",
@ -208,7 +210,7 @@ const en: SynapseTranslationMessages = {
title: "Account Data", title: "Account Data",
global: "Global", global: "Global",
rooms: "Rooms", rooms: "Rooms",
} },
}, },
rooms: { rooms: {
name: "Room |||| Rooms", name: "Room |||| Rooms",
@ -271,10 +273,11 @@ const en: SynapseTranslationMessages = {
assign_admin: "Assign admin", assign_admin: "Assign admin",
title: "Assign a room admin to %{roomName}", title: "Assign a room admin to %{roomName}",
confirm: "Make admin", confirm: "Make admin",
content: "Put the full MXID of the user which will be set as admin.\nWarning: for this to work, the room needs to have at least one local member as admin.", content:
"Put the full MXID of the user which will be set as admin.\nWarning: for this to work, the room needs to have at least one local member as admin.",
success: "The user has been set as room admin.", success: "The user has been set as room admin.",
failure: "The user could not be set as room admin. %{errMsg}", failure: "The user could not be set as room admin. %{errMsg}",
} },
}, },
}, },
reports: { reports: {

View File

@ -17,7 +17,8 @@ const fa: SynapseTranslationMessages = {
access_token: "توکن دسترسی", access_token: "توکن دسترسی",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.", title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.",
content: "آیا می‌خواهید این جلسه (که می‌تواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟", content:
"آیا می‌خواهید این جلسه (که می‌تواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟",
confirm: "نابودی جلسه", confirm: "نابودی جلسه",
cancel: "فقط خروج از پنل مدیریت", cancel: "فقط خروج از پنل مدیریت",
}, },
@ -166,7 +167,8 @@ const fa: SynapseTranslationMessages = {
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.", deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
suspend: "کاربران معلق نمی توانند وارد شوند و پیام های آنها به دیگران نمایش داده نمی شود.", suspend: "کاربران معلق نمی توانند وارد شوند و پیام های آنها به دیگران نمایش داده نمی شود.",
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید", erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.", erase_text:
"وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.", erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.", modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
username_available: "نام کاربری موجود", username_available: "نام کاربری موجود",
@ -186,7 +188,8 @@ const fa: SynapseTranslationMessages = {
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)", redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
generate_password: "توليد رمز عبور", generate_password: "توليد رمز عبور",
overwrite_title: "هشدار!", overwrite_title: "هشدار!",
overwrite_content: "این نام کاربری قبلا استفاده شده است. آیا مطمئن هستید که می خواهید کاربر موجود را بازنویسی کنید؟", overwrite_content:
"این نام کاربری قبلا استفاده شده است. آیا مطمئن هستید که می خواهید کاربر موجود را بازنویسی کنید؟",
overwrite_cancel: "انصراف", overwrite_cancel: "انصراف",
overwrite_confirm: "بازنویسی", overwrite_confirm: "بازنویسی",
}, },
@ -200,7 +203,7 @@ const fa: SynapseTranslationMessages = {
title: "داده های کاربر", title: "داده های کاربر",
global: "عمومی", global: "عمومی",
rooms: "اتاق ها", rooms: "اتاق ها",
} },
}, },
rooms: { rooms: {
name: "اتاق |||| اتاق ها", name: "اتاق |||| اتاق ها",
@ -263,10 +266,11 @@ const fa: SynapseTranslationMessages = {
assign_admin: "مدیر انتخاب کنید", assign_admin: "مدیر انتخاب کنید",
title: "مدیر اتاق %{roomName} را انتخاب کنید", title: "مدیر اتاق %{roomName} را انتخاب کنید",
confirm: "مدیر انتخاب کنید", confirm: "مدیر انتخاب کنید",
content: "کامل MXID کاربری را وارد کنید که به عنوان مدیر تنظیم شود.\nهشدار: برای این کار، اتاق باید حداقل یک اعضای محلی به عنوان مدیر داشته باشد.", content:
"کامل MXID کاربری را وارد کنید که به عنوان مدیر تنظیم شود.\nهشدار: برای این کار، اتاق باید حداقل یک اعضای محلی به عنوان مدیر داشته باشد.",
success: "کاربر به عنوان مدیر اتاق تنظیم شد.", success: "کاربر به عنوان مدیر اتاق تنظیم شد.",
failure: "کاربر به عنوان مدیر اتاق تنظیم نشد. %{errMsg}", failure: "کاربر به عنوان مدیر اتاق تنظیم نشد. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@ -413,8 +417,8 @@ const fa: SynapseTranslationMessages = {
info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.", info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "راهنمای اتاق", name: "راهنمای اتاق",

View File

@ -17,14 +17,20 @@ const fr: SynapseTranslationMessages = {
access_token: "Jeton d'accès", access_token: "Jeton d'accès",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Vous utilisez un jeton d'accès Matrix existant.", title: "Vous utilisez un jeton d'accès Matrix existant.",
content: "Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?", content:
"Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?",
confirm: "Détruire la session", confirm: "Détruire la session",
cancel: "Se déconnecter simplement du panneau d'administration", cancel: "Se déconnecter simplement du panneau d'administration",
}, },
}, },
users: { 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", experimental: "Expérimental", limits: "Limites", account_data: "Données du compte" }, tabs: {
sso: "Authentification unique",
experimental: "Expérimental",
limits: "Limites",
account_data: "Données du compte",
},
}, },
rooms: { rooms: {
tabs: { tabs: {
@ -168,7 +174,8 @@ const fr: SynapseTranslationMessages = {
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.",
suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.", suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.",
erase: "Marquer l'utilisateur comme effacé conformément au RGPD", erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
erase_text: "Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.", erase_text:
"Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.", erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.", modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
username_available: "Nom d'utilisateur disponible", username_available: "Nom d'utilisateur disponible",
@ -188,7 +195,8 @@ const fr: SynapseTranslationMessages = {
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)", redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
generate_password: "Générer un mot de passe", generate_password: "Générer un mot de passe",
overwrite_title: "Attention !", overwrite_title: "Attention !",
overwrite_content: "Ce nom d'utilisateur est déjà pris. Êtes-vous sûr de vouloir écraser l'utilisateur existant ?", overwrite_content:
"Ce nom d'utilisateur est déjà pris. Êtes-vous sûr de vouloir écraser l'utilisateur existant ?",
overwrite_cancel: "Annuler", overwrite_cancel: "Annuler",
overwrite_confirm: "Écraser", overwrite_confirm: "Écraser",
}, },
@ -202,7 +210,7 @@ const fr: SynapseTranslationMessages = {
title: "Données du compte", title: "Données du compte",
global: "Globales", global: "Globales",
rooms: "Salons", rooms: "Salons",
} },
}, },
rooms: { rooms: {
name: "Salon |||| Salons", name: "Salon |||| Salons",
@ -265,10 +273,11 @@ const fr: SynapseTranslationMessages = {
assign_admin: "Assigner un administrateur", assign_admin: "Assigner un administrateur",
title: "Assigner un administrateur au salon %{roomName}", title: "Assigner un administrateur au salon %{roomName}",
confirm: "Assigner un administrateur", confirm: "Assigner un administrateur",
content: "Entrez la MXID complète de l'utilisateur qui sera désigné comme administrateur.\nAttention : pour que cela fonctionne, la salle doit avoir au moins un membre local en tant qu'administrateur.", content:
"Entrez la MXID complète de l'utilisateur qui sera désigné comme administrateur.\nAttention : pour que cela fonctionne, la salle doit avoir au moins un membre local en tant qu'administrateur.",
success: "L'utilisateur a été désigné comme administrateur de la salle.", success: "L'utilisateur a été désigné comme administrateur de la salle.",
failure: "L'utilisateur n'a pas pu être désigné comme administrateur de la salle. %{errMsg}", failure: "L'utilisateur n'a pas pu être désigné comme administrateur de la salle. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@ -415,8 +424,8 @@ const fr: SynapseTranslationMessages = {
info: "Cette liste contient les médias qui ont été téléchargés dans le salon. Il n'est pas possible de supprimer les médias qui ont été téléversés dans des dépôts de médias externes.", info: "Cette liste contient les médias qui ont été téléchargés dans le salon. Il n'est pas possible de supprimer les médias qui ont été téléversés dans des dépôts de médias externes.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "Répertoire des salons", name: "Répertoire des salons",

8
src/i18n/index.d.ts vendored
View File

@ -22,7 +22,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
}; };
users: { users: {
invalid_user_id: string; invalid_user_id: string;
tabs: { sso: string; experimental: string; limits: string; account_data: string; }; tabs: { sso: string; experimental: string; limits: string; account_data: string };
}; };
rooms: { rooms: {
details?: string; // TODO: fa, fr, it, zh details?: string; // TODO: fa, fr, it, zh
@ -188,7 +188,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
support: string; support: string;
regular: string; regular: string;
system_managed: string; system_managed: string;
} };
limits: { limits: {
messages_per_second: string; messages_per_second: string;
messages_per_second_text: string; messages_per_second_text: string;
@ -199,7 +199,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
title: string; title: string;
global: string; global: string;
rooms: string; rooms: string;
} };
}; };
rooms: { rooms: {
name: string; name: string;
@ -252,7 +252,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
content: string; content: string;
fields: { fields: {
block: string; block: string;
}, };
success: string; success: string;
failure: string; failure: string;
}; };

View File

@ -17,7 +17,8 @@ const it: SynapseTranslationMessages = {
access_token: "Token di accesso", access_token: "Token di accesso",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Stai utilizzando un token di accesso Matrix esistente.", title: "Stai utilizzando un token di accesso Matrix esistente.",
content: "Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?", content:
"Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?",
confirm: "Distruggi sessione", confirm: "Distruggi sessione",
cancel: "Disconnetti solo dal pannello di amministrazione", cancel: "Disconnetti solo dal pannello di amministrazione",
}, },
@ -166,7 +167,8 @@ const it: SynapseTranslationMessages = {
deactivate: "Devi fornire una password per riattivare l'account.", deactivate: "Devi fornire una password per riattivare l'account.",
suspend: "Sospendi l'utente", suspend: "Sospendi l'utente",
erase: "Constrassegna l'utente come cancellato dal GDPR", erase: "Constrassegna l'utente come cancellato dal GDPR",
erase_text: "Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.", erase_text:
"Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.",
erase_admin_error: "Non è consentito eliminare il proprio utente.", erase_admin_error: "Non è consentito eliminare il proprio utente.",
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.", modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
username_available: "Nome utente disponibile", username_available: "Nome utente disponibile",
@ -186,7 +188,8 @@ const it: SynapseTranslationMessages = {
redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)", redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)",
generate_password: "Genera password", generate_password: "Genera password",
overwrite_title: "Attenzione!", overwrite_title: "Attenzione!",
overwrite_content: "Questo nome utente è già stato utilizzato. Sei sicuro di voler sovrascrivere l'utente esistente?", overwrite_content:
"Questo nome utente è già stato utilizzato. Sei sicuro di voler sovrascrivere l'utente esistente?",
overwrite_cancel: "Annulla", overwrite_cancel: "Annulla",
overwrite_confirm: "Sovrascrivi", overwrite_confirm: "Sovrascrivi",
}, },
@ -200,7 +203,7 @@ const it: SynapseTranslationMessages = {
title: "Dati del profilo", title: "Dati del profilo",
global: "Globale", global: "Globale",
rooms: "Stanza", rooms: "Stanza",
} },
}, },
rooms: { rooms: {
name: "Stanza |||| Stanze", name: "Stanza |||| Stanze",
@ -258,10 +261,11 @@ const it: SynapseTranslationMessages = {
assign_admin: "Assegna un amministratore", assign_admin: "Assegna un amministratore",
title: "Assegna un amministratore alla stanza %{roomName}", title: "Assegna un amministratore alla stanza %{roomName}",
confirm: "Assegna un amministratore", confirm: "Assegna un amministratore",
content: "Inserisci la MXID completa dell'utente che sarà designato come amministratore.\nAttenzione: per questo funzionare, la stanza deve avere almeno un membro locale come amministratore.", content:
"Inserisci la MXID completa dell'utente che sarà designato come amministratore.\nAttenzione: per questo funzionare, la stanza deve avere almeno un membro locale come amministratore.",
success: "L'utente è stato designato come amministratore della stanza.", success: "L'utente è stato designato come amministratore della stanza.",
failure: "L'utente non può essere designato come amministratore della stanza. %{errMsg}", failure: "L'utente non può essere designato come amministratore della stanza. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@ -408,8 +412,8 @@ const it: SynapseTranslationMessages = {
info: "Questo è un elenco dei media caricati nella stanza. Non è possibile eliminare i media caricati su repository esterni.", info: "Questo è un elenco dei media caricati nella stanza. Non è possibile eliminare i media caricati su repository esterni.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "Elenco delle stanze", name: "Elenco delle stanze",

View File

@ -23,14 +23,12 @@ const fixedRussianMessages = {
}, },
message: { message: {
...russianMessages.ra.message, ...russianMessages.ra.message,
access_denied: access_denied: "У вас нет прав доступа к этой странице.",
"У вас нет прав доступа к этой странице.", authentication_error: "Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
authentication_error:
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
select_all_limit_reached: "Слишком много элементов для выбора. Были выбраны только первые %{max} элементов.", select_all_limit_reached: "Слишком много элементов для выбора. Были выбраны только первые %{max} элементов.",
}, },
}, },
} };
const ru: SynapseTranslationMessages = { const ru: SynapseTranslationMessages = {
...fixedRussianMessages, ...fixedRussianMessages,
@ -48,14 +46,20 @@ const ru: SynapseTranslationMessages = {
access_token: "Токен доступа", access_token: "Токен доступа",
logout_acces_token_dialog: { logout_acces_token_dialog: {
title: "Вы используете существующий токен доступа Matrix.", title: "Вы используете существующий токен доступа Matrix.",
content: "Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?", content:
"Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?",
confirm: "Завершить сессию", confirm: "Завершить сессию",
cancel: "Просто выйти из панели администрирования", cancel: "Просто выйти из панели администрирования",
}, },
}, },
users: { users: {
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.", invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения", account_data: "Данные пользователя" }, tabs: {
sso: "SSO",
experimental: "Экспериментальные",
limits: "Ограничения",
account_data: "Данные пользователя",
},
}, },
rooms: { rooms: {
details: "Данные комнаты", details: "Данные комнаты",
@ -206,9 +210,11 @@ const ru: SynapseTranslationMessages = {
password: "Смена пароля завершит все сессии пользователя.", password: "Смена пароля завершит все сессии пользователя.",
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.", create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.", deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
suspend: "Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.", suspend:
"Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR", erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.", erase_text:
"Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
erase_admin_error: "Удаление собственного пользователя запрещено.", erase_admin_error: "Удаление собственного пользователя запрещено.",
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.", modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
username_available: "Имя пользователя доступно", username_available: "Имя пользователя доступно",
@ -228,7 +234,8 @@ const ru: SynapseTranslationMessages = {
redact_events: "Удаление всех событий, отправленных пользователем (-ами)", redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
generate_password: "Сгенерировать пароль", generate_password: "Сгенерировать пароль",
overwrite_title: "Предупреждение!", overwrite_title: "Предупреждение!",
overwrite_content: "Это имя пользователя уже занято. Вы уверены, что хотите перезаписать существующего пользователя?", overwrite_content:
"Это имя пользователя уже занято. Вы уверены, что хотите перезаписать существующего пользователя?",
overwrite_cancel: "Отмена", overwrite_cancel: "Отмена",
overwrite_confirm: "Перезаписать", overwrite_confirm: "Перезаписать",
}, },
@ -242,7 +249,7 @@ const ru: SynapseTranslationMessages = {
title: "Данные пользователя", title: "Данные пользователя",
global: "Глобальные", global: "Глобальные",
rooms: "Комнаты", rooms: "Комнаты",
} },
}, },
rooms: { rooms: {
name: "Комната |||| Комнаты", name: "Комната |||| Комнаты",
@ -308,10 +315,11 @@ const ru: SynapseTranslationMessages = {
assign_admin: "Назначить администратора", assign_admin: "Назначить администратора",
title: "Назначить администратора комнате %{roomName}", title: "Назначить администратора комнате %{roomName}",
confirm: "Назначить администратора", confirm: "Назначить администратора",
content: "Введите полную MXID пользователя, которого нужно назначить администратором.\nПредупреждение: для этого должен быть назначен хотя бы один локальный участник в качестве администратора.", content:
"Введите полную MXID пользователя, которого нужно назначить администратором.\nПредупреждение: для этого должен быть назначен хотя бы один локальный участник в качестве администратора.",
success: "Пользователь назначен администратором комнаты.", success: "Пользователь назначен администратором комнаты.",
failure: "Пользователь не может быть назначен администратором комнаты. %{errMsg}", failure: "Пользователь не может быть назначен администратором комнаты. %{errMsg}",
} },
}, },
}, },
reports: { reports: {
@ -471,8 +479,8 @@ const ru: SynapseTranslationMessages = {
info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.", info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
room_directory: { room_directory: {
name: "Каталог комнат", name: "Каталог комнат",

View File

@ -27,15 +27,13 @@ const fixedChineseMessages = {
}, },
message: { message: {
...chineseMessages.ra.message, ...chineseMessages.ra.message,
access_denied: access_denied: "您没有访问此页面的权限。",
"您没有访问此页面的权限。", authentication_error: "身份验证服务器返回错误,无法验证您的凭据。",
authentication_error:
"身份验证服务器返回错误,无法验证您的凭据。",
select_all_limit_reached: "选择的元素太多。只选择了前 %{max} 个元素。", select_all_limit_reached: "选择的元素太多。只选择了前 %{max} 个元素。",
placeholder_data_warning: "网络问题:数据刷新失败。", placeholder_data_warning: "网络问题:数据刷新失败。",
}, },
}, },
} };
const zh: SynapseTranslationMessages = { const zh: SynapseTranslationMessages = {
...fixedChineseMessages, ...fixedChineseMessages,
@ -199,7 +197,8 @@ const zh: SynapseTranslationMessages = {
deactivate: "您必须提供一串密码来激活账户。", deactivate: "您必须提供一串密码来激活账户。",
suspend: "您必须提供一串密码来暂停账户。", suspend: "您必须提供一串密码来暂停账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了", erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。", erase_text:
"这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
erase_admin_error: "不允许删除自己的用户", erase_admin_error: "不允许删除自己的用户",
modify_managed_user_error: "不允许修改系统管理的用户。", modify_managed_user_error: "不允许修改系统管理的用户。",
username_available: "用户名可用", username_available: "用户名可用",
@ -233,7 +232,7 @@ const zh: SynapseTranslationMessages = {
title: "账户数据", title: "账户数据",
global: "全局", global: "全局",
rooms: "房间", rooms: "房间",
} },
}, },
rooms: { rooms: {
name: "房间", name: "房间",
@ -384,8 +383,8 @@ const zh: SynapseTranslationMessages = {
info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。", info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。",
}, },
action: { action: {
error: "%{errcode} (%{errstatus}) %{error}" error: "%{errcode} (%{errstatus}) %{error}",
} },
}, },
}, },
}; };

View File

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

View File

@ -1,6 +1,5 @@
import polyglotI18nProvider from "ra-i18n-polyglot";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { AdminContext } from "react-admin"; import { AdminContext } from "react-admin";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
@ -37,9 +36,7 @@ describe("LoginForm", () => {
it("renders with single restricted homeserver", () => { it("renders with single restricted homeserver", () => {
render( render(
<BrowserRouter> <BrowserRouter>
<AppContext.Provider <AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}>
value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}
>
<AdminContext i18nProvider={i18nProvider}> <AdminContext i18nProvider={i18nProvider}>
<LoginPage /> <LoginPage />
</AdminContext> </AdminContext>

View File

@ -1,6 +1,17 @@
import {
Avatar,
Box,
Button,
Card,
CardActions,
CircularProgress,
MenuItem,
Select,
Tab,
Tabs,
Typography,
} from "@mui/material";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
import { import {
Form, Form,
FormDataConsumer, FormDataConsumer,
@ -15,8 +26,10 @@ import {
useLocales, useLocales,
} from "react-admin"; } from "react-admin";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import LoginFormBox from "../components/LoginFormBox";
import { useAppContext } from "../Context"; import { useAppContext } from "../Context";
import Footer from "../components/Footer";
import LoginFormBox from "../components/LoginFormBox";
import { import {
getServerVersion, getServerVersion,
getSupportedFeatures, getSupportedFeatures,
@ -25,7 +38,6 @@ import {
isValidBaseUrl, isValidBaseUrl,
splitMxid, splitMxid,
} from "../synapse/matrix"; } from "../synapse/matrix";
import Footer from "../components/Footer";
export type LoginMethod = "credentials" | "accessToken"; export type LoginMethod = "credentials" | "accessToken";
@ -47,7 +59,7 @@ const LoginPage = () => {
const translate = useTranslate(); const translate = useTranslate();
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url"); const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState(""); const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = new URLSearchParams(window.location.search).get("loginToken") const loginToken = new URLSearchParams(window.location.search).get("loginToken");
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials"); const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
useEffect(() => { useEffect(() => {
@ -57,8 +69,8 @@ const LoginPage = () => {
console.log("SSO token is", loginToken); console.log("SSO token is", loginToken);
// Prevent further requests // Prevent further requests
const previousUrl = new URL(window.location.toString()) const previousUrl = new URL(window.location.toString());
previousUrl.searchParams.delete("loginToken") previousUrl.searchParams.delete("loginToken");
window.history.replaceState({}, "", previousUrl.toString()); window.history.replaceState({}, "", previousUrl.toString());
const baseUrl = localStorage.getItem("sso_base_url"); const baseUrl = localStorage.getItem("sso_base_url");
localStorage.removeItem("sso_base_url"); localStorage.removeItem("sso_base_url");
@ -281,7 +293,7 @@ const LoginPage = () => {
{loading ? ( {loading ? (
<CircularProgress size={25} thickness={2} /> <CircularProgress size={25} thickness={2} />
) : ( ) : (
<Avatar sx={{ width: "120px", height: "120px" }} src="./images/logo.webp"/> <Avatar sx={{ width: "120px", height: "120px" }} src="./images/logo.webp" />
)} )}
</Box> </Box>
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box> <Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
@ -300,7 +312,8 @@ const LoginPage = () => {
))} ))}
</Select> </Select>
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer> <FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
{loginMethod === "credentials" && <CardActions className="actions"> {loginMethod === "credentials" && (
<CardActions className="actions">
<Button <Button
variant="contained" variant="contained"
type="submit" type="submit"
@ -319,18 +332,15 @@ const LoginPage = () => {
> >
{translate("synapseadmin.auth.sso_sign_in")} {translate("synapseadmin.auth.sso_sign_in")}
</Button> </Button>
</CardActions>} </CardActions>
{loginMethod === "accessToken" && <CardActions className="actions"> )}
<Button {loginMethod === "accessToken" && (
variant="contained" <CardActions className="actions">
type="submit" <Button variant="contained" type="submit" color="primary" disabled={loading} fullWidth>
color="primary"
disabled={loading}
fullWidth
>
{translate("ra.auth.sign_in")} {translate("ra.auth.sign_in")}
</Button> </Button>
</CardActions>} </CardActions>
)}
</Box> </Box>
</Card> </Card>
</LoginFormBox> </LoginFormBox>

View File

@ -1,10 +1,10 @@
import { MouseEvent } from "react";
import AutorenewIcon from "@mui/icons-material/Autorenew"; import AutorenewIcon from "@mui/icons-material/Autorenew";
import DestinationsIcon from "@mui/icons-material/CloudQueue"; import DestinationsIcon from "@mui/icons-material/CloudQueue";
import ErrorIcon from "@mui/icons-material/Error";
import FolderSharedIcon from "@mui/icons-material/FolderShared"; import FolderSharedIcon from "@mui/icons-material/FolderShared";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import ErrorIcon from '@mui/icons-material/Error'; import { get } from "lodash";
import { MouseEvent } from "react";
import { import {
Button, Button,
Datagrid, Datagrid,
@ -34,7 +34,6 @@ import {
} from "react-admin"; } from "react-admin";
import { DATE_FORMAT } from "../utils/date"; import { DATE_FORMAT } from "../utils/date";
import { get } from "lodash";
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@ -101,19 +100,18 @@ const RetryDateField = (props: DateFieldProps) => {
return <DateField {...props} />; return <DateField {...props} />;
}; };
const destinationFieldRender = (record: RaRecord) => { const destinationFieldRender = (record: RaRecord) => {
if (record.retry_last_ts > 0) { if (record.retry_last_ts > 0) {
return ( return (
<> <>
<ErrorIcon fontSize="inherit" color="error" sx={{verticalAlign: "middle"}}/> <ErrorIcon fontSize="inherit" color="error" sx={{ verticalAlign: "middle" }} />
{record.destination} {record.destination}
</> </>
); );
} }
return <> {record.destination} </>; return <> {record.destination} </>;
} };
export const DestinationList = (props: ListProps) => { export const DestinationList = (props: ListProps) => {
const record = useRecordContext(props); const record = useRecordContext(props);
@ -126,11 +124,28 @@ export const DestinationList = (props: ListProps) => {
perPage={50} perPage={50}
> >
<DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}> <DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<FunctionField source="destination" render={destinationFieldRender} label="resources.destinations.fields.destination" /> <FunctionField
<DateField source="failure_ts" showTime options={DATE_FORMAT} label="resources.destinations.fields.failure_ts" /> source="destination"
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} label="resources.destinations.fields.retry_last_ts" /> render={destinationFieldRender}
label="resources.destinations.fields.destination"
/>
<DateField
source="failure_ts"
showTime
options={DATE_FORMAT}
label="resources.destinations.fields.failure_ts"
/>
<RetryDateField
source="retry_last_ts"
showTime
options={DATE_FORMAT}
label="resources.destinations.fields.retry_last_ts"
/>
<TextField source="retry_interval" label="resources.destinations.fields.retry_interval" /> <TextField source="retry_interval" label="resources.destinations.fields.retry_interval" />
<TextField source="last_successful_stream_ordering" label="resources.destinations.fields.last_successful_stream_ordering" /> <TextField
source="last_successful_stream_ordering"
label="resources.destinations.fields.last_successful_stream_ordering"
/>
<DestinationReconnectButton /> <DestinationReconnectButton />
</DatagridConfigurable> </DatagridConfigurable>
</List> </List>

View File

@ -41,11 +41,17 @@ export const RegistrationTokenList = (props: ListProps) => (
perPage={50} perPage={50}
> >
<DatagridConfigurable rowClick="edit"> <DatagridConfigurable rowClick="edit">
<TextField source="token" sortable={false} label="resources.registration_tokens.fields.token"/> <TextField source="token" sortable={false} label="resources.registration_tokens.fields.token" />
<NumberField source="uses_allowed" sortable={false} label="resources.registration_tokens.fields.uses_allowed" /> <NumberField source="uses_allowed" sortable={false} label="resources.registration_tokens.fields.uses_allowed" />
<NumberField source="pending" sortable={false} label="resources.registration_tokens.fields.pending" /> <NumberField source="pending" sortable={false} label="resources.registration_tokens.fields.pending" />
<NumberField source="completed" sortable={false} label="resources.registration_tokens.fields.completed" /> <NumberField source="completed" sortable={false} label="resources.registration_tokens.fields.completed" />
<DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} label="resources.registration_tokens.fields.expiry_time" /> <DateField
source="expiry_time"
showTime
options={DATE_FORMAT}
sortable={false}
label="resources.registration_tokens.fields.expiry_time"
/>
</DatagridConfigurable> </DatagridConfigurable>
</List> </List>
); );

View File

@ -22,8 +22,8 @@ import {
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import { DATE_FORMAT } from "../utils/date";
import { ReportMediaContent } from "../components/media"; import { ReportMediaContent } from "../components/media";
import { DATE_FORMAT } from "../utils/date";
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@ -93,7 +93,13 @@ export const ReportList = (props: ListProps) => (
<List {...props} pagination={<ReportPagination />} perPage={50} sort={{ field: "received_ts", order: "DESC" }}> <List {...props} pagination={<ReportPagination />} perPage={50} sort={{ field: "received_ts", order: "DESC" }}>
<DatagridConfigurable rowClick="show" bulkActionButtons={false}> <DatagridConfigurable rowClick="show" bulkActionButtons={false}>
<TextField source="id" sortable={false} label="resources.reports.fields.id" /> <TextField source="id" sortable={false} label="resources.reports.fields.id" />
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} label="resources.reports.fields.received_ts" /> <DateField
source="received_ts"
showTime
options={DATE_FORMAT}
sortable={true}
label="resources.reports.fields.received_ts"
/>
<TextField sortable={false} source="user_id" label="resources.reports.fields.user_id" /> <TextField sortable={false} source="user_id" label="resources.reports.fields.user_id" />
<TextField sortable={false} source="name" label="resources.reports.fields.name" /> <TextField sortable={false} source="name" label="resources.reports.fields.name" />
<TextField sortable={false} source="score" label="resources.reports.fields.score" /> <TextField sortable={false} source="score" label="resources.reports.fields.score" />

View File

@ -1,4 +1,5 @@
import RoomDirectoryIcon from "@mui/icons-material/FolderShared"; import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
import { useMutation } from "@tanstack/react-query";
import { import {
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
@ -25,9 +26,9 @@ import {
useRefresh, useRefresh,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "@tanstack/react-query";
import AvatarField from "../components/AvatarField";
import { MakeAdminBtn } from "./rooms"; import { MakeAdminBtn } from "./rooms";
import AvatarField from "../components/AvatarField";
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />; const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => { export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
@ -141,11 +142,7 @@ export const RoomDirectoryList = () => (
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />} bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
omit={["room_id", "canonical_alias", "topic"]} omit={["room_id", "canonical_alias", "topic"]}
> >
<AvatarField <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} label="resources.rooms.fields.avatar" />
source="avatar_src"
sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar"
/>
<TextField source="name" sortable={false} label="resources.rooms.fields.name" /> <TextField source="name" sortable={false} label="resources.rooms.fields.name" />
<TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" /> <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="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" />

View File

@ -4,12 +4,19 @@ import UserIcon from "@mui/icons-material/Group";
import HttpsIcon from "@mui/icons-material/Https"; import HttpsIcon from "@mui/icons-material/Https";
import NoEncryptionIcon from "@mui/icons-material/NoEncryption"; import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
import PageviewIcon from "@mui/icons-material/Pageview"; import PageviewIcon from "@mui/icons-material/Pageview";
import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonIcon from "@mui/icons-material/Person";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import RoomIcon from "@mui/icons-material/ViewList"; import RoomIcon from "@mui/icons-material/ViewList";
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import PermMediaIcon from "@mui/icons-material/PermMedia"; import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { import {
BooleanField, BooleanField,
DateField, DateField,
@ -40,27 +47,20 @@ import {
useNotify, useNotify,
DeleteButton, DeleteButton,
} from "react-admin"; } from "react-admin";
import { useDataProvider } from "react-admin";
import { Confirm } from "react-admin";
import TextField from "@mui/material/TextField";
import { import {
RoomDirectoryBulkUnpublishButton, RoomDirectoryBulkUnpublishButton,
RoomDirectoryBulkPublishButton, RoomDirectoryBulkPublishButton,
RoomDirectoryUnpublishButton, RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton, RoomDirectoryPublishButton,
} from "./room_directory"; } from "./room_directory";
import { DATE_FORMAT } from "../utils/date"; import AvatarField from "../components/AvatarField";
import DeleteRoomButton from "../components/DeleteRoomButton"; import DeleteRoomButton from "../components/DeleteRoomButton";
import { MediaIDField } from "../components/media"; import { MediaIDField } from "../components/media";
import AvatarField from "../components/AvatarField";
import { Room } from "../synapse/dataProvider"; import { Room } from "../synapse/dataProvider";
import { useMutation } from "@tanstack/react-query"; import { DATE_FORMAT } from "../utils/date";
import { useDataProvider } from "react-admin";
import { Confirm } from "react-admin";
import { useState } from "react";
import Button from "@mui/material/Button";
import PersonIcon from '@mui/icons-material/Person';
import Typography from "@mui/material/Typography";
import Alert from "@mui/material/Alert";
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@ -110,7 +110,6 @@ export const MakeAdminBtn = () => {
return null; return null;
} }
const ownMXID = localStorage.getItem("user_id") || ""; const ownMXID = localStorage.getItem("user_id") || "";
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [userIdValue, setUserIdValue] = useState(ownMXID); const [userIdValue, setUserIdValue] = useState(ownMXID);
@ -134,12 +133,12 @@ export const MakeAdminBtn = () => {
setOpen(false); setOpen(false);
setUserIdValue(""); setUserIdValue("");
}, },
onError: (err) => { onError: err => {
const errorMessage = err instanceof Error ? err.message : "Unknown error"; const errorMessage = err instanceof Error ? err.message : "Unknown error";
notify("resources.rooms.action.make_admin.failure", { type: "error", messageArgs: { errMsg: errorMessage } }); notify("resources.rooms.action.make_admin.failure", { type: "error", messageArgs: { errMsg: errorMessage } });
setOpen(false); setOpen(false);
setUserIdValue(""); setUserIdValue("");
} },
}); });
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -161,8 +160,16 @@ export const MakeAdminBtn = () => {
} }
}; };
return (<> return (
<Button size="small" onClick={(e) => { e.stopPropagation(); setOpen(true); }} disabled={isPending}> <>
<Button
size="small"
onClick={e => {
e.stopPropagation();
setOpen(true);
}}
disabled={isPending}
>
<PersonIcon /> {translate("resources.rooms.action.make_admin.assign_admin")} <PersonIcon /> {translate("resources.rooms.action.make_admin.assign_admin")}
</Button> </Button>
<Confirm <Confirm
@ -171,9 +178,14 @@ export const MakeAdminBtn = () => {
onClose={handleDialogClose} onClose={handleDialogClose}
confirm="resources.rooms.action.make_admin.confirm" confirm="resources.rooms.action.make_admin.confirm"
cancel="ra.action.cancel" cancel="ra.action.cancel"
title={translate("resources.rooms.action.make_admin.title", { roomName: record.name ? record.name : record.room_id })} title={translate("resources.rooms.action.make_admin.title", {
content={<> roomName: record.name ? record.name : record.room_id,
<Typography sx={{ marginBottom: 2, whiteSpace: "pre-line"}}>{translate("resources.rooms.action.make_admin.content")}</Typography> })}
content={
<>
<Typography sx={{ marginBottom: 2, whiteSpace: "pre-line" }}>
{translate("resources.rooms.action.make_admin.content")}
</Typography>
<TextField <TextField
type="text" type="text"
variant="filled" variant="filled"
@ -182,9 +194,11 @@ export const MakeAdminBtn = () => {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
label={"Matrix ID"} label={"Matrix ID"}
/> />
</>} </>
}
/> />
</>); </>
);
}; };
export const RoomShow = (props: ShowProps) => { export const RoomShow = (props: ShowProps) => {
@ -194,11 +208,7 @@ export const RoomShow = (props: ShowProps) => {
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}> <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
<TabbedShowLayout> <TabbedShowLayout>
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}> <Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
<AvatarField <AvatarField source="avatar" sx={{ height: "120px", width: "120px" }} label="resources.rooms.fields.avatar" />
source="avatar"
sx={{ height: "120px", width: "120px" }}
label="resources.rooms.fields.avatar"
/>
<RaTextField source="room_id" /> <RaTextField source="room_id" />
<RaTextField source="name" /> <RaTextField source="name" />
<RaTextField source="topic" /> <RaTextField source="topic" />
@ -301,7 +311,11 @@ export const RoomShow = (props: ShowProps) => {
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}> <Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
<RaTextField source="type" sortable={false} /> <RaTextField 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} />
<FunctionField source="content" sortable={false} render={record => `${JSON.stringify(record.content, null, 2)}`} /> <FunctionField
source="content"
sortable={false}
render={record => `${JSON.stringify(record.content, null, 2)}`}
/>
<ReferenceField source="sender" reference="users" sortable={false}> <ReferenceField source="sender" reference="users" sortable={false}>
<RaTextField source="id" /> <RaTextField source="id" />
</ReferenceField> </ReferenceField>
@ -373,7 +387,13 @@ export const RoomList = (props: ListProps) => {
bulkActionButtons={<RoomBulkActionButtons />} bulkActionButtons={<RoomBulkActionButtons />}
omit={["joined_local_members", "state_events", "version", "federatable"]} omit={["joined_local_members", "state_events", "version", "federatable"]}
> >
<ReferenceField reference="rooms" source="id" label="resources.users.fields.avatar" link={false} sortable={false}> <ReferenceField
reference="rooms"
source="id"
label="resources.users.fields.avatar"
link={false}
sortable={false}
>
<AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} />
</ReferenceField> </ReferenceField>
<RaTextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> <RaTextField source="id" label="resources.rooms.fields.room_id" sortable={false} />
@ -390,7 +410,11 @@ export const RoomList = (props: ListProps) => {
}} }}
/> />
</WrapperField> </WrapperField>
<FunctionField source="name" render={record => record["name"] || record["canonical_alias"] || record["id"]} label="resources.rooms.fields.name" /> <FunctionField
source="name"
render={record => record["name"] || record["canonical_alias"] || record["id"]}
label="resources.rooms.fields.name"
/>
<RaTextField source="joined_members" label="resources.rooms.fields.joined_members" /> <RaTextField source="joined_members" label="resources.rooms.fields.joined_members" />
<RaTextField source="joined_local_members" label="resources.rooms.fields.joined_local_members" /> <RaTextField source="joined_local_members" label="resources.rooms.fields.joined_local_members" />
<RaTextField source="state_events" label="resources.rooms.fields.state_events" /> <RaTextField source="state_events" label="resources.rooms.fields.state_events" />

View File

@ -1,19 +1,19 @@
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd"; import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import ContactMailIcon from "@mui/icons-material/ContactMail"; import ContactMailIcon from "@mui/icons-material/ContactMail";
import DevicesIcon from "@mui/icons-material/Devices"; import DevicesIcon from "@mui/icons-material/Devices";
import DocumentScannerIcon from "@mui/icons-material/DocumentScanner";
import GetAppIcon from "@mui/icons-material/GetApp"; import GetAppIcon from "@mui/icons-material/GetApp";
import UserIcon from "@mui/icons-material/Group"; import UserIcon from "@mui/icons-material/Group";
import LockClockIcon from "@mui/icons-material/LockClock";
import NotificationsIcon from "@mui/icons-material/Notifications"; import NotificationsIcon from "@mui/icons-material/Notifications";
import PermMediaIcon from "@mui/icons-material/PermMedia"; import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonPinIcon from "@mui/icons-material/PersonPin"; import PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import ScienceIcon from "@mui/icons-material/Science"; import ScienceIcon from "@mui/icons-material/Science";
import LockClockIcon from '@mui/icons-material/LockClock'; import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import DocumentScannerIcon from "@mui/icons-material/DocumentScanner";
import { useEffect, useState } from "react";
import { Alert, Typography } from "@mui/material"; import { Alert, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { import {
ArrayInput, ArrayInput,
ArrayField, ArrayField,
@ -67,23 +67,23 @@ import {
useCreate, useCreate,
useRedirect, useRedirect,
} from "react-admin"; } from "react-admin";
import { useFormContext } from "react-hook-form";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { MakeAdminBtn } from "./rooms";
import AvatarField from "../components/AvatarField"; import AvatarField from "../components/AvatarField";
import DeleteUserButton from "../components/DeleteUserButton"; import DeleteUserButton from "../components/DeleteUserButton";
import { isASManaged } from "../utils/mxid";
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { DATE_FORMAT } from "../utils/date";
import DeviceRemoveButton from "../components/DeviceRemoveButton"; import DeviceRemoveButton from "../components/DeviceRemoveButton";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
import { generateRandomPassword } from "../utils/password";
import { useFormContext } from "react-hook-form";
import ExperimentalFeaturesList from "../components/ExperimentalFeatures"; import ExperimentalFeaturesList from "../components/ExperimentalFeatures";
import UserRateLimits from "../components/UserRateLimits"; import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
import { MakeAdminBtn } from "./rooms";
import UserAccountData from "../components/UserAccountData"; import UserAccountData from "../components/UserAccountData";
import UserRateLimits from "../components/UserRateLimits";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
import { DATE_FORMAT } from "../utils/date";
import decodeURLComponent from "../utils/decodeURLComponent"; import decodeURLComponent from "../utils/decodeURLComponent";
import { isASManaged } from "../utils/mxid";
import { generateRandomPassword } from "../utils/password";
const choices_medium = [ const choices_medium = [
{ id: "email", name: "resources.users.email" }, { id: "email", name: "resources.users.email" },
@ -118,7 +118,11 @@ const userFilters = [
<BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />, <BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />,
]; ];
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean; asManagedUserIsSelected: boolean }> = props => { const UserPreventSelfDelete: React.FC<{
children: React.ReactNode;
ownUserIsSelected: boolean;
asManagedUserIsSelected: boolean;
}> = props => {
const ownUserIsSelected = props.ownUserIsSelected; const ownUserIsSelected = props.ownUserIsSelected;
const asManagedUserIsSelected = props.asManagedUserIsSelected; const asManagedUserIsSelected = props.asManagedUserIsSelected;
const notify = useNotify(); const notify = useNotify();
@ -177,7 +181,12 @@ export const UserList = (props: ListProps) => (
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`} rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
bulkActionButtons={<UserBulkActionButtons />} bulkActionButtons={<UserBulkActionButtons />}
> >
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" label="resources.users.fields.avatar" /> <AvatarField
source="avatar_src"
sx={{ height: "40px", width: "40px" }}
sortBy="avatar_url"
label="resources.users.fields.avatar"
/>
<TextField source="id" sortBy="name" label="resources.users.fields.id" /> <TextField source="id" sortBy="name" label="resources.users.fields.id" />
<TextField source="displayname" label="resources.users.fields.displayname" /> <TextField source="displayname" label="resources.users.fields.displayname" />
<BooleanField source="is_guest" label="resources.users.fields.is_guest" /> <BooleanField source="is_guest" label="resources.users.fields.is_guest" />
@ -235,18 +244,28 @@ export const UserCreate = (props: CreateProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [userIsAvailable, setUserIsAvailable] = useState<boolean | undefined>(); const [userIsAvailable, setUserIsAvailable] = useState<boolean | undefined>();
const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(<Typography component="span"></Typography>); const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(
<Typography component="span"></Typography>
);
const [formData, setFormData] = useState<Record<string, any>>({}); const [formData, setFormData] = useState<Record<string, any>>({});
const [create] = useCreate(); const [create] = useCreate();
const checkAvailability = async(event: React.FocusEvent<HTMLInputElement>) => { const checkAvailability = async (event: React.FocusEvent<HTMLInputElement>) => {
const username = event.target.value; const username = event.target.value;
const result: UsernameAvailabilityResult = await dataProvider.checkUsernameAvailability(username); const result: UsernameAvailabilityResult = await dataProvider.checkUsernameAvailability(username);
setUserIsAvailable(!!result?.available); setUserIsAvailable(!!result?.available);
if (result?.available) { if (result?.available) {
setUserAvailabilityEl(<Typography component="span" variant="body2" sx={{ color: theme.palette.success.main }}> {translate("resources.users.helper.username_available")}</Typography>); setUserAvailabilityEl(
<Typography component="span" variant="body2" sx={{ color: theme.palette.success.main }}>
{translate("resources.users.helper.username_available")}
</Typography>
);
} else { } else {
setUserAvailabilityEl(<Typography component="span" variant="body2" sx={{ color: theme.palette.warning.main }}> {result?.error || "unknown error"}</Typography>); setUserAvailabilityEl(
<Typography component="span" variant="body2" sx={{ color: theme.palette.warning.main }}>
{result?.error || "unknown error"}
</Typography>
);
} }
}; };
@ -257,12 +276,18 @@ export const UserCreate = (props: CreateProps) => {
return; return;
} }
create("users", { data: data }, { create(
"users",
{ data: data },
{
onSuccess: (resource: User) => { onSuccess: (resource: User) => {
notify("ra.notification.created", { messageArgs: { smart_count: 1 } }); notify("ra.notification.created", { messageArgs: { smart_count: 1 } });
redirect(() => { return `users/${resource.id}` }); redirect(() => {
} return `users/${resource.id}`;
}); });
},
}
);
}; };
const handleConfirm = () => { const handleConfirm = () => {
@ -275,21 +300,30 @@ export const UserCreate = (props: CreateProps) => {
}; };
const updateUser = () => { const updateUser = () => {
create("users", { data: formData }, { create(
"users",
{ data: formData },
{
onSuccess: (resource: User) => { onSuccess: (resource: User) => {
notify("ra.notification.updated", { messageArgs: { smart_count: 1 } }); notify("ra.notification.updated", { messageArgs: { smart_count: 1 } });
redirect(() => { return `users/${resource.id}` }); redirect(() => {
} return `users/${resource.id}`;
}); });
},
} }
);
};
return <Create return (
{...props} <Create {...props}>
> <SimpleForm onSubmit={postSave}>
<SimpleForm <TextInput
onSubmit={postSave} source="id"
> autoComplete="off"
<TextInput source="id" autoComplete="off" validate={validateUser} onBlur={checkAvailability} helperText={userAvailabilityEl}/> validate={validateUser}
onBlur={checkAvailability}
helperText={userAvailabilityEl}
/>
<TextInput source="displayname" validate={maxLength(256)} /> <TextInput source="displayname" validate={maxLength(256)} />
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" /> <UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable /> <SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
@ -317,6 +351,7 @@ export const UserCreate = (props: CreateProps) => {
cancel="resources.users.action.overwrite_cancel" cancel="resources.users.action.overwrite_cancel"
/> />
</Create> </Create>
);
}; };
const UserTitle = () => { const UserTitle = () => {
@ -326,7 +361,7 @@ const UserTitle = () => {
return null; return null;
} }
let username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : "" const username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : "";
return ( return (
<span> <span>
{translate("resources.users.name", { {translate("resources.users.name", {
@ -352,7 +387,10 @@ const UserEditToolbar = () => {
<div className={ToolbarClasses.defaultToolbar}> <div className={ToolbarClasses.defaultToolbar}>
<Toolbar sx={{ justifyContent: "space-between" }}> <Toolbar sx={{ justifyContent: "space-between" }}>
<SaveButton /> <SaveButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}> <UserPreventSelfDelete
ownUserIsSelected={ownUserIsSelected}
asManagedUserIsSelected={asManagedUserIsSelected}
>
<DeleteButton /> <DeleteButton />
</UserPreventSelfDelete> </UserPreventSelfDelete>
</Toolbar> </Toolbar>
@ -395,7 +433,17 @@ const UserPasswordInput = props => {
return ( return (
<> <>
<PasswordInput {...props} helperText={asManagedUserIsSelected ? "resources.users.helper.modify_managed_user_error" : (record ? "resources.users.helper.password" : "resources.users.helper.create_password")} disabled={asManagedUserIsSelected} /> <PasswordInput
{...props}
helperText={
asManagedUserIsSelected
? "resources.users.helper.modify_managed_user_error"
: record
? "resources.users.helper.password"
: "resources.users.helper.create_password"
}
disabled={asManagedUserIsSelected}
/>
<Button <Button
variant="outlined" variant="outlined"
label="resources.users.action.generate_password" label="resources.users.action.generate_password"
@ -411,11 +459,17 @@ export const UserEdit = (props: EditProps) => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic" queryOptions={{ <Edit
{...props}
title={<UserTitle />}
actions={<UserEditActions />}
mutationMode="pessimistic"
queryOptions={{
meta: { meta: {
include: ["features"] // Tell your dataProvider to include features include: ["features"], // Tell your dataProvider to include features
} },
}}> }}
>
<TabbedForm toolbar={<UserEditToolbar />}> <TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}> <FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} /> <AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} />
@ -425,16 +479,26 @@ export const UserEdit = (props: EditProps) => {
label="resources.users.fields.avatar" label="resources.users.fields.avatar"
accept={{ "image/*": [".png", ".jpg"] }} accept={{ "image/*": [".png", ".jpg"] }}
> >
<ImageField source="src" title="Avatar" sx={{ '& img': { <ImageField
source="src"
title="Avatar"
sx={{
"& img": {
width: "120px !important", width: "120px !important",
height: "120px !important", height: "120px !important",
objectFit: "cover !important", objectFit: "cover !important",
borderRadius: '50% !important', borderRadius: "50% !important",
}}} /> },
}}
/>
</ImageInput> </ImageInput>
<TextInput source="id" readOnly /> <TextInput source="id" readOnly />
<TextInput source="displayname" /> <TextInput source="displayname" />
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" /> <UserPasswordInput
source="password"
autoComplete="new-password"
helperText="resources.users.helper.password"
/>
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable /> <SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
<UserBooleanInput source="locked" /> <UserBooleanInput source="locked" />
@ -500,13 +564,16 @@ export const UserEdit = (props: EditProps) => {
perPage={10} perPage={10}
sort={{ field: "created_ts", order: "DESC" }} sort={{ field: "created_ts", order: "DESC" }}
> >
<Datagrid sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton/>}> <Datagrid sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton />}>
<MediaIDField source="media_id" /> <MediaIDField source="media_id" />
<DateField source="created_ts" showTime options={DATE_FORMAT} /> <DateField source="created_ts" showTime options={DATE_FORMAT} />
<DateField source="last_access_ts" showTime options={DATE_FORMAT} /> <DateField source="last_access_ts" showTime options={DATE_FORMAT} />
<NumberField source="media_length" /> <NumberField source="media_length" />
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} /> <TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
<FunctionField source="upload_name" render={record => record.upload_name ? decodeURLComponent(record.upload_name) : ""} /> <FunctionField
source="upload_name"
render={record => (record.upload_name ? decodeURLComponent(record.upload_name) : "")}
/>
<TextField source="quarantined_by" /> <TextField source="quarantined_by" />
<QuarantineMediaButton label="resources.quarantine_media.action.name" /> <QuarantineMediaButton label="resources.quarantine_media.action.name" />
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" /> <ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
@ -516,16 +583,34 @@ export const UserEdit = (props: EditProps) => {
</FormTab> </FormTab>
<FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms"> <FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms">
<ReferenceManyField reference="joined_rooms" target="user_id" label={false} perPage={10} pagination={<Pagination />}> <ReferenceManyField
reference="joined_rooms"
target="user_id"
label={false}
perPage={10}
pagination={<Pagination />}
>
<Datagrid sx={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}> <Datagrid sx={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}>
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
<AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} />
</ReferenceField> </ReferenceField>
<TextField source="id" label="resources.rooms.fields.room_id" sortable={false}/> <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} />
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.name" link={false} sortable={false}> <ReferenceField
reference="rooms"
source="id"
label="resources.rooms.fields.name"
link={false}
sortable={false}
>
<TextField source="name" /> <TextField source="name" />
</ReferenceField> </ReferenceField>
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.joined_members" link={false} sortable={false}> <ReferenceField
reference="rooms"
source="id"
label="resources.rooms.fields.joined_members"
link={false}
sortable={false}
>
<TextField source="joined_members" sortable={false} /> <TextField source="joined_members" sortable={false} />
</ReferenceField> </ReferenceField>
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>

View File

@ -1,7 +1,7 @@
import fetchMock from "jest-fetch-mock"; import fetchMock from "jest-fetch-mock";
import { HttpError } from "ra-core";
import authProvider from "./authProvider"; import authProvider from "./authProvider";
import { HttpError } from "ra-core";
fetchMock.enableMocks(); fetchMock.enableMocks();
@ -28,7 +28,7 @@ describe("authProvider", () => {
password: "secret", password: "secret",
}); });
expect(ret).toEqual({redirectTo: "/"}); expect(ret).toEqual({ redirectTo: "/" });
expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/v3/login", { expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}', body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
headers: new Headers({ headers: new Headers({
@ -60,7 +60,7 @@ describe("authProvider", () => {
loginToken: "login_token", loginToken: "login_token",
}); });
expect(ret).toEqual({redirectTo: "/"}); expect(ret).toEqual({ redirectTo: "/" });
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/v3/login", { expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}', body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({ headers: new Headers({
@ -103,11 +103,15 @@ describe("authProvider", () => {
}); });
it("should reject if error.status is 401", async () => { it("should reject if error.status is 401", async () => {
await expect(authProvider.checkError(new HttpError("test-error", 401, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined(); await expect(
authProvider.checkError(new HttpError("test-error", 401, { errcode: "test-errcode", error: "test-error" }))
).rejects.toBeDefined();
}); });
it("should reject if error.status is 403", async () => { it("should reject if error.status is 403", async () => {
await expect(authProvider.checkError(new HttpError("test-error", 403, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined(); await expect(
authProvider.checkError(new HttpError("test-error", 403, { errcode: "test-errcode", error: "test-error" }))
).rejects.toBeDefined();
}); });
}); });

View File

@ -1,9 +1,9 @@
import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin"; import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
import { MatrixError, displayError } from "../utils/error";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { FetchConfig, ClearConfig, GetConfig } from "../utils/config"; import { FetchConfig, ClearConfig, GetConfig } from "../utils/config";
import decodeURLComponent from "../utils/decodeURLComponent"; import decodeURLComponent from "../utils/decodeURLComponent";
import { MatrixError, displayError } from "../utils/error";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const authProvider: AuthProvider = { const authProvider: AuthProvider = {
// called when the user attempts to log in // called when the user attempts to log in
@ -53,14 +53,15 @@ const authProvider: AuthProvider = {
if (!base_url) { if (!base_url) {
// there is some kind of bug with base_url being present in the form, but not submitted // there is some kind of bug with base_url being present in the form, but not submitted
// ref: https://github.com/etkecc/synapse-admin/issues/14 // ref: https://github.com/etkecc/synapse-admin/issues/14
localStorage.removeItem("base_url") localStorage.removeItem("base_url");
throw new Error("Homeserver URL is required."); throw new Error("Homeserver URL is required.");
} }
base_url = base_url.replace(/\/+$/g, ""); base_url = base_url.replace(/\/+$/g, "");
localStorage.setItem("base_url", base_url); localStorage.setItem("base_url", base_url);
const decoded_base_url = decodeURLComponent(base_url); const decoded_base_url = decodeURLComponent(base_url);
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login"); const login_api_url =
decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
let response; let response;
@ -69,7 +70,7 @@ const authProvider: AuthProvider = {
// this a login with an already obtained access token, let's just validate it // this a login with an already obtained access token, let's just validate it
options = { options = {
headers: new Headers({ headers: new Headers({
Accept: 'application/json', Accept: "application/json",
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}), }),
}; };
@ -91,19 +92,16 @@ const authProvider: AuthProvider = {
pageToRedirectTo = "/server_status"; pageToRedirectTo = "/server_status";
} }
return Promise.resolve({redirectTo: pageToRedirectTo}); return Promise.resolve({ redirectTo: pageToRedirectTo });
} catch(err) { } catch (err) {
const error = err as HttpError; const error = err as HttpError;
const errorStatus = error.status; const errorStatus = error.status;
const errorBody = error.body as MatrixError; const errorBody = error.body as MatrixError;
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message); const errMsg = errorBody?.errcode
? displayError(errorBody.errcode, errorStatus, errorBody.error)
: displayError("M_INVALID", errorStatus, error.message);
return Promise.reject( return Promise.reject(new HttpError(errMsg, errorStatus));
new HttpError(
errMsg,
errorStatus,
)
);
} }
}, },
getIdentity: async () => { getIdentity: async () => {
@ -175,7 +173,7 @@ const authProvider: AuthProvider = {
const status = err.status; const status = err.status;
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
return Promise.reject({message: displayError(errorBody.errcode, status, errorBody.error)}); return Promise.reject({ message: displayError(errorBody.errcode, status, errorBody.error) });
} }
return Promise.resolve(); return Promise.resolve();
}, },

View File

@ -13,9 +13,9 @@ import {
withLifecycleCallbacks, withLifecycleCallbacks,
} from "react-admin"; } from "react-admin";
import { returnMXID } from "../utils/mxid";
import { GetConfig } from "../utils/config"; import { GetConfig } from "../utils/config";
import { MatrixError, displayError } from "../utils/error"; import { MatrixError, displayError } from "../utils/error";
import { returnMXID } from "../utils/mxid";
const CACHED_MANY_REF: Record<string, any> = {}; const CACHED_MANY_REF: Record<string, any> = {};
@ -37,7 +37,7 @@ const jsonClient = async (url: string, options: Options = {}) => {
const error = err as HttpError; const error = err as HttpError;
const errorStatus = error.status; const errorStatus = error.status;
const errorBody = error.body as MatrixError; const errorBody = error.body as MatrixError;
const errMsg = !!errorBody?.errcode const errMsg = errorBody?.errcode
? displayError(errorBody.errcode, errorStatus, errorBody.error) ? displayError(errorBody.errcode, errorStatus, errorBody.error)
: displayError("M_INVALID", errorStatus, error.message); : displayError("M_INVALID", errorStatus, error.message);
@ -254,9 +254,7 @@ export interface UploadMediaResult {
} }
export interface ExperimentalFeaturesModel { export interface ExperimentalFeaturesModel {
features: { features: Record<string, boolean>;
[key: string]: boolean;
};
} }
export interface RateLimitsModel { export interface RateLimitsModel {
@ -266,13 +264,9 @@ export interface RateLimitsModel {
export interface AccountDataModel { export interface AccountDataModel {
account_data: { account_data: {
global: { global: Record<string, object>;
[key: string]: object; rooms: Record<string, object>;
},
rooms: {
[key: string]: object;
}; };
}
} }
export interface UsernameAvailabilityResult { export interface UsernameAvailabilityResult {
@ -291,7 +285,7 @@ export interface ServerStatusComponent {
url: string; url: string;
icon: string; icon: string;
text: string; text: string;
} };
} }
export interface ServerStatusResponse { export interface ServerStatusResponse {
@ -326,9 +320,7 @@ export interface ServerCommand {
additionalArgs?: string; additionalArgs?: string;
} }
export interface ServerCommandsResponse { export type ServerCommandsResponse = Record<string, ServerCommand>;
[command: string]: ServerCommand;
}
export interface SynapseDataProvider extends DataProvider { export interface SynapseDataProvider extends DataProvider {
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>; deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
@ -691,13 +683,14 @@ const baseDataProvider: SynapseDataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = base_url + res.path; const endpoint_url = base_url + res.path;
const responses = await Promise.all(params.ids.map(id => { const responses = await Promise.all(
params.ids.map(id => {
// edge case: when user is external / federated, homeserver will return error, as querying external users via // edge case: when user is external / federated, homeserver will return error, as querying external users via
// /_synapse/admin/v2/users is not allowed. // /_synapse/admin/v2/users is not allowed.
// That leads to an issue when a user is referenced (e.g., in room state datagrid) - the user cell is just empty. // That leads to an issue when a user is referenced (e.g., in room state datagrid) - the user cell is just empty.
// To avoid that, we fake the response with one specific field (name) which is used in the datagrid. // To avoid that, we fake the response with one specific field (name) which is used in the datagrid.
if (homeserver && resource === "users") { if (homeserver && resource === "users") {
if (!(<string>id).endsWith(homeserver)) { if (!(id as string).endsWith(homeserver)) {
const json = { const json = {
name: id, name: id,
}; };
@ -705,7 +698,8 @@ const baseDataProvider: SynapseDataProvider = {
} }
} }
return jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`); return jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`);
})); })
);
return { return {
data: responses.map(({ json }) => res.map(json)), data: responses.map(({ json }) => res.map(json)),
total: responses.length, total: responses.length,
@ -724,8 +718,6 @@ const baseDataProvider: SynapseDataProvider = {
dir: getSearchOrder(order), dir: getSearchOrder(order),
}; };
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
@ -734,7 +726,7 @@ const baseDataProvider: SynapseDataProvider = {
const ref = res.reference(params.id); const ref = res.reference(params.id);
const endpoint_url = `${homeserver}${ref.endpoint}?${new URLSearchParams(filterUndefined(query)).toString()}`; const endpoint_url = `${homeserver}${ref.endpoint}?${new URLSearchParams(filterUndefined(query)).toString()}`;
let CACHE_KEY = ref.endpoint; const CACHE_KEY = ref.endpoint;
let jsonData = []; let jsonData = [];
let total = 0; let total = 0;
@ -743,7 +735,7 @@ const baseDataProvider: SynapseDataProvider = {
total = CACHED_MANY_REF[CACHE_KEY]["total"]; total = CACHED_MANY_REF[CACHE_KEY]["total"];
} else { } else {
const { json } = await jsonClient(endpoint_url); const { json } = await jsonClient(endpoint_url);
jsonData = json[res.data] jsonData = json[res.data];
total = res.total(json, from, perPage); total = res.total(json, from, perPage);
if (resource === "joined_rooms") { if (resource === "joined_rooms") {
// cache will be applied only for joined_rooms // cache will be applied only for joined_rooms
@ -967,9 +959,9 @@ const baseDataProvider: SynapseDataProvider = {
return json as AccountDataModel; return json as AccountDataModel;
}, },
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => { setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
const filtered = Object.entries(rateLimits). const filtered = Object.entries(rateLimits)
filter(([key, value]) => value !== null && value !== undefined). .filter(([key, value]) => value !== null && value !== undefined)
reduce((obj, [key, value]) => { .reduce((obj, [key, value]) => {
obj[key] = value; obj[key] = value;
return obj; return obj;
}, {}); }, {});
@ -978,7 +970,7 @@ const baseDataProvider: SynapseDataProvider = {
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`; const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
if (Object.keys(filtered).length === 0) { if (Object.keys(filtered).length === 0) {
await jsonClient(endpoint_url, { method: "DELETE" }); await jsonClient(endpoint_url, { method: "DELETE" });
return return;
} }
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify(filtered) }); await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify(filtered) });
@ -1010,7 +1002,7 @@ const baseDataProvider: SynapseDataProvider = {
throw error; throw error;
} }
}, },
getServerRunningProcess: async (etkeAdminUrl: string, burstCache: boolean = false): Promise<ServerProcessResponse> => { getServerRunningProcess: async (etkeAdminUrl: string, burstCache = false): Promise<ServerProcessResponse> => {
const locked_at = ""; const locked_at = "";
const command = ""; const command = "";
@ -1022,8 +1014,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(serverURL, { const response = await fetch(serverURL, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
@ -1045,7 +1037,7 @@ const baseDataProvider: SynapseDataProvider = {
return { locked_at, command }; return { locked_at, command };
}, },
getServerStatus: async (etkeAdminUrl: string, burstCache: boolean = false): Promise<ServerStatusResponse> => { getServerStatus: async (etkeAdminUrl: string, burstCache = false): Promise<ServerStatusResponse> => {
let serverURL = `${etkeAdminUrl}/status`; let serverURL = `${etkeAdminUrl}/status`;
if (burstCache) { if (burstCache) {
serverURL += `?time=${new Date().getTime()}`; serverURL += `?time=${new Date().getTime()}`;
@ -1054,8 +1046,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(serverURL, { const response = await fetch(serverURL, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error getting server status: ${response.status} ${response.statusText}`); console.error(`Error getting server status: ${response.status} ${response.statusText}`);
@ -1074,7 +1066,10 @@ const baseDataProvider: SynapseDataProvider = {
return { success: false, ok: false, host: "", results: [] }; return { success: false, ok: false, host: "", results: [] };
}, },
getServerNotifications: async (serverNotificationsUrl: string, burstCache: boolean = false): Promise<ServerNotificationsResponse> => { getServerNotifications: async (
serverNotificationsUrl: string,
burstCache = false
): Promise<ServerNotificationsResponse> => {
let serverURL = `${serverNotificationsUrl}/notifications`; let serverURL = `${serverNotificationsUrl}/notifications`;
if (burstCache) { if (burstCache) {
serverURL += `?time=${new Date().getTime()}`; serverURL += `?time=${new Date().getTime()}`;
@ -1083,8 +1078,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(serverURL, { const response = await fetch(serverURL, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error getting server notifications: ${response.status} ${response.statusText}`); console.error(`Error getting server notifications: ${response.status} ${response.statusText}`);
@ -1113,9 +1108,9 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(`${serverNotificationsUrl}/notifications`, { const response = await fetch(`${serverNotificationsUrl}/notifications`, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
}, },
method: "DELETE" method: "DELETE",
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error deleting server notifications: ${response.status} ${response.statusText}`); console.error(`Error deleting server notifications: ${response.status} ${response.statusText}`);
@ -1124,7 +1119,7 @@ const baseDataProvider: SynapseDataProvider = {
const status = response.status; const status = response.status;
if (status === 204) { if (status === 204) {
const result = { success: true } const result = { success: true };
return result; return result;
} }
} catch (error) { } catch (error) {
@ -1137,8 +1132,8 @@ const baseDataProvider: SynapseDataProvider = {
try { try {
const response = await fetch(`${serverCommandsUrl}/commands`, { const response = await fetch(`${serverCommandsUrl}/commands`, {
headers: { headers: {
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
console.error(`Error fetching server commands: ${response.status} ${response.statusText}`); console.error(`Error fetching server commands: ${response.status} ${response.statusText}`);
@ -1163,15 +1158,15 @@ const baseDataProvider: SynapseDataProvider = {
const endpoint_url = `${serverCommandsUrl}/commands`; const endpoint_url = `${serverCommandsUrl}/commands`;
const body = { const body = {
command: command, command: command,
...additionalArgs ...additionalArgs,
} };
const response = await fetch(endpoint_url, { const response = await fetch(endpoint_url, {
method: "POST", method: "POST",
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("access_token")}` Authorization: `Bearer ${localStorage.getItem("access_token")}`,
} },
}); });
if (!response.ok) { if (!response.ok) {
@ -1186,13 +1181,13 @@ const baseDataProvider: SynapseDataProvider = {
if (status === 204) { if (status === 204) {
return { return {
success: true, success: true,
} };
} }
return { return {
success: false, success: false,
} };
} },
}; };
const dataProvider = withLifecycleCallbacks(baseDataProvider, [ const dataProvider = withLifecycleCallbacks(baseDataProvider, [

View File

@ -20,15 +20,15 @@ let config: Config = {
corsCredentials: "same-origin", corsCredentials: "same-origin",
asManagedUsers: [], asManagedUsers: [],
menu: [], menu: [],
etkeccAdmin: "" etkeccAdmin: "",
}; };
export const FetchConfig = async () => { export const FetchConfig = async () => {
// load config.json and honor vite base url (import.meta.env.BASE_URL) // load config.json and honor vite base url (import.meta.env.BASE_URL)
// if that url doesn't have a trailing slash - add it // if that url doesn't have a trailing slash - add it
let configJSONUrl = "config.json" let configJSONUrl = "config.json";
if (import.meta.env.BASE_URL) { if (import.meta.env.BASE_URL) {
configJSONUrl = `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}config.json`; configJSONUrl = `${import.meta.env.BASE_URL.replace(/\/?$/, "/")}config.json`;
} }
try { try {
const resp = await fetch(configJSONUrl); const resp = await fetch(configJSONUrl);
@ -52,7 +52,10 @@ export const FetchConfig = async () => {
const resp = await fetch(`${protocol}://${homeserver}/.well-known/matrix/client`); const resp = await fetch(`${protocol}://${homeserver}/.well-known/matrix/client`);
const configWK = await resp.json(); const configWK = await resp.json();
if (!configWK[WellKnownKey]) { if (!configWK[WellKnownKey]) {
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK); console.log(
`Loaded ${protocol}://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`,
configWK
);
} else { } else {
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client`, configWK); console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client`, configWK);
LoadConfig(configWK[WellKnownKey]); LoadConfig(configWK[WellKnownKey]);
@ -61,7 +64,7 @@ export const FetchConfig = async () => {
console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e); console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e);
} }
} }
} };
// load config from context // load config from context
// we deliberately processing each key separately to avoid overwriting the whole config, loosing some keys, and messing // we deliberately processing each key separately to avoid overwriting the whole config, loosing some keys, and messing
@ -90,13 +93,12 @@ export const LoadConfig = (context: any) => {
if (context?.etkeccAdmin) { if (context?.etkeccAdmin) {
config.etkeccAdmin = context.etkeccAdmin; config.etkeccAdmin = context.etkeccAdmin;
} }
} };
// get config // get config
export const GetConfig = (): Config => { export const GetConfig = (): Config => {
return config; return config;
} };
// clear config // clear config
export const ClearConfig = () => { export const ClearConfig = () => {
@ -104,4 +106,4 @@ export const ClearConfig = () => {
config = {} as Config; config = {} as Config;
// session // session
localStorage.clear(); localStorage.clear();
} };

View File

@ -10,6 +10,6 @@ const decodeURLComponent = (str: any): any => {
} catch (e) { } catch (e) {
return str; return str;
} }
} };
export default decodeURLComponent; export default decodeURLComponent;

View File

@ -1,4 +1,4 @@
export type MatrixError = { export interface MatrixError {
errcode: string; errcode: string;
error: string; error: string;
} }

View File

@ -1,4 +1,4 @@
export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string, mediaId: string } => { export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string; mediaId: string } => {
const re = /^mxc:\/\/([^/]+)\/(\w+)/; const re = /^mxc:\/\/([^/]+)\/(\w+)/;
const ret = re.exec(mxcUrl); const ret = re.exec(mxcUrl);
if (ret == null) { if (ret == null) {

View File

@ -1,13 +1,13 @@
import AnnouncementIcon from '@mui/icons-material/Announcement'; import AnnouncementIcon from "@mui/icons-material/Announcement";
import EngineeringIcon from '@mui/icons-material/Engineering'; import EngineeringIcon from "@mui/icons-material/Engineering";
import HelpCenterIcon from '@mui/icons-material/HelpCenter'; import HelpCenterIcon from "@mui/icons-material/HelpCenter";
import SupportAgentIcon from '@mui/icons-material/SupportAgent'; import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import PieChartIcon from "@mui/icons-material/PieChart";
import PieChartIcon from '@mui/icons-material/PieChart'; import PriceCheckIcon from "@mui/icons-material/PriceCheck";
import UpgradeIcon from '@mui/icons-material/Upgrade'; import RestartAltIcon from "@mui/icons-material/RestartAlt";
import RouterIcon from '@mui/icons-material/Router'; import RouterIcon from "@mui/icons-material/Router";
import PriceCheckIcon from '@mui/icons-material/PriceCheck'; import SupportAgentIcon from "@mui/icons-material/SupportAgent";
import RestartAltIcon from '@mui/icons-material/RestartAlt'; import UpgradeIcon from "@mui/icons-material/Upgrade";
export const Icons = { export const Icons = {
Announcement: AnnouncementIcon, Announcement: AnnouncementIcon,

View File

@ -1,4 +1,5 @@
import { Identifier } from "ra-core"; import { Identifier } from "ra-core";
import { GetConfig } from "../utils/config"; import { GetConfig } from "../utils/config";
const mxidPattern = /^@[^@:]+:[^@:]+$/; const mxidPattern = /^@[^@:]+:[^@:]+$/;
@ -56,6 +57,6 @@ export function returnMXID(input: string | Identifier): string {
} }
// If input is not a valid MXID, assume it's a localpart and construct the MXID // If input is not a valid MXID, assume it's a localpart and construct the MXID
const localpart = typeof input === 'string' && inputStr.startsWith('@') ? inputStr.slice(1) : inputStr; const localpart = typeof input === "string" && inputStr.startsWith("@") ? inputStr.slice(1) : inputStr;
return `@${localpart}:${homeserver}`; return `@${localpart}:${homeserver}`;
} }

View File

@ -1,7 +1,6 @@
import { vitePluginVersionMark } from "vite-plugin-version-mark";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { vitePluginVersionMark } from "vite-plugin-version-mark";
export default defineConfig({ export default defineConfig({
base: "./", base: "./",
@ -16,7 +15,7 @@ export default defineConfig({
ifMeta: false, ifMeta: false,
ifLog: false, ifLog: false,
ifGlobal: true, ifGlobal: true,
outputFile: (version) => ({ outputFile: version => ({
path: "manifest.json", path: "manifest.json",
content: JSON.stringify({ content: JSON.stringify({
name: "Synapse Admin", name: "Synapse Admin",
@ -24,20 +23,23 @@ export default defineConfig({
description: "Synapse Admin is an admin console for synapse Matrix homeserver with additional features.", description: "Synapse Admin is an admin console for synapse Matrix homeserver with additional features.",
categories: ["productivity", "utilities"], categories: ["productivity", "utilities"],
orientation: "landscape", orientation: "landscape",
icons: [{ icons: [
{
src: "favicon.ico", src: "favicon.ico",
sizes: "32x32", sizes: "32x32",
type: "image/x-icon" type: "image/x-icon",
},{ },
{
src: "images/logo.webp", src: "images/logo.webp",
sizes: "512x512", sizes: "512x512",
type: "image/webp", type: "image/webp",
purpose: "any maskable" purpose: "any maskable",
}], },
],
start_url: ".", start_url: ".",
display: "standalone", display: "standalone",
theme_color: "#000000", theme_color: "#000000",
background_color: "#ffffff" background_color: "#ffffff",
}), }),
}), }),
}), }),

169
yarn.lock
View File

@ -1252,7 +1252,7 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/estree@1.0.7", "@types/estree@^1.0.0", "@types/estree@^1.0.6": "@types/estree@^1.0.6":
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
@ -1916,14 +1916,6 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d"
integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==
cli@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14"
integrity sha512-41U72MB56TfUMGndAKK8vJ78eooOD4Z5NOL4xEfjc0c23s+6EYKXlXsmACBVclLP1yOfWCgEganVzddVrSNoTg==
dependencies:
exit "0.1.2"
glob "^7.1.1"
cliui@^8.0.1: cliui@^8.0.1:
version "8.0.1" version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@ -1977,13 +1969,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
console-browserify@1.1.x:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
integrity sha512-duS7VP5pvfsNLDvL1O4VOEbw37AI3A4ZUQYemvDlnpGrNu9tprR7BYWpDYwC0Xia0Zxz5ZupdiIrUp0GH1aXfg==
dependencies:
date-now "^0.1.4"
convert-source-map@^1.5.0: convert-source-map@^1.5.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
@ -1999,11 +1984,6 @@ cookie@^1.0.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610"
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cosmiconfig@^7.0.0: cosmiconfig@^7.0.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
@ -2134,11 +2114,6 @@ date-fns@^3.6.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
integrity sha512-AsElvov3LoNB7tf5k37H2jYSB+ZZPMT5sG2QjJCcdlV5chIv6htBUBUui2IKRjgtKAKtCBN7Zbwa+MtwLjSeNw==
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
@ -2251,24 +2226,6 @@ dom-helpers@^5.0.1:
"@babel/runtime" "^7.8.7" "@babel/runtime" "^7.8.7"
csstype "^3.0.2" csstype "^3.0.2"
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
dependencies:
domelementtype "^2.0.1"
entities "^2.0.0"
domelementtype@1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
domelementtype@^2.0.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domexception@^4.0.0: domexception@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
@ -2276,13 +2233,6 @@ domexception@^4.0.0:
dependencies: dependencies:
webidl-conversions "^7.0.0" webidl-conversions "^7.0.0"
domhandler@2.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
integrity sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ==
dependencies:
domelementtype "1"
dompurify@^3.2.4: dompurify@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
@ -2290,14 +2240,6 @@ dompurify@^3.2.4:
optionalDependencies: optionalDependencies:
"@types/trusted-types" "^2.0.7" "@types/trusted-types" "^2.0.7"
domutils@1.5:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
integrity sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==
dependencies:
dom-serializer "0"
domelementtype "1"
dunder-proto@^1.0.0, dunder-proto@^1.0.1: dunder-proto@^1.0.0, dunder-proto@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
@ -2334,16 +2276,6 @@ emoji-regex@^9.2.2:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
entities@1.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
integrity sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
entities@^4.5.0: entities@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
@ -2593,20 +2525,6 @@ eslint-plugin-unused-imports@^4.1.4:
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz#62ddc7446ccbf9aa7b6f1f0b00a980423cda2738" resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz#62ddc7446ccbf9aa7b6f1f0b00a980423cda2738"
integrity sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ== integrity sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==
eslint-plugin-yaml@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-yaml/-/eslint-plugin-yaml-1.1.2.tgz#b3f45efbd3d6655fb36388208078abad771d9231"
integrity sha512-SV6fQ/+rI3caRyQy9RSNhE6IxSAqH1Pmpnj3AtcgEwmzfzhcOHgMTbUbaKytrpo/XxCtois7w9Ugbcd4URjJ6g==
dependencies:
"@types/estree" "1.0.7"
eslint-plugin-yaml "link:../../../.cache/yarn/v6/npm-eslint-plugin-yaml-1.1.2-b3f45efbd3d6655fb36388208078abad771d9231-integrity/node_modules/eslint-plugin-yaml"
estree-util-value-to-estree "3.3.2"
js-yaml "4.1.0"
jshint "2.13.6"
"eslint-plugin-yaml@link:../../../.cache/yarn/v6/npm-eslint-plugin-yaml-1.1.2-b3f45efbd3d6655fb36388208078abad771d9231-integrity/node_modules/eslint-plugin-yaml":
version "0.0.0"
eslint-scope@^8.3.0: eslint-scope@^8.3.0:
version "8.3.0" version "8.3.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.3.0.tgz#10cd3a918ffdd722f5f3f7b5b83db9b23c87340d" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.3.0.tgz#10cd3a918ffdd722f5f3f7b5b83db9b23c87340d"
@ -2699,13 +2617,6 @@ estraverse@^5.1.0, estraverse@^5.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-util-value-to-estree@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-3.3.2.tgz#75bb2263850b6f5ac35edd343929c36b51a69806"
integrity sha512-hYH1aSvQI63Cvq3T3loaem6LW4u72F187zW4FHpTrReJSm6W66vYTFNO1vH/chmcOulp1HlAj1pxn8Ag0oXI5Q==
dependencies:
"@types/estree" "^1.0.0"
esutils@^2.0.2: esutils@^2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@ -2736,7 +2647,7 @@ execa@^5.0.0:
signal-exit "^3.0.3" signal-exit "^3.0.3"
strip-final-newline "^2.0.0" strip-final-newline "^2.0.0"
exit@0.1.2, exit@0.1.x, exit@^0.1.2: exit@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==
@ -2979,7 +2890,7 @@ glob-parent@^6.0.2:
dependencies: dependencies:
is-glob "^4.0.3" is-glob "^4.0.3"
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: glob@^7.1.3, glob@^7.1.4:
version "7.2.3" version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -3093,17 +3004,6 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
htmlparser2@3.8.x:
version "3.8.3"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
integrity sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q==
dependencies:
domelementtype "1"
domhandler "2.3"
domutils "1.5"
entities "1.0"
readable-stream "1.1"
http-proxy-agent@^5.0.0: http-proxy-agent@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
@ -3182,7 +3082,7 @@ inflight@^1.0.4:
once "^1.3.0" once "^1.3.0"
wrappy "1" wrappy "1"
inherits@2, inherits@~2.0.1: inherits@2:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -3398,11 +3298,6 @@ is-weakset@^2.0.3:
call-bound "^1.0.3" call-bound "^1.0.3"
get-intrinsic "^1.2.6" get-intrinsic "^1.2.6"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
isarray@^2.0.5: isarray@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
@ -3871,13 +3766,6 @@ js-sha3@0.8.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@4.1.0, js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
js-yaml@^3.13.1: js-yaml@^3.13.1:
version "3.14.1" version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
@ -3886,6 +3774,13 @@ js-yaml@^3.13.1:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
jsdom@^20.0.0: jsdom@^20.0.0:
version "20.0.3" version "20.0.3"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db"
@ -3923,19 +3818,6 @@ jsesc@^3.0.2:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
jshint@2.13.6:
version "2.13.6"
resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.13.6.tgz#3679a2687a3066fa9034ef85d8c305613a31eec6"
integrity sha512-IVdB4G0NTTeQZrBoM8C5JFVLjV2KtZ9APgybDA1MK73xb09qFs0jCXyQLnCOp1cSZZZbvhq/6mfXHUTaDkffuQ==
dependencies:
cli "~1.0.0"
console-browserify "1.1.x"
exit "0.1.x"
htmlparser2 "3.8.x"
lodash "~4.17.21"
minimatch "~3.0.2"
strip-json-comments "1.0.x"
json-buffer@3.0.1: json-buffer@3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
@ -4049,7 +3931,7 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21, lodash@~4.17.21, lodash@~4.17.5: lodash@^4.17.21, lodash@~4.17.5:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -4171,13 +4053,6 @@ minimatch@^9.0.4:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
minimatch@~3.0.2:
version "3.0.8"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1"
integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.6: minimist@^1.2.0, minimist@^1.2.6:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@ -4778,16 +4653,6 @@ react@^19.1.0:
resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
readable-stream@1.1:
version "1.1.13"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
integrity sha512-E98tWzqShvKDGpR2MbjsDkDQWLW2TfWUC15H4tNQhIJ5Lsta84l8nUGL9/ybltGwe+wZzWPpc1Kmd2wQP4bdCA==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
redent@^3.0.0: redent@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@ -5191,11 +5056,6 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1" define-properties "^1.2.1"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
strip-ansi@^6.0.0, strip-ansi@^6.0.1: strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@ -5225,11 +5085,6 @@ strip-indent@^3.0.0:
dependencies: dependencies:
min-indent "^1.0.0" min-indent "^1.0.0"
strip-json-comments@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
integrity sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg==
strip-json-comments@^3.1.1: strip-json-comments@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"