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
62 changed files with 1782 additions and 1502 deletions

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

View File

@@ -1,6 +1,7 @@
import { render, screen, waitFor } from "@testing-library/react";
import { RecordContextProvider } from "react-admin";
import { act } from "react";
import { RecordContextProvider } from "react-admin";
import AvatarField from "./AvatarField";
describe("AvatarField", () => {
@@ -8,7 +9,7 @@ describe("AvatarField", () => {
// Mock fetch
global.fetch = jest.fn(() =>
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;

View File

@@ -1,7 +1,8 @@
import { get } from "lodash";
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 { FieldProps, useRecordContext, useTranslate } from "react-admin";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { isMXID, isASManaged } from "../utils/mxid";
@@ -96,12 +97,15 @@ const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
{letter}
</Avatar>
</Badge>
</Tooltip>);
</Tooltip>
);
}
return (<Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant}>
{letter}
</Avatar>);
return (
<Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant}>
{letter}
</Avatar>
);
};
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 { Fragment, useState } from "react";
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin";
import ActionDelete from "@mui/icons-material/Delete";
import ActionCheck from "@mui/icons-material/CheckCircle";
import AlertError from "@mui/icons-material/ErrorOutline";
import {
SimpleForm,
BooleanInput,
useTranslate,
RaRecord,
useNotify,
useRedirect,
useDelete,
NotificationType,
useDeleteMany,
Identifier,
useUnselectAll,
} from "react-admin";
interface DeleteRoomButtonProps {
selectedIds: Identifier[];
@@ -13,7 +25,7 @@ interface DeleteRoomButtonProps {
const resourceName = "rooms";
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = props => {
const translate = useTranslate();
const [open, setOpen] = useState(false);
const [block, setBlock] = useState(false);
@@ -28,7 +40,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleDelete = (values: {block: boolean}) => {
const handleDelete = (values: { block: boolean }) => {
deleteMany(
resourceName,
{ ids: recordIds, meta: values },
@@ -39,8 +51,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
unselectAll();
redirect("/rooms");
},
onError: (error) =>
notify("resources.rooms.action.erase.failure", { type: 'error' as NotificationType }),
onError: error => 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 { Fragment, useState } from "react";
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin";
import ActionDelete from "@mui/icons-material/Delete";
import ActionCheck from "@mui/icons-material/CheckCircle";
import AlertError from "@mui/icons-material/ErrorOutline";
import {
SimpleForm,
BooleanInput,
useTranslate,
RaRecord,
useNotify,
useRedirect,
useDelete,
NotificationType,
useDeleteMany,
Identifier,
useUnselectAll,
} from "react-admin";
interface DeleteUserButtonProps {
selectedIds: Identifier[];
@@ -13,7 +25,7 @@ interface DeleteUserButtonProps {
const resourceName = "users";
const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
const DeleteUserButton: React.FC<DeleteUserButtonProps> = props => {
const translate = useTranslate();
const [open, setOpen] = useState(false);
const [deleteMedia, setDeleteMedia] = useState(false);
@@ -29,7 +41,7 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleDelete = (values: {deleteMedia: boolean, redactEvents: boolean}) => {
const handleDelete = (values: { deleteMedia: boolean; redactEvents: boolean }) => {
deleteMany(
resourceName,
{ ids: recordIds, meta: values },
@@ -39,14 +51,13 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
messageArgs: {
smart_count: recordIds.length,
},
type: 'info' as NotificationType,
type: "info" as NotificationType,
});
handleDialogClose();
unselectAll();
redirect("/users");
},
onError: (error) =>
notify("ra.notification.data_provider_error", { type: 'error' as NotificationType }),
onError: error => notify("ra.notification.data_provider_error", { type: "error" as NotificationType }),
}
);
};

View File

@@ -1,4 +1,5 @@
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
import { isASManaged } from "../utils/mxid";
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 { useNotify } 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";
const experimentalFeaturesMap = {
msc3881: "enable remotely toggling push notifications for another client",
msc3575: "enable experimental sliding sync support",
msc3881: "enable remotely toggling push notifications for another client",
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 featureValue = props.featureValue;
const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
@@ -20,34 +25,33 @@ const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boole
props.updateFeature(featureKey, event.target.checked);
};
return <Stack
return (
<Stack
direction="row"
spacing={2}
alignItems="start"
sx={{
padding: 2,
padding: 2,
}}
>
<Switch checked={checked} onChange={handleChange} />
<Stack>
<Typography
>
<Switch checked={checked} onChange={handleChange} />
<Stack>
<Typography
variant="subtitle1"
sx={{
fontWeight: "medium",
color: "text.primary"
fontWeight: "medium",
color: "text.primary",
}}
>
>
{featureKey}
</Typography>
<Typography
variant="body2"
color="text.secondary"
>
</Typography>
<Typography variant="body2" color="text.secondary">
{featureDescription}
</Typography>
</Typography>
</Stack>
</Stack>
</Stack>
}
);
};
export const ExperimentalFeaturesList = () => {
const record = useRecordContext();
@@ -62,36 +66,35 @@ export const ExperimentalFeaturesList = () => {
const fetchFeatures = async () => {
const features = await dataProvider.getFeatures(record.id);
setFeatures(features);
}
};
fetchFeatures();
}, []);
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);
const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures);
notify("ra.notification.updated", {
messageArgs: { smart_count: 1 },
type: "success",
messageArgs: { smart_count: 1 },
type: "success",
});
};
return <>
<Stack
direction="column"
spacing={1}
>
{Object.keys(features).map((featureKey: string) =>
<ExperimentalFeatureRow
key={featureKey}
featureKey={featureKey}
featureValue={features[featureKey]}
updateFeature={updateFeature}
/>
)}
</Stack>
</>
}
return (
<>
<Stack direction="column" spacing={1}>
{Object.keys(features).map((featureKey: string) => (
<ExperimentalFeatureRow
key={featureKey}
featureKey={featureKey}
featureValue={features[featureKey]}
updateFeature={updateFeature}
/>
))}
</Stack>
</>
);
};
export default ExperimentalFeaturesList;

View File

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

View File

@@ -1,59 +1,58 @@
import { styled } from "@mui/material/styles";
import { Box } from "@mui/material";
import { styled } from "@mui/material/styles";
const LoginFormBox = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 1rem)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.background.default : theme.palette.background.paper,
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 1rem)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: theme.palette.mode === "dark" ? theme.palette.background.default : theme.palette.background.paper,
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
[`& .card`]: {
width: "30rem",
marginTop: "6rem",
marginBottom: "6rem",
},
[`& .avatar`]: {
margin: "1rem",
display: "flex",
justifyContent: "center",
},
[`& .icon`]: {
backgroundColor: theme.palette.grey[500],
},
[`& .hint`]: {
marginTop: "1em",
marginBottom: "1em",
display: "flex",
justifyContent: "center",
color: theme.palette.grey[600],
},
[`& .form`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .select`]: {
marginBottom: "2rem",
},
[`& .actions`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .serverVersion`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginLeft: "0.5rem",
},
[`& .matrixVersions`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem",
marginBottom: "1rem",
marginLeft: "0.5rem",
},
}
));
[`& .card`]: {
width: "30rem",
marginTop: "6rem",
marginBottom: "6rem",
},
[`& .avatar`]: {
margin: "1rem",
display: "flex",
justifyContent: "center",
},
[`& .icon`]: {
backgroundColor: theme.palette.grey[500],
},
[`& .hint`]: {
marginTop: "1em",
marginBottom: "1em",
display: "flex",
justifyContent: "center",
color: theme.palette.grey[600],
},
[`& .form`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .select`]: {
marginBottom: "2rem",
},
[`& .actions`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .serverVersion`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginLeft: "0.5rem",
},
[`& .matrixVersions`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem",
marginBottom: "1rem",
marginLeft: "0.5rem",
},
}));
export default LoginFormBox;

View File

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

View File

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

View File

@@ -1,10 +1,18 @@
import { Stack, Typography } from "@mui/material";
import { TextField } from "@mui/material";
import { useEffect, useState } from "react";
import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin";
import { TextField } from "@mui/material";
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 handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -16,35 +24,34 @@ const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value:
updateRateLimit(limit, value);
};
return <Stack
spacing={1}
alignItems="start"
sx={{
return (
<Stack
spacing={1}
alignItems="start"
sx={{
padding: 2,
}}
>
<TextField
id="outlined-number"
type="number"
value={value}
onChange={handleChange}
slotProps={{
inputLabel: {
shrink: true,
},
}}
label={translate(`resources.users.limits.${limit}`)}
/>
<Stack>
<Typography
variant="body2"
color="text.secondary"
>
{translate(`resources.users.limits.${limit}_text`)}
</Typography>
>
<TextField
id="outlined-number"
type="number"
value={value}
onChange={handleChange}
slotProps={{
inputLabel: {
shrink: true,
},
}}
label={translate(`resources.users.limits.${limit}`)}
/>
<Stack>
<Typography variant="body2" color="text.secondary">
{translate(`resources.users.limits.${limit}_text`)}
</Typography>
</Stack>
</Stack>
</Stack>
}
);
};
const UserRateLimits = () => {
const translate = useTranslate();
@@ -62,36 +69,31 @@ const UserRateLimits = () => {
}
useEffect(() => {
const fetchRateLimits = async () => {
const rateLimits = await dataProvider.getRateLimits(record.id);
if (Object.keys(rateLimits).length > 0) {
setRateLimits(rateLimits);
}
const fetchRateLimits = async () => {
const rateLimits = await dataProvider.getRateLimits(record.id);
if (Object.keys(rateLimits).length > 0) {
setRateLimits(rateLimits);
}
};
fetchRateLimits();
fetchRateLimits();
}, []);
const updateRateLimit = async (limit: string, value: any) => {
let updatedRateLimits = { ...rateLimits, [limit]: value };
const updatedRateLimits = { ...rateLimits, [limit]: value };
setRateLimits(updatedRateLimits);
form.setValue(`rates.${limit}`, value, { shouldDirty: true });
};
return <>
<Stack
direction="column"
>
{Object.keys(rateLimits).map((limit: string) =>
<RateLimitRow
key={limit}
limit={limit}
value={rateLimits[limit]}
updateRateLimit={updateRateLimit}
/>
)}
</Stack>
</>
return (
<>
<Stack direction="column">
{Object.keys(rateLimits).map((limit: string) => (
<RateLimitRow key={limit} limit={limit} value={rateLimits[limit]} updateRateLimit={updateRateLimit} />
))}
</Stack>
</>
);
};
export default UserRateLimits;

View File

@@ -1,131 +1,147 @@
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 { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin";
import { useAppContext } from "../../Context";
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 { 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 IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null;
}
};
const ServerCommandsPanel = () => {
const { etkeccAdmin } = useAppContext();
const createPath = useCreatePath();
const [ isLoading, setLoading ] = useState(true);
const [serverCommands, setServerCommands] = useState<{ [key: string]: ServerCommand }>({});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== "");
const [commandResult, setCommandResult] = useState<any[]>([]);
const [isLoading, setLoading] = useState(true);
const [serverCommands, setServerCommands] = useState<Record<string, ServerCommand>>({});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== "");
const [commandResult, setCommandResult] = useState<any[]>([]);
const dataProvider = useDataProvider();
const dataProvider = useDataProvider();
useEffect(() => {
const fetchIsAdmin = async () => {
const serverCommandsResponse = await dataProvider.getServerCommands(etkeccAdmin);
if (serverCommandsResponse) {
const serverCommands = serverCommandsResponse;
Object.keys(serverCommandsResponse).forEach((command: string) => {
serverCommands[command].additionalArgs = "";
});
setServerCommands(serverCommands);
}
setLoading(false);
useEffect(() => {
const fetchIsAdmin = async () => {
const serverCommandsResponse = await dataProvider.getServerCommands(etkeccAdmin);
if (serverCommandsResponse) {
const serverCommands = serverCommandsResponse;
Object.keys(serverCommandsResponse).forEach((command: string) => {
serverCommands[command].additionalArgs = "";
});
setServerCommands(serverCommands);
}
fetchIsAdmin();
}, []);
setLoading(false);
};
fetchIsAdmin();
}, []);
useEffect(() => {
if (serverProcess.command === "") {
useEffect(() => {
if (serverProcess.command === "") {
setCommandIsRunning(false);
}
}, [serverProcess]);
const setCommandAdditionalArgs = (command: string, additionalArgs: string) => {
const updatedServerCommands = { ...serverCommands };
updatedServerCommands[command].additionalArgs = additionalArgs;
setServerCommands(updatedServerCommands);
};
const runCommand = async (command: string) => {
setCommandResult([]);
setCommandIsRunning(true);
try {
const additionalArgs = serverCommands[command].additionalArgs || "";
const requestParams = additionalArgs ? { args: additionalArgs } : {};
const response = await dataProvider.runServerCommand(etkeccAdmin, command, requestParams);
if (!response.success) {
setCommandIsRunning(false);
return;
}
}, [serverProcess]);
const setCommandAdditionalArgs = (command: string, additionalArgs: string) => {
const updatedServerCommands = {...serverCommands};
updatedServerCommands[command].additionalArgs = additionalArgs;
setServerCommands(updatedServerCommands);
// Update UI with success message
const commandResults = buildCommandResultMessages(command, additionalArgs);
setCommandResult(commandResults);
// Reset the additional args field
resetCommandArgs(command);
// Update server process status
await updateServerProcessStatus(serverCommands[command]);
} catch (error) {
setCommandIsRunning(false);
}
};
const buildCommandResultMessages = (command: string, additionalArgs: string): React.ReactNode[] => {
const results: React.ReactNode[] = [];
let commandScheduledText = `Command scheduled: ${command}`;
if (additionalArgs) {
commandScheduledText += `, with additional args: ${additionalArgs}`;
}
const runCommand = async (command: string) => {
setCommandResult([]);
setCommandIsRunning(true);
results.push(<Box>{commandScheduledText}</Box>);
results.push(
<Box>
Expect your result in the{" "}
<Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon.
</Box>
);
try {
const additionalArgs = serverCommands[command].additionalArgs || "";
const requestParams = additionalArgs ? { args: additionalArgs } : {};
return results;
};
const response = await dataProvider.runServerCommand(etkeccAdmin, command, requestParams);
const resetCommandArgs = (command: string) => {
const updatedServerCommands = { ...serverCommands };
updatedServerCommands[command].additionalArgs = "";
setServerCommands(updatedServerCommands);
};
if (!response.success) {
setCommandIsRunning(false);
return;
}
// Update UI with success message
const commandResults = buildCommandResultMessages(command, additionalArgs);
setCommandResult(commandResults);
// Reset the additional args field
resetCommandArgs(command);
// Update server process status
await updateServerProcessStatus(serverCommands[command]);
} catch (error) {
setCommandIsRunning(false);
}
};
const buildCommandResultMessages = (command: string, additionalArgs: string): React.ReactNode[] => {
const results: React.ReactNode[] = [];
let commandScheduledText = `Command scheduled: ${command}`;
if (additionalArgs) {
commandScheduledText += `, with additional args: ${additionalArgs}`;
}
results.push(<Box>{commandScheduledText}</Box>);
results.push(
<Box>
Expect your result in the <Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon.
</Box>
);
return results;
};
const resetCommandArgs = (command: string) => {
const updatedServerCommands = {...serverCommands};
updatedServerCommands[command].additionalArgs = "";
setServerCommands(updatedServerCommands);
};
const updateServerProcessStatus = async (command: ServerCommand) => {
const commandIsLocking = command.with_lock;
const serverProcess = await dataProvider.getServerRunningProcess(etkeccAdmin, true);
if (!commandIsLocking && serverProcess.command === "") {
// if command is not locking, we simulate the "lock" mechanism so notifications will be refetched
serverProcess["command"] = command.name;
serverProcess["locked_at"] = new Date().toISOString();
}
setServerProcess({...serverProcess});
};
if (isLoading) {
return <Loading />
const updateServerProcessStatus = async (command: ServerCommand) => {
const commandIsLocking = command.with_lock;
const serverProcess = await dataProvider.getServerRunningProcess(etkeccAdmin, true);
if (!commandIsLocking && serverProcess.command === "") {
// if command is not locking, we simulate the "lock" mechanism so notifications will be refetched
serverProcess["command"] = command.name;
serverProcess["locked_at"] = new Date().toISOString();
}
return (<>
<h2>Server Commands</h2>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table">
<TableHead>
setServerProcess({ ...serverProcess });
};
if (isLoading) {
return <Loading />;
}
return (
<>
<h2>Server Commands</h2>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Command</TableCell>
<TableCell>Description</TableCell>
@@ -134,10 +150,7 @@ const ServerCommandsPanel = () => {
</TableHead>
<TableBody>
{Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => (
<TableRow
key={command}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableRow key={command} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
<TableCell scope="row">
<Box>
{renderIcon(icon)}
@@ -146,22 +159,28 @@ const ServerCommandsPanel = () => {
</TableCell>
<TableCell>{description}</TableCell>
<TableCell>
{args && <TextField
size="small"
variant="standard"
onChange={(e) => {
setCommandAdditionalArgs(command, e.target.value);
}}
value={additionalArgs}
/>}
{args && (
<TextField
size="small"
variant="standard"
onChange={e => {
setCommandAdditionalArgs(command, e.target.value);
}}
value={additionalArgs}
/>
)}
<Button
size="small"
variant="contained"
color="primary"
label="Run"
startIcon={<PlayArrow />}
onClick={() => { runCommand(command); }}
disabled={commandIsRunning || (args && typeof additionalArgs === 'string' && additionalArgs.length === 0)}
onClick={() => {
runCommand(command);
}}
disabled={
commandIsRunning || (args && typeof additionalArgs === "string" && additionalArgs.length === 0)
}
></Button>
</TableCell>
</TableRow>
@@ -170,13 +189,15 @@ const ServerCommandsPanel = () => {
</Table>
</TableContainer>
{commandResult.length > 0 && <Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
{commandResult.map((result, index) => (
<div key={index}>{result}</div>
))}
</Alert>}
{commandResult.length > 0 && (
<Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
{commandResult.map((result, index) => (
<div key={index}>{result}</div>
))}
</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 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 { useNavigate } from "react-router";
import { Fragment, useEffect, useState } from "react";
import { useAppContext } from "../../Context";
import { ServerNotificationsResponse, ServerProcessResponse } from "../../synapse/dataProvider";
import { getTimeSince } from "../../utils/date";
@@ -12,8 +29,14 @@ import { getTimeSince } from "../../utils/date";
const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;
const useServerNotifications = () => {
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { notifications: [], success: false });
const [ serverProcess, setServerProcess ] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", {
notifications: [],
success: false,
});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess;
const { etkeccAdmin } = useAppContext();
@@ -21,13 +44,16 @@ const useServerNotifications = () => {
const { notifications, success } = serverNotifications;
const fetchNotifications = async () => {
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin, command !== "");
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(
etkeccAdmin,
command !== ""
);
const serverNotifications = [...notificationsResponse.notifications];
serverNotifications.reverse();
setServerNotifications({
...notificationsResponse,
notifications: serverNotifications,
success: notificationsResponse.success
success: notificationsResponse.success,
});
};
@@ -60,7 +86,7 @@ const useServerNotifications = () => {
if (serverNotificationsInterval) {
clearInterval(serverNotificationsInterval);
}
}
};
}, [etkeccAdmin, command, locked_at]);
return { success, notifications, deleteServerNotifications };
@@ -89,7 +115,7 @@ export const ServerNotificationsBadge = () => {
};
const handleClearAllNotifications = async () => {
deleteServerNotifications()
deleteServerNotifications();
handleClose();
};
@@ -100,20 +126,21 @@ export const ServerNotificationsBadge = () => {
return (
<Box>
<IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}>
<Tooltip title={notifications && notifications.length > 0 ? `${notifications.length} new notifications` : `No notifications yet`}>
{notifications && notifications.length > 0 && (
<Badge badgeContent={notifications.length} color="error">
<NotificationsIcon />
</Badge>
) || <NotificationsIcon />}
<Tooltip
title={
notifications && notifications.length > 0
? `${notifications.length} new notifications`
: `No notifications yet`
}
>
{(notifications && notifications.length > 0 && (
<Badge badgeContent={notifications.length} color="error">
<NotificationsIcon />
</Badge>
)) || <NotificationsIcon />}
</Tooltip>
</IconButton>
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom-end"
style={{ zIndex: 1300 }}
>
<Popper open={open} anchorEl={anchorEl} placement="bottom-end" style={{ zIndex: 1300 }}>
<ClickAwayListener onClickAway={handleClose}>
<Paper
elevation={3}
@@ -125,12 +152,14 @@ export const ServerNotificationsBadge = () => {
minWidth: "300px",
maxWidth: {
xs: "100vw", // Full width on mobile
sm: "400px" // Fixed width on desktop
}
sm: "400px", // Fixed width on desktop
},
}}
>
{(!notifications || notifications.length === 0) ? (
<Typography sx={{ p: 1 }} variant="body2">No new notifications</Typography>
{!notifications || notifications.length === 0 ? (
<Typography sx={{ p: 1 }} variant="body2">
No new notifications
</Typography>
) : (
<List sx={{ p: 0 }} dense={true}>
<ListSubheader
@@ -141,46 +170,55 @@ export const ServerNotificationsBadge = () => {
fontWeight: "bold",
}}
>
<Typography variant="h6">Notifications</Typography>
<Box sx={{ cursor: "pointer", color: theme.palette.primary.main }} onClick={() => handleSeeAllNotifications()}>See all notifications</Box>
</ListSubheader>
<Typography variant="h6">Notifications</Typography>
<Box
sx={{ cursor: "pointer", color: theme.palette.primary.main }}
onClick={() => handleSeeAllNotifications()}
>
See all notifications
</Box>
</ListSubheader>
<Divider />
{notifications.map((notification, index) => {
return (<Fragment key={notification.event_id ? notification.event_id + index : index }>
<ListItem
onClick={() => handleSeeAllNotifications()}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
overflow: "hidden",
"&:hover": {
backgroundColor: "action.hover",
cursor: "pointer"
}
}}
>
<ListItemText
primary={
<Typography
variant="body2"
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
}}
dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }}
/>
}
/>
<ListItemText
primary={
<Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>{getTimeSince(notification.sent_at) + " ago"}</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
)})}
return (
<Fragment key={notification.event_id ? notification.event_id + index : index}>
<ListItem
onClick={() => handleSeeAllNotifications()}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
overflow: "hidden",
"&:hover": {
backgroundColor: "action.hover",
cursor: "pointer",
},
}}
>
<ListItemText
primary={
<Typography
variant="body2"
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
}}
dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }}
/>
}
/>
<ListItemText
primary={
<Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>
{getTimeSince(notification.sent_at) + " ago"}
</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
);
})}
<ListItem>
<Button
key="clear-all-notifications"
@@ -190,7 +228,7 @@ export const ServerNotificationsBadge = () => {
sx={{
pl: 0,
pt: 1,
verticalAlign: "middle"
verticalAlign: "middle",
}}
>
<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 { getTimeSince } from "../../utils/date";
import { Box, Typography, Paper, Button } from "@mui/material";
import { Stack } 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 dateFromDateString = new Date(date.replace(" ", "T") + "Z");
@@ -15,8 +16,8 @@ const DisplayTime = ({ date }: { date: string }) => {
const ServerNotificationsPage = () => {
const { etkeccAdmin } = useAppContext();
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", {
notifications: [],
success: false,
notifications: [],
success: false,
});
const notifications = serverNotifications.notifications;
@@ -26,13 +27,17 @@ const ServerNotificationsPage = () => {
<Stack spacing={1} direction="row" alignItems="center">
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}>
<Typography variant="h4">Server Notifications</Typography>
<Button variant="text" color="error" onClick={async () => {
await dataProvider.deleteServerNotifications(etkeccAdmin);
setServerNotifications({
notifications: [],
success: true,
});
}}>
<Button
variant="text"
color="error"
onClick={async () => {
await dataProvider.deleteServerNotifications(etkeccAdmin);
setServerNotifications({
notifications: [],
success: true,
});
}}
>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear
</Button>
</Box>

View File

@@ -1,48 +1,50 @@
import MonitorHeartIcon from "@mui/icons-material/MonitorHeart";
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 { useNavigate } from "react-router";
import { styled } 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";
interface StyledBadgeProps extends BadgeProps {
backgroundColor: string;
badgeColor: string
badgeColor: string;
theme?: Theme;
}
const StyledBadge = styled(Badge, { shouldForwardProp: (prop) => !['badgeColor', 'backgroundColor'].includes(prop as string) })<StyledBadgeProps>
(({ theme, backgroundColor, badgeColor }) => ({
'& .MuiBadge-badge': {
backgroundColor: backgroundColor,
color: badgeColor,
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
'&::after': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
animation: 'ripple 2.5s infinite ease-in-out',
border: '1px solid currentColor',
content: '""',
},
const StyledBadge = styled(Badge, {
shouldForwardProp: prop => !["badgeColor", "backgroundColor"].includes(prop as string),
})<StyledBadgeProps>(({ theme, backgroundColor, badgeColor }) => ({
"& .MuiBadge-badge": {
backgroundColor: backgroundColor,
color: badgeColor,
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
"&::after": {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
borderRadius: "50%",
animation: "ripple 2.5s infinite ease-in-out",
border: "1px solid currentColor",
content: '""',
},
'@keyframes ripple': {
'0%': {
transform: 'scale(.8)',
opacity: 1,
},
'100%': {
transform: 'scale(2.4)',
opacity: 0,
},
},
"@keyframes ripple": {
"0%": {
transform: "scale(.8)",
opacity: 1,
},
"100%": {
transform: "scale(2.4)",
opacity: 0,
},
},
}));
// every 5 minutes
@@ -51,8 +53,16 @@ const SERVER_STATUS_INTERVAL_TIME = 5 * 60 * 1000;
const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000;
const useServerStatus = () => {
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", { ok: false, success: false, host: "", results: [] });
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
ok: false,
success: false,
host: "",
results: [],
});
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { command, locked_at } = serverProcess;
const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider();
@@ -79,7 +89,7 @@ const useServerStatus = () => {
// start the interval after 10 seconds to avoid too many requests
serverStatusInterval = setInterval(checkServerStatus, SERVER_STATUS_INTERVAL_TIME);
}, 10000);
} else {
} else {
setServerStatus({ ok: false, success: false, host: "", results: [] });
}
@@ -90,26 +100,32 @@ const useServerStatus = () => {
if (serverStatusInterval) {
clearInterval(serverStatusInterval);
}
}
};
}, [etkeccAdmin, command]);
return { isOkay, successCheck };
};
const useCurrentServerProcess = () => {
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", { command: "", locked_at: "" });
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
command: "",
locked_at: "",
});
const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider();
const { command, locked_at } = serverProcess;
const checkServerRunningProcess = async () => {
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(etkeccAdmin, command !== "");
const serverProcess: ServerProcessResponse = await dataProvider.getServerRunningProcess(
etkeccAdmin,
command !== ""
);
setServerProcess({
...serverProcess,
command: serverProcess.command,
locked_at: serverProcess.locked_at
locked_at: serverProcess.locked_at,
});
}
};
useEffect(() => {
let serverCheckInterval: NodeJS.Timeout | null = null;
@@ -131,13 +147,23 @@ const useCurrentServerProcess = () => {
if (serverCheckInterval) {
clearInterval(serverCheckInterval);
}
}
};
}, [etkeccAdmin, command]);
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();
let badgeBackgroundColor = 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];
}
return <StyledBadge
return (
<StyledBadge
overlap="circular"
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
variant="dot"
backgroundColor={badgeBackgroundColor}
badgeColor={badgeColor}
@@ -161,35 +188,43 @@ export const ServerStatusStyledBadge = ({ command, locked_at, isOkay, inSidebar
<Avatar sx={{ height: 24, width: 24, background: avatarBackgroundColor }}>
<MonitorHeartIcon sx={{ height: 22, width: 22, color: theme.palette.common.white }} />
</Avatar>
</StyledBadge>
</StyledBadge>
);
};
const ServerStatusBadge = () => {
const { isOkay, successCheck } = useServerStatus();
const { command, locked_at } = useCurrentServerProcess();
const navigate = useNavigate();
const { isOkay, successCheck } = useServerStatus();
const { command, locked_at } = useCurrentServerProcess();
const navigate = useNavigate();
if (!successCheck) {
return null;
}
if (!successCheck) {
return null;
}
const handleServerStatusClick = () => {
navigate("/server_status");
};
const handleServerStatusClick = () => {
navigate("/server_status");
};
let tooltipText = "Click to view Server Status";
let tooltipText = "Click to view Server Status";
if (command && locked_at) {
tooltipText = `Running: ${command}; ${tooltipText}`;
}
if (command && locked_at) {
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" }}>
<Box>
<ServerStatusStyledBadge inSidebar={false} command={command || ""} locked_at={locked_at || ""} isOkay={isOkay} />
</Box>
<Box>
<ServerStatusStyledBadge
inSidebar={false}
command={command || ""}
locked_at={locked_at || ""}
isOkay={isOkay}
/>
</Box>
</Tooltip>
</Button>
);
};
export default ServerStatusBadge;

View File

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

View File

@@ -1,17 +1,26 @@
import { get } from "lodash";
import { useState } from "react";
import BlockIcon from "@mui/icons-material/Block";
import IconCancel from "@mui/icons-material/Cancel";
import ClearIcon from "@mui/icons-material/Clear";
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
import DownloadIcon from "@mui/icons-material/Download";
import DownloadingIcon from "@mui/icons-material/Downloading";
import FileOpenIcon from "@mui/icons-material/FileOpen";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import DownloadIcon from '@mui/icons-material/Download';
import DownloadingIcon from '@mui/icons-material/Downloading';
import { Grid2 as Grid, Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip, Link } from "@mui/material";
import {
Grid2 as Grid,
Box,
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
Tooltip,
Link,
} from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles";
import { useMutation } from "@tanstack/react-query";
import { get } from "lodash";
import { useState } from "react";
import {
BooleanInput,
Button,
@@ -30,12 +39,11 @@ import {
useRefresh,
useTranslate,
} from "react-admin";
import { useMutation } from "@tanstack/react-query";
import { dateParser } from "../utils/date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { dateParser } from "../utils/date";
import decodeURLComponent from "../utils/decodeURLComponent";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate();
@@ -55,24 +63,9 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
<DialogContent>
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
<DateTimeInput
source="before_ts"
label="delete_media.fields.before_ts"
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}
/>
<DateTimeInput source="before_ts" label="delete_media.fields.before_ts" 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>
</DialogContent>
</Dialog>
@@ -410,7 +403,7 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
document.body.appendChild(anchorElement);
anchorElement.click();
document.body.removeChild(anchorElement);
setTimeout(() => URL.revokeObjectURL(blobURL), 10);;
setTimeout(() => URL.revokeObjectURL(blobURL), 10);
};
const handleFile = async (preview: boolean) => {
@@ -460,7 +453,7 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
onClick={() => handleFile(false)}
style={{ minWidth: 0, padding: 0, marginRight: 8 }}
>
{loading ? <DownloadingIcon /> : <DownloadIcon />}
{loading ? <DownloadingIcon /> : <DownloadIcon />}
</Button>
<span>{label}</span>
</Box>
@@ -491,7 +484,7 @@ export const MediaIDField = ({ source }) => {
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 }) => {
@@ -510,5 +503,5 @@ export const ReportMediaContent = ({ source }) => {
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 { CardContent, CardHeader, Container } from "@mui/material";
import { useTranslate } from "ra-core";
import { ParsedStats, Progress } from "./types";
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();
return <option value={value}>{translate(text)}</option>;
}
};
const ConflictModeCard = ({ stats, importResults, onConflictModeChanged, conflictMode, progress }:
{ stats: ParsedStats | null, importResults: any, onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>, conflictMode: string, progress: Progress }) => {
const ConflictModeCard = ({
stats,
importResults,
onConflictModeChanged,
conflictMode,
progress,
}: {
stats: ParsedStats | null;
importResults: any;
onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>;
conflictMode: string;
progress: Progress;
}) => {
const translate = useTranslate();
if (!stats || importResults) {
@@ -25,16 +35,16 @@ const ConflictModeCard = ({ stats, importResults, onConflictModeChanged, conflic
<CardHeader
title={translate("import_users.cards.conflicts.header")}
sx={{ borderBottom: 1, borderColor: "divider" }}
/>
<CardContent>
<NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
<TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
<TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
/>
<CardContent>
<NativeSelect onChange={onConflictModeChanged} value={conflictMode} disabled={progress !== null}>
<TranslatableOption value="stop" text="import_users.cards.conflicts.mode.stop" />
<TranslatableOption value="skip" text="import_users.cards.conflicts.mode.skip" />
</NativeSelect>
</CardContent>
</Paper>
</Container>
);
}
};
export default ConflictModeCard;
export default ConflictModeCard;

View File

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

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 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();
if (!importResults) {
@@ -30,7 +49,7 @@ const ResultsCard = ({ importResults, downloadSkippedRecords }: { importResults:
{translate("import_users.cards.results.successful", importResults.succeededRecords.length)}
</Typography>
<List dense>
{importResults.succeededRecords.map((record) => (
{importResults.succeededRecords.map(record => (
<ListItem key={record.id}>
<ListItemText primary={record.displayname} />
</ListItem>
@@ -47,8 +66,7 @@ const ResultsCard = ({ importResults, downloadSkippedRecords }: { importResults:
onClick={downloadSkippedRecords}
sx={{ mt: 2 }}
label={translate("import_users.cards.results.download_skipped")}
>
</Button>
></Button>
</Box>
)}
{importResults.erroredRecords.length > 0 && (
@@ -58,15 +76,17 @@ const ResultsCard = ({ importResults, downloadSkippedRecords }: { importResults:
)}
{importResults.wasDryRun && (
<Alert severity="warning" key="simulated">
{translate("import_users.cards.results.simulated_only")}
</Alert>
)}
<Alert severity="warning" key="simulated">
{translate("import_users.cards.results.simulated_only")}
</Alert>
)}
</Stack>
</CardContent>
</Paper>
<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>
</Container>
);

View File

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

View File

@@ -4,9 +4,26 @@ import { FormControlLabel } from "@mui/material";
import { Checkbox } from "@mui/material";
import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react";
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();
if (!stats) {
@@ -80,4 +97,4 @@ const StatsCard = ({ stats, progress, importResults, useridMode, passwordMode, o
);
};
export default StatsCard;
export default StatsCard;

View File

@@ -1,10 +1,18 @@
import { CardHeader, CardContent, Container, Link, Stack, Typography, Paper } from "@mui/material";
import { useTranslate } from "ra-core";
import { ChangeEventHandler } from "react";
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();
if (importResults) {
return null;
@@ -33,4 +41,4 @@ const UploadCard = ({ importResults, onFileChange, progress }: { importResults:
);
};
export default UploadCard;
export default UploadCard;

View File

@@ -1,16 +1,14 @@
import {
Stack,
} from "@mui/material";
import { Stack } from "@mui/material";
import { useTranslate } from "ra-core";
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 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 {
@@ -29,23 +27,43 @@ const UserImport = () => {
onConflictModeChanged,
onPasswordModeChange,
onUseridModeChanged,
downloadSkippedRecords
downloadSkippedRecords,
} = useImportFile();
const translate = useTranslate();
return (
<Stack spacing={3} mt={3} direction="column">
<Title defaultTitle={translate("import_users.title")} />
<UploadCard importResults={importResults} onFileChange={onFileChange} progress={progress} />
<ErrorsCard errors={errors} />
<ConflictModeCard stats={stats} 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} />
<Title defaultTitle={translate("import_users.title")} />
<UploadCard importResults={importResults} onFileChange={onFileChange} progress={progress} />
<ErrorsCard errors={errors} />
<ConflictModeCard
stats={stats}
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} />
</Stack>
);
};
export default UserImport;

View File

@@ -14,7 +14,7 @@ export interface ImportLine {
}
export interface ParsedStats {
user_types: { [key: string]: number };
user_types: Record<string, number>;
is_guest: number;
admin: number;
deactivated: number;
@@ -44,4 +44,4 @@ export interface ImportResult {
totalRecordCount: number;
changeStats: ChangeStats;
wasDryRun: boolean;
}
}

View File

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