(etke.cc-exclusive) Server Actions page (#457)
* WIP: add scheduler commands * refactor scheduler commands page * WIP on CRUD for ScheduledCommands * more refactoring, finish CRUD On scheduled pages * Add info text about Scheduler service * Finish recurring commands add/edit * add more texts * fix server status behavior on not-loaded-yet state; adjust texts; lint fixes * add link to the help pages in the commands palette * Move Commands Panel to ServerSchedulesPage * Rename Server Schedules to Server Actions * more texts, a bit changed visual of the actions page, lint fix * add docs * fix tests * Add UTC label to scheduled command create/edit
This commit is contained in:
parent
e2d3c0792b
commit
0832c43d76
@ -122,6 +122,7 @@ The following list contains such features - they are only available for [etke.cc
|
|||||||
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
||||||
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
|
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
|
||||||
* 🛠️ [Server Commands panel](https://github.com/etkecc/synapse-admin/pull/365)
|
* 🛠️ [Server Commands panel](https://github.com/etkecc/synapse-admin/pull/365)
|
||||||
|
* 🚀 [Server Actions page](https://github.com/etkecc/synapse-admin/pull/457)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
|
BIN
screenshots/etke.cc/server-actions/page.webp
Normal file
BIN
screenshots/etke.cc/server-actions/page.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
10
src/App.tsx
10
src/App.tsx
@ -5,8 +5,12 @@ import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin
|
|||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
|
|
||||||
import AdminLayout from "./components/AdminLayout";
|
import AdminLayout from "./components/AdminLayout";
|
||||||
|
import ServerActionsPage from "./components/etke.cc/ServerActionsPage";
|
||||||
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
||||||
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||||
|
import RecurringCommandEdit from "./components/etke.cc/schedules/components/recurring/RecurringCommandEdit";
|
||||||
|
import ScheduledCommandEdit from "./components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit";
|
||||||
|
import ScheduledCommandShow from "./components/etke.cc/schedules/components/scheduled/ScheduledCommandShow";
|
||||||
import UserImport from "./components/user-import/UserImport";
|
import UserImport from "./components/user-import/UserImport";
|
||||||
import germanMessages from "./i18n/de";
|
import germanMessages from "./i18n/de";
|
||||||
import englishMessages from "./i18n/en";
|
import englishMessages from "./i18n/en";
|
||||||
@ -64,6 +68,12 @@ export const App = () => (
|
|||||||
<CustomRoutes>
|
<CustomRoutes>
|
||||||
<Route path="/import_users" element={<UserImport />} />
|
<Route path="/import_users" element={<UserImport />} />
|
||||||
<Route path="/server_status" element={<ServerStatusPage />} />
|
<Route path="/server_status" element={<ServerStatusPage />} />
|
||||||
|
<Route path="/server_actions" element={<ServerActionsPage />} />
|
||||||
|
<Route path="/server_actions/scheduled/:id/show" element={<ScheduledCommandShow />} />
|
||||||
|
<Route path="/server_actions/scheduled/:id" element={<ScheduledCommandEdit />} />
|
||||||
|
<Route path="/server_actions/scheduled/create" element={<ScheduledCommandEdit />} />
|
||||||
|
<Route path="/server_actions/recurring/:id" element={<RecurringCommandEdit />} />
|
||||||
|
<Route path="/server_actions/recurring/create" element={<RecurringCommandEdit />} />
|
||||||
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
||||||
</CustomRoutes>
|
</CustomRoutes>
|
||||||
<Resource {...users} />
|
<Resource {...users} />
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import ManageHistoryIcon from "@mui/icons-material/ManageHistory";
|
||||||
import { useEffect, useState, Suspense } from "react";
|
import { useEffect, useState, Suspense } from "react";
|
||||||
import {
|
import {
|
||||||
CheckForApplicationUpdate,
|
CheckForApplicationUpdate,
|
||||||
@ -75,11 +76,11 @@ const AdminAppBar = () => {
|
|||||||
|
|
||||||
const AdminMenu = props => {
|
const AdminMenu = props => {
|
||||||
const [menu, setMenu] = useState([] as MenuItem[]);
|
const [menu, setMenu] = useState([] as MenuItem[]);
|
||||||
const [serverStatusEnabled, setServerStatusEnabled] = useState(false);
|
const [etkeRoutesEnabled, setEtkeRoutesEnabled] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMenu(GetConfig().menu);
|
setMenu(GetConfig().menu);
|
||||||
if (GetConfig().etkeccAdmin) {
|
if (GetConfig().etkeccAdmin) {
|
||||||
setServerStatusEnabled(true);
|
setEtkeRoutesEnabled(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
@ -95,8 +96,9 @@ const AdminMenu = props => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu {...props}>
|
<Menu {...props}>
|
||||||
{serverStatusEnabled && (
|
{etkeRoutesEnabled && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
|
key="server_status"
|
||||||
to="/server_status"
|
to="/server_status"
|
||||||
leftIcon={
|
leftIcon={
|
||||||
<ServerStatusStyledBadge
|
<ServerStatusStyledBadge
|
||||||
@ -104,11 +106,20 @@ const AdminMenu = props => {
|
|||||||
command={serverProcess.command}
|
command={serverProcess.command}
|
||||||
locked_at={serverProcess.locked_at}
|
locked_at={serverProcess.locked_at}
|
||||||
isOkay={serverStatus.ok}
|
isOkay={serverStatus.ok}
|
||||||
|
isLoaded={serverStatus.success}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
primaryText="Server Status"
|
primaryText="Server Status"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{etkeRoutesEnabled && (
|
||||||
|
<Menu.Item
|
||||||
|
key="server_actions"
|
||||||
|
to="/server_actions"
|
||||||
|
leftIcon={<ManageHistoryIcon />}
|
||||||
|
primaryText="Server Actions"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Menu.ResourceItems />
|
<Menu.ResourceItems />
|
||||||
{menu &&
|
{menu &&
|
||||||
menu.map((item, index) => {
|
menu.map((item, index) => {
|
||||||
|
37
src/components/etke.cc/CurrentlyRunningCommand.tsx
Normal file
37
src/components/etke.cc/CurrentlyRunningCommand.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Stack, Tooltip, Typography, Box, Link } from "@mui/material";
|
||||||
|
import { useStore } from "react-admin";
|
||||||
|
|
||||||
|
import { ServerProcessResponse } from "../../synapse/dataProvider";
|
||||||
|
import { getTimeSince } from "../../utils/date";
|
||||||
|
|
||||||
|
const CurrentlyRunningCommand = () => {
|
||||||
|
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
|
command: "",
|
||||||
|
locked_at: "",
|
||||||
|
});
|
||||||
|
const { command, locked_at } = serverProcess;
|
||||||
|
|
||||||
|
if (!command || !locked_at) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CurrentlyRunningCommand;
|
@ -47,10 +47,21 @@ In the application bar the new notifications icon is displayed that shows the nu
|
|||||||
|
|
||||||
When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server.
|
When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server.
|
||||||
|
|
||||||
|
### Server Actions Page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When you click on the `Server Actions` sidebar menu item, you will be redirected to the Server Actions page.
|
||||||
|
On this page you can do the following:
|
||||||
|
|
||||||
|
* [Run a command](#server-commands-panel) on your server immediately
|
||||||
|
* [Schedule a command](https://etke.cc/help/extras/scheduler/#schedule) to run at a specific date and time
|
||||||
|
* [Configure a recurring schedule](https://etke.cc/help/extras/scheduler/#recurring) for a command to run at a specific time every week
|
||||||
|
|
||||||
### Server Commands Panel
|
### Server Commands Panel
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
When you open [Server Status page](#server-status-page), you will see the Server Commands panel. This panel contains all
|
When you open [Server Actions page](#server-status-page), you will see the Server Commands panel.
|
||||||
[the commands](https://etke.cc/help/extras/scheduler/#commands) you can run on your server in 1 click. Once command is finished, you will get a notification about the
|
This panel contains all [the commands](https://etke.cc/help/extras/scheduler/#commands) you can run on your server in 1 click.
|
||||||
result.
|
Once command is finished, you will get a notification about the result.
|
||||||
|
51
src/components/etke.cc/ServerActionsPage.tsx
Normal file
51
src/components/etke.cc/ServerActionsPage.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import RestoreIcon from "@mui/icons-material/Restore";
|
||||||
|
import ScheduleIcon from "@mui/icons-material/Schedule";
|
||||||
|
import { Box, Typography, Link, Divider } from "@mui/material";
|
||||||
|
import { Stack } from "@mui/material";
|
||||||
|
|
||||||
|
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
||||||
|
import ServerCommandsPanel from "./ServerCommandsPanel";
|
||||||
|
import RecurringCommandsList from "./schedules/components/recurring/RecurringCommandsList";
|
||||||
|
import ScheduledCommandsList from "./schedules/components/scheduled/ScheduledCommandsList";
|
||||||
|
const ServerActionsPage = () => {
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
<Stack direction="column">
|
||||||
|
<CurrentlyRunningCommand />
|
||||||
|
<ServerCommandsPanel />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="h5">
|
||||||
|
<ScheduleIcon sx={{ verticalAlign: "middle", mr: 1 }} /> Scheduled commands
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
The following commands are scheduled to run at specific times. You can view their details and modify them as
|
||||||
|
needed. More details about the mode can be found{" "}
|
||||||
|
<Link href="https://etke.cc/help/extras/scheduler/#schedule" target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
<ScheduledCommandsList />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="h5">
|
||||||
|
<RestoreIcon sx={{ verticalAlign: "middle", mr: 1 }} /> Recurring commands
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
The following commands are set to run at specific weekday and time (weekly). You can view their details and
|
||||||
|
modify them as needed. More details about the mode can be found{" "}
|
||||||
|
<Link href="https://etke.cc/help/extras/scheduler/#recurring" target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
<RecurringCommandsList />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerActionsPage;
|
@ -1,4 +1,4 @@
|
|||||||
import { PlayArrow, CheckCircle } from "@mui/icons-material";
|
import { PlayArrow, CheckCircle, HelpCenter, Construction } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -10,12 +10,15 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
TextField,
|
TextField,
|
||||||
Box,
|
Box,
|
||||||
|
Link,
|
||||||
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin";
|
import { Button, Loading, useDataProvider, useCreatePath, useStore } from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import { useAppContext } from "../../Context";
|
import { useAppContext } from "../../Context";
|
||||||
|
import { useServerCommands } from "./hooks/useServerCommands";
|
||||||
import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider";
|
import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider";
|
||||||
import { Icons } from "../../utils/icons";
|
import { Icons } from "../../utils/icons";
|
||||||
|
|
||||||
@ -26,34 +29,20 @@ const renderIcon = (icon: string) => {
|
|||||||
|
|
||||||
const ServerCommandsPanel = () => {
|
const ServerCommandsPanel = () => {
|
||||||
const { etkeccAdmin } = useAppContext();
|
const { etkeccAdmin } = useAppContext();
|
||||||
const createPath = useCreatePath();
|
if (!etkeccAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(true);
|
const createPath = useCreatePath();
|
||||||
const [serverCommands, setServerCommands] = useState<Record<string, ServerCommand>>({});
|
const { isLoading, serverCommands, setServerCommands } = useServerCommands();
|
||||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
command: "",
|
command: "",
|
||||||
locked_at: "",
|
locked_at: "",
|
||||||
});
|
});
|
||||||
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== "");
|
const [commandIsRunning, setCommandIsRunning] = useState<boolean>(serverProcess.command !== "");
|
||||||
const [commandResult, setCommandResult] = useState<any[]>([]);
|
const [commandResult, setCommandResult] = useState<React.ReactNode[]>([]);
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
fetchIsAdmin();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serverProcess.command === "") {
|
if (serverProcess.command === "") {
|
||||||
setCommandIsRunning(false);
|
setCommandIsRunning(false);
|
||||||
@ -103,11 +92,12 @@ const ServerCommandsPanel = () => {
|
|||||||
commandScheduledText += `, with additional args: ${additionalArgs}`;
|
commandScheduledText += `, with additional args: ${additionalArgs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(<Box>{commandScheduledText}</Box>);
|
results.push(<Box key="command-text">{commandScheduledText}</Box>);
|
||||||
results.push(
|
results.push(
|
||||||
<Box>
|
<Box key="notification-link">
|
||||||
Expect your result in the{" "}
|
Expect your result in the{" "}
|
||||||
<Link to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</Link> page soon.
|
<RouterLink to={createPath({ resource: "server_notifications", type: "list" })}>Notifications</RouterLink> page
|
||||||
|
soon.
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -138,25 +128,40 @@ const ServerCommandsPanel = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Server Commands</h2>
|
<Typography variant="h5">
|
||||||
<TableContainer component={Paper}>
|
<Construction sx={{ verticalAlign: "middle", mr: 1 }} /> Available Commands
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ mt: 0 }}>
|
||||||
|
The following commands are available to run. More details about each of them can be found{" "}
|
||||||
|
<Link href="https://etke.cc/help/extras/scheduler/#commands" target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||||
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table">
|
<Table sx={{ minWidth: 450 }} size="small" aria-label="simple table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Command</TableCell>
|
<TableCell>Command</TableCell>
|
||||||
|
<TableCell></TableCell>
|
||||||
<TableCell>Description</TableCell>
|
<TableCell>Description</TableCell>
|
||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => (
|
{Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => (
|
||||||
<TableRow key={command} sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
|
<TableRow key={command}>
|
||||||
<TableCell scope="row">
|
<TableCell scope="row">
|
||||||
<Box>
|
<Box>
|
||||||
{renderIcon(icon)}
|
{renderIcon(icon)}
|
||||||
{command}
|
{command}
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link href={"https://etke.cc/help/extras/scheduler/#" + command} target="_blank">
|
||||||
|
<Button size="small" startIcon={<HelpCenter />} title={command + " help"} />
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{description}</TableCell>
|
<TableCell>{description}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{args && (
|
{args && (
|
||||||
|
@ -157,16 +157,26 @@ export const ServerStatusStyledBadge = ({
|
|||||||
command,
|
command,
|
||||||
locked_at,
|
locked_at,
|
||||||
isOkay,
|
isOkay,
|
||||||
|
isLoaded,
|
||||||
inSidebar = false,
|
inSidebar = false,
|
||||||
}: {
|
}: {
|
||||||
command: string;
|
command: string;
|
||||||
locked_at: string;
|
locked_at: string;
|
||||||
isOkay: boolean;
|
isOkay: boolean;
|
||||||
|
isLoaded: boolean;
|
||||||
inSidebar: boolean;
|
inSidebar: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
let badgeBackgroundColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
let badgeBackgroundColor = isLoaded
|
||||||
let badgeColor = isOkay ? theme.palette.success.light : theme.palette.error.main;
|
? isOkay
|
||||||
|
? theme.palette.success.light
|
||||||
|
: theme.palette.error.main
|
||||||
|
: theme.palette.grey[600];
|
||||||
|
let badgeColor = isLoaded
|
||||||
|
? isOkay
|
||||||
|
? theme.palette.success.light
|
||||||
|
: theme.palette.error.main
|
||||||
|
: theme.palette.grey[600];
|
||||||
|
|
||||||
if (command && locked_at) {
|
if (command && locked_at) {
|
||||||
badgeBackgroundColor = theme.palette.warning.main;
|
badgeBackgroundColor = theme.palette.warning.main;
|
||||||
@ -220,6 +230,7 @@ const ServerStatusBadge = () => {
|
|||||||
command={command || ""}
|
command={command || ""}
|
||||||
locked_at={locked_at || ""}
|
locked_at={locked_at || ""}
|
||||||
isOkay={isOkay}
|
isOkay={isOkay}
|
||||||
|
isLoaded={successCheck}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import CheckIcon from "@mui/icons-material/Check";
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import EngineeringIcon from "@mui/icons-material/Engineering";
|
import EngineeringIcon from "@mui/icons-material/Engineering";
|
||||||
import { Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
|
import { Alert, Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
|
||||||
import { useStore } from "ra-core";
|
import { useStore } from "ra-core";
|
||||||
|
|
||||||
import ServerCommandsPanel from "./ServerCommandsPanel";
|
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
||||||
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
|
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
|
||||||
import { getTimeSince } from "../../utils/date";
|
import { getTimeSince } from "../../utils/date";
|
||||||
|
|
||||||
@ -68,8 +68,7 @@ const ServerStatusPage = () => {
|
|||||||
return (
|
return (
|
||||||
<Paper elevation={3} sx={{ p: 3, mt: 3 }}>
|
<Paper elevation={3} sx={{ p: 3, mt: 3 }}>
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
<CloseIcon color="error" />
|
<Typography color="info">Fetching real-time server health... Just a moment!</Typography>
|
||||||
<Typography color="error">Unable to fetch server status. Please try again later.</Typography>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
@ -86,25 +85,23 @@ const ServerStatusPage = () => {
|
|||||||
{host}
|
{host}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</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>
|
|
||||||
<Tooltip title={locked_at.toString()}>
|
|
||||||
<Typography component="span" color="text.secondary" sx={{ display: "inline-block", ml: 1 }}>
|
|
||||||
(started {getTimeSince(locked_at)} ago)
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ServerCommandsPanel />
|
<CurrentlyRunningCommand />
|
||||||
|
|
||||||
|
<Typography variant="body1">
|
||||||
|
This is a{" "}
|
||||||
|
<Link href="https://etke.cc/services/monitoring/" target="_blank">
|
||||||
|
monitoring report
|
||||||
|
</Link>{" "}
|
||||||
|
of the server. If any of the checks below concern you, please check the{" "}
|
||||||
|
<Link
|
||||||
|
href="https://etke.cc/services/monitoring/#what-to-do-if-the-monitoring-report-shows-issues"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
suggested actions
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Stack spacing={2} direction="row">
|
<Stack spacing={2} direction="row">
|
||||||
{Object.keys(groupedResults).map((category, idx) => (
|
{Object.keys(groupedResults).map((category, idx) => (
|
||||||
|
29
src/components/etke.cc/hooks/useServerCommands.ts
Normal file
29
src/components/etke.cc/hooks/useServerCommands.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useDataProvider } from "react-admin";
|
||||||
|
|
||||||
|
import { useAppContext } from "../../../Context";
|
||||||
|
import { ServerCommand } from "../../../synapse/dataProvider";
|
||||||
|
|
||||||
|
export const useServerCommands = () => {
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [serverCommands, setServerCommands] = useState<Record<string, ServerCommand>>({});
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchServerCommands = 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);
|
||||||
|
};
|
||||||
|
fetchServerCommands();
|
||||||
|
}, [dataProvider, etkeccAdmin]);
|
||||||
|
|
||||||
|
return { isLoading, serverCommands, setServerCommands };
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
||||||
|
return Object.entries(commands).map(([key, value]) => ({
|
||||||
|
id: key,
|
||||||
|
name: value.name,
|
||||||
|
description: value.description,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScheduledCommandCreate = () => {
|
||||||
|
const commandChoices = transformCommandsToChoices(serverCommands);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<SelectInput
|
||||||
|
source="command"
|
||||||
|
choices={commandChoices}
|
||||||
|
optionText={choice => `${choice.name} - ${choice.description}`}
|
||||||
|
/>
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,182 @@
|
|||||||
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
|
import { Card, CardContent, CardHeader, Box, Alert, Typography, Link } from "@mui/material";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
TextInput,
|
||||||
|
SaveButton,
|
||||||
|
useNotify,
|
||||||
|
useDataProvider,
|
||||||
|
Loading,
|
||||||
|
Button,
|
||||||
|
SelectInput,
|
||||||
|
TimeInput,
|
||||||
|
} from "react-admin";
|
||||||
|
import { useWatch } from "react-hook-form";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import RecurringDeleteButton from "./RecurringDeleteButton";
|
||||||
|
import { useAppContext } from "../../../../../Context";
|
||||||
|
import { RecurringCommand } from "../../../../../synapse/dataProvider";
|
||||||
|
import { useServerCommands } from "../../../hooks/useServerCommands";
|
||||||
|
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
|
||||||
|
|
||||||
|
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
||||||
|
return Object.entries(commands).map(([key, value]) => ({
|
||||||
|
id: key,
|
||||||
|
name: value.name,
|
||||||
|
description: value.description,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArgumentsField = ({ serverCommands }) => {
|
||||||
|
const selectedCommand = useWatch({ name: "command" });
|
||||||
|
const showArgs = selectedCommand && serverCommands[selectedCommand]?.args === true;
|
||||||
|
|
||||||
|
if (!showArgs) return null;
|
||||||
|
|
||||||
|
return <TextInput required source="args" label="Arguments" fullWidth multiline />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecurringCommandEdit = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const notify = useNotify();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const [command, setCommand] = useState<RecurringCommand | undefined>(undefined);
|
||||||
|
const isCreating = typeof id === "undefined";
|
||||||
|
const [loading, setLoading] = useState(!isCreating);
|
||||||
|
const { data: recurringCommands, isLoading: isLoadingList } = useRecurringCommands();
|
||||||
|
const { serverCommands, isLoading: isLoadingServerCommands } = useServerCommands();
|
||||||
|
const pageTitle = isCreating ? "Create Recurring Command" : "Edit Recurring Command";
|
||||||
|
|
||||||
|
const commandChoices = transformCommandsToChoices(serverCommands);
|
||||||
|
const dayOfWeekChoices = [
|
||||||
|
{ id: "Monday", name: "Monday" },
|
||||||
|
{ id: "Tuesday", name: "Tuesday" },
|
||||||
|
{ id: "Wednesday", name: "Wednesday" },
|
||||||
|
{ id: "Thursday", name: "Thursday" },
|
||||||
|
{ id: "Friday", name: "Friday" },
|
||||||
|
{ id: "Saturday", name: "Saturday" },
|
||||||
|
{ id: "Sunday", name: "Sunday" },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCreating && recurringCommands) {
|
||||||
|
const commandToEdit = recurringCommands.find(cmd => cmd.id === id);
|
||||||
|
if (commandToEdit) {
|
||||||
|
const timeValue = commandToEdit.time || "";
|
||||||
|
const timeParts = timeValue.split(" ");
|
||||||
|
|
||||||
|
const parsedCommand = {
|
||||||
|
...commandToEdit,
|
||||||
|
day_of_week: timeParts.length > 1 ? timeParts[0] : "Monday",
|
||||||
|
execution_time: timeParts.length > 1 ? timeParts[1] : timeValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
setCommand(parsedCommand);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [id, recurringCommands, isCreating]);
|
||||||
|
|
||||||
|
const handleSubmit = async data => {
|
||||||
|
try {
|
||||||
|
// Format the time from the Date object to a string in HH:MM format
|
||||||
|
let formattedTime = "00:00";
|
||||||
|
|
||||||
|
if (data.execution_time instanceof Date) {
|
||||||
|
const hours = String(data.execution_time.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(data.execution_time.getMinutes()).padStart(2, "0");
|
||||||
|
formattedTime = `${hours}:${minutes}`;
|
||||||
|
} else if (typeof data.execution_time === "string") {
|
||||||
|
formattedTime = data.execution_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissionData = {
|
||||||
|
...data,
|
||||||
|
time: `${data.day_of_week} ${formattedTime}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
delete submissionData.day_of_week;
|
||||||
|
delete submissionData.execution_time;
|
||||||
|
delete submissionData.scheduled_at;
|
||||||
|
|
||||||
|
// Only include args when it's required for the selected command
|
||||||
|
const selectedCommand = data.command;
|
||||||
|
if (!selectedCommand || !serverCommands[selectedCommand]?.args) {
|
||||||
|
delete submissionData.args;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (isCreating) {
|
||||||
|
result = await dataProvider.createRecurringCommand(etkeccAdmin, submissionData);
|
||||||
|
notify("recurring_commands.action.create_success", { type: "success" });
|
||||||
|
} else {
|
||||||
|
result = await dataProvider.updateRecurringCommand(etkeccAdmin, {
|
||||||
|
...submissionData,
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
notify("recurring_commands.action.update_success", { type: "success" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate scheduled commands queries
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["scheduledCommands"] });
|
||||||
|
|
||||||
|
navigate("/server_actions");
|
||||||
|
} catch (error) {
|
||||||
|
notify("recurring_commands.action.update_failure", { type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading || isLoadingList || isLoadingServerCommands) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Button label="Back" onClick={() => navigate("/server_actions")} startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader title={pageTitle} />
|
||||||
|
<CardContent>
|
||||||
|
{command && (
|
||||||
|
<Alert severity="info">
|
||||||
|
<Typography variant="body1" sx={{ px: 2 }}>
|
||||||
|
You can find more details about the command{" "}
|
||||||
|
<Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Form
|
||||||
|
defaultValues={command || undefined}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
record={command || undefined}
|
||||||
|
warnWhenUnsavedChanges
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
{!isCreating && <TextInput readOnly source="id" label="ID" fullWidth required />}
|
||||||
|
<SelectInput source="command" choices={commandChoices} label="Command" fullWidth required />
|
||||||
|
<ArgumentsField serverCommands={serverCommands} />
|
||||||
|
<SelectInput source="day_of_week" choices={dayOfWeekChoices} label="Day of Week" fullWidth required />
|
||||||
|
<TimeInput source="execution_time" label="Time (UTC)" fullWidth required />
|
||||||
|
<Box mt={2} display="flex" justifyContent="space-between">
|
||||||
|
<SaveButton label={isCreating ? "Create" : "Update"} />
|
||||||
|
{!isCreating && <RecurringDeleteButton />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecurringCommandEdit;
|
@ -0,0 +1,62 @@
|
|||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { Paper } from "@mui/material";
|
||||||
|
import { Loading, Button } from "react-admin";
|
||||||
|
import { DateField } from "react-admin";
|
||||||
|
import { Datagrid } from "react-admin";
|
||||||
|
import { ListContextProvider, TextField, TopToolbar, Identifier } from "react-admin";
|
||||||
|
import { ResourceContextProvider, useList } from "react-admin";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { DATE_FORMAT } from "../../../../../utils/date";
|
||||||
|
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
|
||||||
|
|
||||||
|
const ListActions = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopToolbar>
|
||||||
|
<Button label="Create" onClick={() => navigate("/server_actions/recurring/create")} startIcon={<AddIcon />} />
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecurringCommandsList = () => {
|
||||||
|
const { data, isLoading, error } = useRecurringCommands();
|
||||||
|
|
||||||
|
const listContext = useList({
|
||||||
|
resource: "recurring",
|
||||||
|
sort: { field: "scheduled_at", order: "DESC" },
|
||||||
|
perPage: 50,
|
||||||
|
data: data || [],
|
||||||
|
isLoading: isLoading,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <Loading />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResourceContextProvider value="recurring">
|
||||||
|
<ListContextProvider value={listContext}>
|
||||||
|
<ListActions />
|
||||||
|
<Paper>
|
||||||
|
<Datagrid
|
||||||
|
bulkActionButtons={false}
|
||||||
|
rowClick={(id: Identifier, resource: string, record: any) => {
|
||||||
|
if (!record) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/server_actions/${resource}/${id}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField source="command" />
|
||||||
|
<TextField source="args" label="Arguments" />
|
||||||
|
<TextField source="time" label="Time (UTC)" />
|
||||||
|
<DateField options={DATE_FORMAT} showTime source="scheduled_at" label="Next run at (local time)" />
|
||||||
|
</Datagrid>
|
||||||
|
</Paper>
|
||||||
|
</ListContextProvider>
|
||||||
|
</ResourceContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecurringCommandsList;
|
@ -0,0 +1,65 @@
|
|||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNotify, useDataProvider, useRecordContext } from "react-admin";
|
||||||
|
import { Button, Confirm } from "react-admin";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useAppContext } from "../../../../../Context";
|
||||||
|
import { RecurringCommand } from "../../../../../synapse/dataProvider";
|
||||||
|
|
||||||
|
const RecurringDeleteButton = () => {
|
||||||
|
const record = useRecordContext() as RecurringCommand;
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const notify = useNotify();
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await dataProvider.deleteRecurringCommand(etkeccAdmin, record.id);
|
||||||
|
notify("recurring_commands.action.delete_success", { type: "success" });
|
||||||
|
navigate("/server_actions");
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
|
notify(`Error: ${errorMessage}`, { type: "error" });
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
sx={{ color: theme.palette.error.main }}
|
||||||
|
label="Delete"
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={isDeleting}
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
/>
|
||||||
|
<Confirm
|
||||||
|
isOpen={open}
|
||||||
|
title="Delete Recurring Command"
|
||||||
|
content={`Are you sure you want to delete the command: ${record?.command || ""}?`}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleCancel}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecurringDeleteButton;
|
@ -0,0 +1,141 @@
|
|||||||
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
|
import { Card, CardContent, CardHeader, Box } from "@mui/material";
|
||||||
|
import { Typography, Link } from "@mui/material";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
TextInput,
|
||||||
|
DateTimeInput,
|
||||||
|
SaveButton,
|
||||||
|
useNotify,
|
||||||
|
useDataProvider,
|
||||||
|
Loading,
|
||||||
|
Button,
|
||||||
|
BooleanInput,
|
||||||
|
SelectInput,
|
||||||
|
} from "react-admin";
|
||||||
|
import { useWatch } from "react-hook-form";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import ScheduleDeleteButton from "./ScheduledDeleteButton";
|
||||||
|
import { useAppContext } from "../../../../../Context";
|
||||||
|
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
||||||
|
import { useServerCommands } from "../../../hooks/useServerCommands";
|
||||||
|
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||||
|
|
||||||
|
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
||||||
|
return Object.entries(commands).map(([key, value]) => ({
|
||||||
|
id: key,
|
||||||
|
name: value.name,
|
||||||
|
description: value.description,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArgumentsField = ({ serverCommands }) => {
|
||||||
|
const selectedCommand = useWatch({ name: "command" });
|
||||||
|
const showArgs = selectedCommand && serverCommands[selectedCommand]?.args === true;
|
||||||
|
|
||||||
|
if (!showArgs) return null;
|
||||||
|
|
||||||
|
return <TextInput required source="args" label="Arguments" fullWidth multiline />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScheduledCommandEdit = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const notify = useNotify();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const [command, setCommand] = useState<ScheduledCommand | null>(null);
|
||||||
|
const isCreating = typeof id === "undefined";
|
||||||
|
const [loading, setLoading] = useState(!isCreating);
|
||||||
|
const { data: scheduledCommands, isLoading: isLoadingList } = useScheduledCommands();
|
||||||
|
const { serverCommands, isLoading: isLoadingServerCommands } = useServerCommands();
|
||||||
|
const pageTitle = isCreating ? "Create Scheduled Command" : "Edit Scheduled Command";
|
||||||
|
|
||||||
|
const commandChoices = transformCommandsToChoices(serverCommands);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCreating && scheduledCommands) {
|
||||||
|
const commandToEdit = scheduledCommands.find(cmd => cmd.id === id);
|
||||||
|
if (commandToEdit) {
|
||||||
|
setCommand(commandToEdit);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [id, scheduledCommands, isCreating]);
|
||||||
|
|
||||||
|
const handleSubmit = async data => {
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
data.scheduled_at = new Date(data.scheduled_at).toISOString();
|
||||||
|
|
||||||
|
if (isCreating) {
|
||||||
|
result = await dataProvider.createScheduledCommand(etkeccAdmin, data);
|
||||||
|
notify("scheduled_commands.action.create_success", { type: "success" });
|
||||||
|
} else {
|
||||||
|
result = await dataProvider.updateScheduledCommand(etkeccAdmin, {
|
||||||
|
...data,
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
notify("scheduled_commands.action.update_success", { type: "success" });
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate("/server_actions");
|
||||||
|
} catch (error) {
|
||||||
|
notify("scheduled_commands.action.update_failure", { type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading || isLoadingList) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Button label="Back" onClick={() => navigate("/server_actions")} startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader title={pageTitle} />
|
||||||
|
{command && (
|
||||||
|
<Typography variant="body1" sx={{ px: 2 }}>
|
||||||
|
You can find more details about the command{" "}
|
||||||
|
<Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<CardContent>
|
||||||
|
<Form
|
||||||
|
defaultValues={command || undefined}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
record={command || undefined}
|
||||||
|
warnWhenUnsavedChanges
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
{command && <TextInput readOnly source="id" label="ID" fullWidth required />}
|
||||||
|
<SelectInput
|
||||||
|
readOnly={!isCreating}
|
||||||
|
source="command"
|
||||||
|
choices={commandChoices}
|
||||||
|
label="Command"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<ArgumentsField serverCommands={serverCommands} />
|
||||||
|
<DateTimeInput source="scheduled_at" label="Scheduled at" fullWidth required />
|
||||||
|
<Box mt={2} display="flex" justifyContent="space-between">
|
||||||
|
<SaveButton label={isCreating ? "Create" : "Update"} />
|
||||||
|
{!isCreating && <ScheduleDeleteButton />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduledCommandEdit;
|
@ -0,0 +1,89 @@
|
|||||||
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
|
import { Alert, Box, Card, CardContent, CardHeader, Typography, Link } from "@mui/material";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Loading,
|
||||||
|
Button,
|
||||||
|
useDataProvider,
|
||||||
|
useNotify,
|
||||||
|
SimpleShowLayout,
|
||||||
|
TextField,
|
||||||
|
BooleanField,
|
||||||
|
DateField,
|
||||||
|
RecordContextProvider,
|
||||||
|
} from "react-admin";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import ScheduledDeleteButton from "./ScheduledDeleteButton";
|
||||||
|
import { useAppContext } from "../../../../../Context";
|
||||||
|
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
||||||
|
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||||
|
|
||||||
|
const ScheduledCommandShow = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [command, setCommand] = useState<ScheduledCommand | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { data: scheduledCommands, isLoading: isLoadingList } = useScheduledCommands();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scheduledCommands) {
|
||||||
|
const commandToShow = scheduledCommands.find(cmd => cmd.id === id);
|
||||||
|
if (commandToShow) {
|
||||||
|
setCommand(commandToShow);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [id, scheduledCommands]);
|
||||||
|
|
||||||
|
if (loading || isLoadingList) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Button label="Back" onClick={() => navigate("/server_actions")} startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
<RecordContextProvider value={command}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title="Scheduled Command Details" />
|
||||||
|
<CardContent>
|
||||||
|
{command && (
|
||||||
|
<Alert severity="info">
|
||||||
|
<Typography variant="body1" sx={{ px: 2 }}>
|
||||||
|
You can find more details about the command{" "}
|
||||||
|
<Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<SimpleShowLayout>
|
||||||
|
<TextField source="id" label="ID" />
|
||||||
|
<TextField source="command" label="Command" />
|
||||||
|
{command.args && <TextField source="args" label="Arguments" />}
|
||||||
|
<BooleanField source="is_recurring" label="Is recurring" />
|
||||||
|
<DateField source="scheduled_at" label="Scheduled at" showTime />
|
||||||
|
</SimpleShowLayout>
|
||||||
|
{command.is_recurring && (
|
||||||
|
<Alert severity="warning">
|
||||||
|
Scheduled commands created from a recurring one are not editable as they will be regenerated
|
||||||
|
automatically. Please edit the recurring command instead.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Box display="flex" justifyContent="flex-end" mt={2}>
|
||||||
|
<ScheduledDeleteButton />
|
||||||
|
</Box>
|
||||||
|
</RecordContextProvider>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduledCommandShow;
|
@ -0,0 +1,72 @@
|
|||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { Paper } from "@mui/material";
|
||||||
|
import { Loading, Button, useNotify, useRefresh, useCreatePath, useRecordContext } from "react-admin";
|
||||||
|
import { ResourceContextProvider, useList } from "react-admin";
|
||||||
|
import { ListContextProvider, TextField } from "react-admin";
|
||||||
|
import { Datagrid } from "react-admin";
|
||||||
|
import { BooleanField, DateField, TopToolbar } from "react-admin";
|
||||||
|
import { useDataProvider } from "react-admin";
|
||||||
|
import { Identifier } from "react-admin";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useAppContext } from "../../../../../Context";
|
||||||
|
import { DATE_FORMAT } from "../../../../../utils/date";
|
||||||
|
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||||
|
const ListActions = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
navigate("/server_actions/scheduled/create");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopToolbar>
|
||||||
|
<Button label="Create" onClick={handleCreate} startIcon={<AddIcon />} />
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScheduledCommandsList = () => {
|
||||||
|
const { data, isLoading, error } = useScheduledCommands();
|
||||||
|
|
||||||
|
const listContext = useList({
|
||||||
|
resource: "scheduled",
|
||||||
|
sort: { field: "scheduled_at", order: "DESC" },
|
||||||
|
perPage: 50,
|
||||||
|
data: data || [],
|
||||||
|
isLoading: isLoading,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <Loading />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResourceContextProvider value="scheduled">
|
||||||
|
<ListContextProvider value={listContext}>
|
||||||
|
<ListActions />
|
||||||
|
<Paper>
|
||||||
|
<Datagrid
|
||||||
|
bulkActionButtons={false}
|
||||||
|
rowClick={(id: Identifier, resource: string, record: any) => {
|
||||||
|
if (!record) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.is_recurring) {
|
||||||
|
return `/server_actions/${resource}/${id}/show`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/server_actions/${resource}/${id}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField source="command" />
|
||||||
|
<TextField source="args" label="Arguments" />
|
||||||
|
<BooleanField source="is_recurring" label="Is recurring?" />
|
||||||
|
<DateField options={DATE_FORMAT} showTime source="scheduled_at" label="Run at (local time)" />
|
||||||
|
</Datagrid>
|
||||||
|
</Paper>
|
||||||
|
</ListContextProvider>
|
||||||
|
</ResourceContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduledCommandsList;
|
@ -0,0 +1,65 @@
|
|||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNotify, useDataProvider, useRecordContext } from "react-admin";
|
||||||
|
import { Button, Confirm } from "react-admin";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useAppContext } from "../../../../../Context";
|
||||||
|
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
||||||
|
|
||||||
|
const ScheduledDeleteButton = () => {
|
||||||
|
const record = useRecordContext() as ScheduledCommand;
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const notify = useNotify();
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await dataProvider.deleteScheduledCommand(etkeccAdmin, record.id);
|
||||||
|
notify("scheduled_commands.action.delete_success", { type: "success" });
|
||||||
|
navigate("/server_actions");
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
|
notify(`Error: ${errorMessage}`, { type: "error" });
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
sx={{ color: theme.palette.error.main }}
|
||||||
|
label="Delete"
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={isDeleting}
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
/>
|
||||||
|
<Confirm
|
||||||
|
isOpen={open}
|
||||||
|
title="Delete Scheduled Command"
|
||||||
|
content={`Are you sure you want to delete the command: ${record?.command || ""}?`}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleCancel}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduledDeleteButton;
|
@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useDataProvider } from "react-admin";
|
||||||
|
|
||||||
|
import { useAppContext } from "../../../../Context";
|
||||||
|
|
||||||
|
export const useRecurringCommands = () => {
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: ["recurringCommands"],
|
||||||
|
queryFn: () => dataProvider.getRecurringCommands(etkeccAdmin),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, isLoading, error };
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useDataProvider } from "react-admin";
|
||||||
|
|
||||||
|
import { useAppContext } from "../../../../Context";
|
||||||
|
|
||||||
|
export const useScheduledCommands = () => {
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: ["scheduledCommands"],
|
||||||
|
queryFn: () => dataProvider.getScheduledCommands(etkeccAdmin),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, isLoading, error };
|
||||||
|
};
|
@ -482,5 +482,23 @@ const en: SynapseTranslationMessages = {
|
|||||||
helper: { length: "Length of the token if no token is given." },
|
helper: { length: "Length of the token if no token is given." },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
scheduled_commands: {
|
||||||
|
action: {
|
||||||
|
create_success: "Scheduled command created successfully",
|
||||||
|
update_success: "Scheduled command updated successfully",
|
||||||
|
update_failure: "An error has occurred",
|
||||||
|
delete_success: "Scheduled command deleted successfully",
|
||||||
|
delete_failure: "An error has occurred",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recurring_commands: {
|
||||||
|
action: {
|
||||||
|
create_success: "Recurring command created successfully",
|
||||||
|
update_success: "Recurring command updated successfully",
|
||||||
|
update_failure: "An error has occurred",
|
||||||
|
delete_success: "Recurring command deleted successfully",
|
||||||
|
delete_failure: "An error has occurred",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default en;
|
export default en;
|
||||||
|
18
src/i18n/index.d.ts
vendored
18
src/i18n/index.d.ts
vendored
@ -473,4 +473,22 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
scheduled_commands?: {
|
||||||
|
action: {
|
||||||
|
create_success: string;
|
||||||
|
update_success: string;
|
||||||
|
update_failure: string;
|
||||||
|
delete_success: string;
|
||||||
|
delete_failure: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
recurring_commands?: {
|
||||||
|
action: {
|
||||||
|
create_success: string;
|
||||||
|
update_success: string;
|
||||||
|
update_failure: string;
|
||||||
|
delete_success: string;
|
||||||
|
delete_failure: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,14 @@ describe("LoginForm", () => {
|
|||||||
it("renders with single restricted homeserver", () => {
|
it("renders with single restricted homeserver", () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppContext.Provider value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}>
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
restrictBaseUrl: "https://matrix.example.com",
|
||||||
|
asManagedUsers: [],
|
||||||
|
menu: [],
|
||||||
|
corsCredentials: "include",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AdminContext>
|
</AdminContext>
|
||||||
@ -62,6 +69,7 @@ describe("LoginForm", () => {
|
|||||||
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
||||||
asManagedUsers: [],
|
asManagedUsers: [],
|
||||||
menu: [],
|
menu: [],
|
||||||
|
corsCredentials: "include",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
|
@ -87,6 +87,7 @@ describe("authProvider", () => {
|
|||||||
expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/v3/logout", {
|
expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/v3/logout", {
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer foo",
|
Authorization: "Bearer foo",
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -23,7 +23,11 @@ const authProvider: AuthProvider = {
|
|||||||
console.log("login ");
|
console.log("login ");
|
||||||
let options: Options = {
|
let options: Options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: GetConfig().corsCredentials,
|
credentials: GetConfig().corsCredentials as RequestCredentials,
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
@ -150,7 +154,11 @@ const authProvider: AuthProvider = {
|
|||||||
|
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: GetConfig().corsCredentials,
|
credentials: GetConfig().corsCredentials as RequestCredentials,
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
user: {
|
user: {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
token: `Bearer ${access_token}`,
|
token: `Bearer ${access_token}`,
|
||||||
|
@ -23,7 +23,7 @@ const CACHED_MANY_REF: Record<string, any> = {};
|
|||||||
const jsonClient = async (url: string, options: Options = {}) => {
|
const jsonClient = async (url: string, options: Options = {}) => {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
console.log("httpClient " + url);
|
console.log("httpClient " + url);
|
||||||
options.credentials = GetConfig().corsCredentials;
|
options.credentials = GetConfig().corsCredentials as RequestCredentials;
|
||||||
if (token !== null) {
|
if (token !== null) {
|
||||||
options.user = {
|
options.user = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@ -322,6 +322,22 @@ export interface ServerCommand {
|
|||||||
|
|
||||||
export type ServerCommandsResponse = Record<string, ServerCommand>;
|
export type ServerCommandsResponse = Record<string, ServerCommand>;
|
||||||
|
|
||||||
|
export interface ScheduledCommand {
|
||||||
|
args: string;
|
||||||
|
command: string;
|
||||||
|
id: string;
|
||||||
|
is_recurring: boolean;
|
||||||
|
scheduled_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecurringCommand {
|
||||||
|
args: string;
|
||||||
|
command: string;
|
||||||
|
id: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
@ -337,6 +353,14 @@ export interface SynapseDataProvider extends DataProvider {
|
|||||||
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
||||||
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
|
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
|
||||||
getServerCommands: (etkeAdminUrl: string) => Promise<ServerCommandsResponse>;
|
getServerCommands: (etkeAdminUrl: string) => Promise<ServerCommandsResponse>;
|
||||||
|
getScheduledCommands: (etkeAdminUrl: string) => Promise<ScheduledCommand[]>;
|
||||||
|
getRecurringCommands: (etkeAdminUrl: string) => Promise<RecurringCommand[]>;
|
||||||
|
createScheduledCommand: (etkeAdminUrl: string, command: Partial<ScheduledCommand>) => Promise<ScheduledCommand>;
|
||||||
|
updateScheduledCommand: (etkeAdminUrl: string, command: ScheduledCommand) => Promise<ScheduledCommand>;
|
||||||
|
deleteScheduledCommand: (etkeAdminUrl: string, id: string) => Promise<{ success: boolean }>;
|
||||||
|
createRecurringCommand: (etkeAdminUrl: string, command: Partial<RecurringCommand>) => Promise<RecurringCommand>;
|
||||||
|
updateRecurringCommand: (etkeAdminUrl: string, command: RecurringCommand) => Promise<RecurringCommand>;
|
||||||
|
deleteRecurringCommand: (etkeAdminUrl: string, id: string) => Promise<{ success: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
@ -1188,6 +1212,212 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
success: false,
|
success: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getScheduledCommands: async (scheduledCommandsUrl: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${scheduledCommandsUrl}/schedules`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error fetching scheduled commands: ${response.status} ${response.statusText}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
return json as ScheduledCommand[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching scheduled commands, error");
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
getRecurringCommands: async (recurringCommandsUrl: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${recurringCommandsUrl}/recurrings`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error fetching recurring commands: ${response.status} ${response.statusText}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
return json as RecurringCommand[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching recurring commands, error");
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
createScheduledCommand: async (scheduledCommandsUrl: string, command: Partial<ScheduledCommand>) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${scheduledCommandsUrl}/schedules`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(command),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error creating scheduled command: ${response.status} ${response.statusText}`);
|
||||||
|
throw new Error("Failed to create scheduled command");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return command as ScheduledCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return json as ScheduledCommand;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating scheduled command", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateScheduledCommand: async (scheduledCommandsUrl: string, command: ScheduledCommand) => {
|
||||||
|
try {
|
||||||
|
// Use the base endpoint without ID and use PUT for upsert
|
||||||
|
const response = await fetch(`${scheduledCommandsUrl}/schedules`, {
|
||||||
|
method: "PUT", // Using PUT on the base endpoint
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(command),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const jsonErr = JSON.parse(await response.text());
|
||||||
|
console.error(`Error updating scheduled command: ${response.status} ${response.statusText}`);
|
||||||
|
throw new Error(jsonErr.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// According to docs, successful response is 204 No Content
|
||||||
|
if (response.status === 204) {
|
||||||
|
// Return the command object we sent since the server doesn't return data
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If server does return data (though docs suggest it returns 204)
|
||||||
|
const json = await response.json();
|
||||||
|
console.log("JSON", json);
|
||||||
|
return json as ScheduledCommand;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating scheduled command", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteScheduledCommand: async (scheduledCommandsUrl: string, id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${scheduledCommandsUrl}/schedules/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error deleting scheduled command: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting scheduled command", error);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createRecurringCommand: async (recurringCommandsUrl: string, command: Partial<RecurringCommand>) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${recurringCommandsUrl}/recurrings`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(command),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error creating recurring command: ${response.status} ${response.statusText}`);
|
||||||
|
throw new Error("Failed to create recurring command");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
// Return the command object we sent since the server doesn't return data
|
||||||
|
return command as RecurringCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return json as RecurringCommand;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating recurring command", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateRecurringCommand: async (recurringCommandsUrl: string, command: RecurringCommand) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${recurringCommandsUrl}/recurrings`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(command),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error updating recurring command: ${response.status} ${response.statusText}`);
|
||||||
|
throw new Error("Failed to update recurring command");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
// Return the command object we sent since the server doesn't return data
|
||||||
|
return command as RecurringCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return json as RecurringCommand;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating recurring command", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteRecurringCommand: async (recurringCommandsUrl: string, id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${recurringCommandsUrl}/recurrings/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error deleting recurring command: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting recurring command", error);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user