Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Finish recurring commands add/edit
  • Loading branch information
beastafk committed Apr 4, 2025
commit cd1a576b46b1fc072c09e112087b849162d4381e
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import ScheduledCommandEdit from "./components/etke.cc/schedules/components/sche
import ScheduledCommandShow from "./components/etke.cc/schedules/components/scheduled/ScheduledCommandShow";
import RecurringCommandEdit from "./components/etke.cc/schedules/components/recurring/RecurringCommandEdit";
import ServerSchedulesPage from "./components/etke.cc/schedules/components/ServerSchedulesPage";

// TODO: Can we use lazy loading together with browser locale?
const messages = {
de: germanMessages,
Expand Down Expand Up @@ -73,7 +74,8 @@ export const App = () => (
<Route path="/scheduled_commands/:id/show" element={<ScheduledCommandShow />} />
<Route path="/scheduled_commands/:id" element={<ScheduledCommandEdit />} />
<Route path="/scheduled_commands/create" element={<ScheduledCommandEdit />} />
<Route path="/recurring_command/:id" element={<RecurringCommandEdit />} />
<Route path="/recurring_commands/:id" element={<RecurringCommandEdit />} />
<Route path="/recurring_commands/create" element={<RecurringCommandEdit />} />
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
</CustomRoutes>
<Resource {...users} />
Expand Down
30 changes: 17 additions & 13 deletions src/components/etke.cc/schedules/components/ServerSchedulesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Typography, Link, Alert } from "@mui/material";
import { Box, Typography, Link, Alert, Divider } from "@mui/material";
import { Stack } from "@mui/material"
import ScheduleIcon from '@mui/icons-material/Schedule';
import RestoreIcon from '@mui/icons-material/Restore';
Expand All @@ -18,18 +18,22 @@ const ServerSchedulesPage = () => {
</Typography>
</Alert>
</Stack>
<Stack spacing={1} direction="column" alignItems="center">
<Box sx={{ width: "100%" }}>
<Typography variant="h5"><ScheduleIcon sx={{ verticalAlign: "middle" }} /> Scheduled commands:</Typography>
<ScheduledCommandsList />
</Box>
</Stack>
<Stack spacing={1} direction="column" alignItems="center">
<Box sx={{ width: "100%" }}>
<Typography sx={{ mb: 2 }} variant="h5"><RestoreIcon sx={{ verticalAlign: "middle" }} /> Recurring commands:</Typography>
<RecurringCommandsList />
</Box>
</Stack>

<Box sx={{ mt: 2 }}>
<Typography variant="h5">
<ScheduleIcon sx={{ verticalAlign: "middle", mr: 1 }} /> Scheduled commands:
</Typography>
<ScheduledCommandsList />
</Box>

<Divider sx={{ my: 4, borderWidth: 2 }} />

<Box sx={{ mt: 2 }}>
<Typography variant="h5">
<RestoreIcon sx={{ verticalAlign: "middle", mr: 1 }} /> Recurring commands:
</Typography>
<RecurringCommandsList />
</Box>
</Stack>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import {
useNavigate,
} from "react-router-dom";
import {
SimpleForm,
Form,
TextInput,
DateTimeInput,
SaveButton,
useNotify,
useDataProvider,
Loading,
Button,
SelectInput,
TimeInput,
} from "react-admin";
import { useQueryClient } from "@tanstack/react-query";
import {
Card,
CardContent,
Expand All @@ -22,87 +24,156 @@ import {
import { useAppContext } from "../../../../../Context";
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useServerCommands } from "../../../hooks/useServerCommands";
import { useWatch } from "react-hook-form";
import RecurringDeleteButton from "./RecurringDeleteButton";
import { RecurringCommand } from "../../../../../synapse/dataProvider";

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({
command: "",
args: "",
time: "00:00",
scheduled_at: new Date().toISOString(),
});
const [loading, setLoading] = useState(id !== "create");
const [command, setCommand] = useState<RecurringCommand | undefined>(undefined);
const isCreating = typeof id === "undefined";
const [loading, setLoading] = useState(!isCreating);
const { data: recurringCommands, isLoading: isLoadingList } = useRecurringCommands();
const isCreating = id === "create";
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) {
setCommand(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, data);
notify("Recurring command created successfully", { type: "success" });
result = await dataProvider.createRecurringCommand(etkeccAdmin, submissionData);
notify("recurring_commands.action.create_success", { type: "success" });
} else {
result = await dataProvider.updateRecurringCommand(etkeccAdmin, {
...data,
...submissionData,
id: id,
});
notify("Recurring command updated successfully", { type: "success" });
notify("recurring_commands.action.update_success", { type: "success" });
}

navigate("/scheduler");
// Invalidate scheduled commands queries
queryClient.invalidateQueries({ queryKey: ["scheduledCommands"] });

navigate("/server_schedules");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
notify(`Error: ${errorMessage}`, { type: "error" });
notify("recurring_commands.action.update_failure", { type: "error" });
}
};

if (loading || isLoadingList) {
if (loading || isLoadingList || isLoadingServerCommands) {
return <Loading />;
}

return (
<Box sx={{ mt: 2 }}>
<Button
label="Back"
onClick={() => navigate("/scheduler")}
onClick={() => navigate("/server_schedules")}
startIcon={<ArrowBackIcon />}
sx={{ mb: 2 }}
/>

<Card>
<CardHeader title={pageTitle} />
<CardContent>
<SimpleForm
onSubmit={handleSubmit}
record={command}
warnWhenUnsavedChanges
>
<Form defaultValues={command || undefined} onSubmit={handleSubmit} record={command || undefined} warnWhenUnsavedChanges>
<Box display="flex" flexDirection="column" gap={2}>
<TextInput source="command" label="Command" fullWidth required />
<TextInput source="args" label="Arguments" fullWidth multiline />
<TextInput source="time" label="Time (HH:MM in UTC)" fullWidth required />
<DateTimeInput source="scheduled_at" label="Initial execution date and time" fullWidth required />
<Box mt={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"
fullWidth
required
/>
<Box mt={2} display="flex" justifyContent="space-between">
<SaveButton label={isCreating ? "Create" : "Update"} />
{!isCreating && <RecurringDeleteButton />}
</Box>
</Box>
</SimpleForm>
</Form>
</CardContent>
</Card>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
import { Loading, Button, useNotify, useRefresh, useCreatePath } from "react-admin";
import { Loading, Button } from "react-admin";
import { DateField } from "react-admin";
import { Datagrid } from "react-admin";
import { Box, Paper } from "@mui/material";
import { ListContextProvider, TextField, DeleteButton, TopToolbar } from "react-admin";
import { Paper } from "@mui/material";
import { ListContextProvider, TextField, TopToolbar } from "react-admin";
import { ResourceContextProvider, useList } from "react-admin";
import { DATE_FORMAT } from "../../../../../utils/date";
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
import { useAppContext } from "../../../../../Context";
import { useDataProvider } from "react-admin";
import AddIcon from "@mui/icons-material/Add";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";

const ListActions = () => {
const createPath = useCreatePath();
const navigate = useNavigate();

return (
<TopToolbar>
<Button
label="Create"
component={Link}
to={createPath({
type: "create",
resource: "recurring-command",
})}
onClick={() => navigate("/recurring_commands/create")}
startIcon={<AddIcon />}
/>
</TopToolbar>
Expand All @@ -31,45 +25,26 @@ const ListActions = () => {

const RecurringCommandsList = () => {
const { data, isLoading, error } = useRecurringCommands();
const { etkeccAdmin } = useAppContext();
const dataProvider = useDataProvider();
const notify = useNotify();
const refresh = useRefresh();

const listContext = useList({
resource: "recurring_commands",
sort: { field: "scheduled_at", order: "DESC" },
perPage: 10,
data: data || [], // Provide the data from your custom hook
perPage: 50,
data: data || [],
isLoading: isLoading,
});

if (isLoading) return <Loading />;

const handleDelete = async (id) => {
try {
await dataProvider.deleteRecurringCommand(etkeccAdmin, id);
notify("Recurring command deleted successfully", { type: "success" });
refresh();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
notify(`Error: ${errorMessage}`, { type: "error" });
}
};

return (<ResourceContextProvider value="recurring_commands">
<ListContextProvider value={listContext}>
<ListActions />
<Paper>
<Datagrid bulkActionButtons={false} rowClick="show">
<Datagrid bulkActionButtons={false} rowClick="edit">
<TextField source="command" />
<TextField source="args" />
<TextField source="time" label="Time (UTC)" />
<DateField options={DATE_FORMAT} showTime source="scheduled_at" label="Scheduled at (local time)" />
<DeleteButton resource="recurring_commands" onClick={(e) => {
e.stopPropagation();
handleDelete(e.currentTarget.dataset.id);
}} />
</Datagrid>
</Paper>
</ListContextProvider>
Expand Down
Loading