remove unused eslint plugin, run eslint --fix, rollback node memory workaround in ci
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
||||
|
||||
import { isASManaged } from "../utils/mxid";
|
||||
|
||||
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user