mirror of
https://github.com/fccview/cronmaster.git
synced 2025-12-23 22:18:20 -05:00
huge translations work
This commit is contained in:
@@ -2,42 +2,16 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/GlobalComponents/Cards/Card";
|
||||
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
||||
import {
|
||||
Trash2,
|
||||
Clock,
|
||||
Edit,
|
||||
Plus,
|
||||
Files,
|
||||
User,
|
||||
Play,
|
||||
Pause,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { Clock, Plus } from "lucide-react";
|
||||
import { CronJob } from "@/app/_utils/cronjob-utils";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { CreateTaskModal } from "@/app/_components/FeatureComponents/Modals/CreateTaskModal";
|
||||
import { EditTaskModal } from "@/app/_components/FeatureComponents/Modals/EditTaskModal";
|
||||
import { DeleteTaskModal } from "@/app/_components/FeatureComponents/Modals/DeleteTaskModal";
|
||||
import { CloneTaskModal } from "@/app/_components/FeatureComponents/Modals/CloneTaskModal";
|
||||
import { UserFilter } from "@/app/_components/FeatureComponents/User/UserFilter";
|
||||
import { ErrorBadge } from "@/app/_components/GlobalComponents/Badges/ErrorBadge";
|
||||
import { ErrorDetailsModal } from "@/app/_components/FeatureComponents/Modals/ErrorDetailsModal";
|
||||
import { Script } from "@/app/_utils/scripts-utils";
|
||||
import { UserFilter } from "@/app/_components/FeatureComponents/User/UserFilter";
|
||||
|
||||
import {
|
||||
getJobErrorsByJobId,
|
||||
JobError,
|
||||
} from "@/app/_utils/error-utils";
|
||||
import {
|
||||
handleErrorClick,
|
||||
handleDelete,
|
||||
handleClone,
|
||||
handlePause,
|
||||
handleResume,
|
||||
handleRun,
|
||||
handleEditSubmit,
|
||||
handleNewCronSubmit,
|
||||
} from "@/app/_components/FeatureComponents/Cronjobs/helpers";
|
||||
import { useCronJobState } from "@/app/_hooks/useCronJobState";
|
||||
import { CronJobItem } from "@/app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem";
|
||||
import { CronJobEmptyState } from "@/app/_components/FeatureComponents/Cronjobs/Parts/CronJobEmptyState";
|
||||
import { CronJobListModals } from "@/app/_components/FeatureComponents/Modals/CronJobListsModals";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface CronJobListProps {
|
||||
cronJobs: CronJob[];
|
||||
@@ -45,206 +19,46 @@ interface CronJobListProps {
|
||||
}
|
||||
|
||||
export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [editingJob, setEditingJob] = useState<CronJob | null>(null);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isNewCronModalOpen, setIsNewCronModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
|
||||
const [jobToDelete, setJobToDelete] = useState<CronJob | null>(null);
|
||||
const [jobToClone, setJobToClone] = useState<CronJob | null>(null);
|
||||
const [isCloning, setIsCloning] = useState(false);
|
||||
const [runningJobId, setRunningJobId] = useState<string | null>(null);
|
||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||
const [jobErrors, setJobErrors] = useState<Record<string, JobError[]>>({});
|
||||
const [errorModalOpen, setErrorModalOpen] = useState(false);
|
||||
const [selectedError, setSelectedError] = useState<JobError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const savedUser = localStorage.getItem("selectedCronUser");
|
||||
if (savedUser) {
|
||||
setSelectedUser(savedUser);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUser) {
|
||||
localStorage.setItem("selectedCronUser", selectedUser);
|
||||
} else {
|
||||
localStorage.removeItem("selectedCronUser");
|
||||
}
|
||||
}, [selectedUser]);
|
||||
|
||||
const [editForm, setEditForm] = useState({
|
||||
schedule: "",
|
||||
command: "",
|
||||
comment: "",
|
||||
});
|
||||
const [newCronForm, setNewCronForm] = useState({
|
||||
schedule: "",
|
||||
command: "",
|
||||
comment: "",
|
||||
selectedScriptId: null as string | null,
|
||||
user: "",
|
||||
});
|
||||
|
||||
const filteredJobs = useMemo(() => {
|
||||
if (!selectedUser) return cronJobs;
|
||||
return cronJobs.filter((job) => job.user === selectedUser);
|
||||
}, [cronJobs, selectedUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors: Record<string, JobError[]> = {};
|
||||
filteredJobs.forEach((job) => {
|
||||
errors[job.id] = getJobErrorsByJobId(job.id);
|
||||
});
|
||||
setJobErrors(errors);
|
||||
}, [filteredJobs]);
|
||||
|
||||
const handleErrorClickLocal = (error: JobError) => {
|
||||
handleErrorClick(error, setSelectedError, setErrorModalOpen);
|
||||
};
|
||||
|
||||
const refreshJobErrorsLocal = () => {
|
||||
const errors: Record<string, JobError[]> = {};
|
||||
filteredJobs.forEach((job) => {
|
||||
errors[job.id] = getJobErrorsByJobId(job.id);
|
||||
});
|
||||
setJobErrors(errors);
|
||||
};
|
||||
|
||||
const handleDeleteLocal = async (id: string) => {
|
||||
await handleDelete(id, {
|
||||
setDeletingId,
|
||||
setIsDeleteModalOpen,
|
||||
setJobToDelete,
|
||||
setIsCloneModalOpen,
|
||||
setJobToClone,
|
||||
setIsCloning,
|
||||
setIsEditModalOpen,
|
||||
setEditingJob,
|
||||
setIsNewCronModalOpen,
|
||||
setNewCronForm,
|
||||
setRunningJobId,
|
||||
refreshJobErrors: refreshJobErrorsLocal,
|
||||
jobToClone,
|
||||
editingJob,
|
||||
editForm,
|
||||
newCronForm,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloneLocal = async (newComment: string) => {
|
||||
await handleClone(newComment, {
|
||||
setDeletingId,
|
||||
setIsDeleteModalOpen,
|
||||
setJobToDelete,
|
||||
setIsCloneModalOpen,
|
||||
setJobToClone,
|
||||
setIsCloning,
|
||||
setIsEditModalOpen,
|
||||
setEditingJob,
|
||||
setIsNewCronModalOpen,
|
||||
setNewCronForm,
|
||||
setRunningJobId,
|
||||
refreshJobErrors: refreshJobErrorsLocal,
|
||||
jobToClone,
|
||||
editingJob,
|
||||
editForm,
|
||||
newCronForm,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePauseLocal = async (id: string) => {
|
||||
await handlePause(id);
|
||||
};
|
||||
|
||||
const handleResumeLocal = async (id: string) => {
|
||||
await handleResume(id);
|
||||
};
|
||||
|
||||
const handleRunLocal = async (id: string) => {
|
||||
await handleRun(id, {
|
||||
setDeletingId,
|
||||
setIsDeleteModalOpen,
|
||||
setJobToDelete,
|
||||
setIsCloneModalOpen,
|
||||
setJobToClone,
|
||||
setIsCloning,
|
||||
setIsEditModalOpen,
|
||||
setEditingJob,
|
||||
setIsNewCronModalOpen,
|
||||
setNewCronForm,
|
||||
setRunningJobId,
|
||||
refreshJobErrors: refreshJobErrorsLocal,
|
||||
jobToClone,
|
||||
editingJob,
|
||||
editForm,
|
||||
newCronForm,
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDelete = (job: CronJob) => {
|
||||
setJobToDelete(job);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const confirmClone = (job: CronJob) => {
|
||||
setJobToClone(job);
|
||||
setIsCloneModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (job: CronJob) => {
|
||||
setEditingJob(job);
|
||||
setEditForm({
|
||||
schedule: job.schedule,
|
||||
command: job.command,
|
||||
comment: job.comment || "",
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditSubmitLocal = async (e: React.FormEvent) => {
|
||||
await handleEditSubmit(e, {
|
||||
setDeletingId,
|
||||
setIsDeleteModalOpen,
|
||||
setJobToDelete,
|
||||
setIsCloneModalOpen,
|
||||
setJobToClone,
|
||||
setIsCloning,
|
||||
setIsEditModalOpen,
|
||||
setEditingJob,
|
||||
setIsNewCronModalOpen,
|
||||
setNewCronForm,
|
||||
setRunningJobId,
|
||||
refreshJobErrors: refreshJobErrorsLocal,
|
||||
jobToClone,
|
||||
editingJob,
|
||||
editForm,
|
||||
newCronForm,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNewCronSubmitLocal = async (e: React.FormEvent) => {
|
||||
await handleNewCronSubmit(e, {
|
||||
setDeletingId,
|
||||
setIsDeleteModalOpen,
|
||||
setJobToDelete,
|
||||
setIsCloneModalOpen,
|
||||
setJobToClone,
|
||||
setIsCloning,
|
||||
setIsEditModalOpen,
|
||||
setEditingJob,
|
||||
setIsNewCronModalOpen,
|
||||
setNewCronForm,
|
||||
setRunningJobId,
|
||||
refreshJobErrors: refreshJobErrorsLocal,
|
||||
jobToClone,
|
||||
editingJob,
|
||||
editForm,
|
||||
newCronForm,
|
||||
});
|
||||
};
|
||||
const t = useTranslations();
|
||||
const {
|
||||
deletingId,
|
||||
runningJobId,
|
||||
selectedUser,
|
||||
setSelectedUser,
|
||||
jobErrors,
|
||||
errorModalOpen,
|
||||
setErrorModalOpen,
|
||||
selectedError,
|
||||
setSelectedError,
|
||||
filteredJobs,
|
||||
isNewCronModalOpen,
|
||||
setIsNewCronModalOpen,
|
||||
isEditModalOpen,
|
||||
setIsEditModalOpen,
|
||||
isDeleteModalOpen,
|
||||
setIsDeleteModalOpen,
|
||||
isCloneModalOpen,
|
||||
setIsCloneModalOpen,
|
||||
jobToDelete,
|
||||
jobToClone,
|
||||
isCloning,
|
||||
editForm,
|
||||
setEditForm,
|
||||
newCronForm,
|
||||
setNewCronForm,
|
||||
handleErrorClickLocal,
|
||||
refreshJobErrorsLocal,
|
||||
handleDeleteLocal,
|
||||
handleCloneLocal,
|
||||
handlePauseLocal,
|
||||
handleResumeLocal,
|
||||
handleRunLocal,
|
||||
confirmDelete,
|
||||
confirmClone,
|
||||
handleEdit,
|
||||
handleEditSubmitLocal,
|
||||
handleNewCronSubmitLocal,
|
||||
} = useCronJobState({ cronJobs, scripts });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -257,12 +71,12 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl brand-gradient">
|
||||
Scheduled Tasks
|
||||
{t("cronjobs.scheduledTasks")}
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{filteredJobs.length} of {cronJobs.length} scheduled job
|
||||
{filteredJobs.length !== 1 ? "s" : ""}
|
||||
{selectedUser && ` for ${selectedUser}`}
|
||||
{t("cronjobs.nOfNJObs", { filtered: filteredJobs.length, total: cronJobs.length })}
|
||||
{" "}
|
||||
{selectedUser && t("cronjobs.forUser", { user: selectedUser })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,7 +85,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
className="btn-primary glow-primary"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Task
|
||||
{t("cronjobs.newTask")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -285,219 +99,74 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
</div>
|
||||
|
||||
{filteredJobs.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-primary/20 to-blue-500/20 rounded-full flex items-center justify-center mb-6">
|
||||
<Clock className="h-10 w-10 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3 brand-gradient">
|
||||
{selectedUser
|
||||
? `No tasks for user ${selectedUser}`
|
||||
: "No scheduled tasks yet"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
||||
{selectedUser
|
||||
? `No scheduled tasks found for user ${selectedUser}. Try selecting a different user or create a new task.`
|
||||
: "Create your first scheduled task to automate your system operations and boost productivity."}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setIsNewCronModalOpen(true)}
|
||||
className="btn-primary glow-primary"
|
||||
size="lg"
|
||||
>
|
||||
<Plus className="h-5 w-5 mr-2" />
|
||||
Create Your First Task
|
||||
</Button>
|
||||
</div>
|
||||
<CronJobEmptyState
|
||||
selectedUser={selectedUser}
|
||||
onNewTaskClick={() => setIsNewCronModalOpen(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredJobs.map((job) => (
|
||||
<div
|
||||
<CronJobItem
|
||||
key={job.id}
|
||||
className="glass-card p-4 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
||||
<div className="flex-1 min-w-0 order-2 lg:order-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<code className="text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 px-2 py-1 rounded font-mono border border-purple-500/20">
|
||||
{job.schedule}
|
||||
</code>
|
||||
<div className="flex-1 min-w-0">
|
||||
<pre
|
||||
className="text-sm font-medium text-foreground truncate bg-muted/30 px-2 py-1 rounded border border-border/30"
|
||||
title={job.command}
|
||||
>
|
||||
{job.command}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{job.user}</span>
|
||||
</div>
|
||||
{job.paused && (
|
||||
<span className="text-xs bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/20">
|
||||
Paused
|
||||
</span>
|
||||
)}
|
||||
<ErrorBadge
|
||||
errors={jobErrors[job.id] || []}
|
||||
onErrorClick={handleErrorClickLocal}
|
||||
onErrorDismiss={refreshJobErrorsLocal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{job.comment && (
|
||||
<p
|
||||
className="text-xs text-muted-foreground italic truncate"
|
||||
title={job.comment}
|
||||
>
|
||||
{job.comment}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0 order-1 lg:order-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleRunLocal(job.id)}
|
||||
disabled={runningJobId === job.id || job.paused}
|
||||
className="btn-outline h-8 px-3"
|
||||
title="Run cron job manually"
|
||||
aria-label="Run cron job manually"
|
||||
>
|
||||
{runningJobId === job.id ? (
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<Code className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(job)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title="Edit cron job"
|
||||
aria-label="Edit cron job"
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => confirmClone(job)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title="Clone cron job"
|
||||
aria-label="Clone cron job"
|
||||
>
|
||||
<Files className="h-3 w-3" />
|
||||
</Button>
|
||||
{job.paused ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleResumeLocal(job.id)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title="Resume cron job"
|
||||
aria-label="Resume cron job"
|
||||
>
|
||||
<Play className="h-3 w-3" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePauseLocal(job.id)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title="Pause cron job"
|
||||
aria-label="Pause cron job"
|
||||
>
|
||||
<Pause className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => confirmDelete(job)}
|
||||
disabled={deletingId === job.id}
|
||||
className="btn-destructive h-8 px-3"
|
||||
title="Delete cron job"
|
||||
aria-label="Delete cron job"
|
||||
>
|
||||
{deletingId === job.id ? (
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<Trash2 className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
job={job}
|
||||
errors={jobErrors[job.id] || []}
|
||||
runningJobId={runningJobId}
|
||||
deletingId={deletingId}
|
||||
onRun={handleRunLocal}
|
||||
onEdit={handleEdit}
|
||||
onClone={confirmClone}
|
||||
onResume={handleResumeLocal}
|
||||
onPause={handlePauseLocal}
|
||||
onDelete={confirmDelete}
|
||||
onErrorClick={handleErrorClickLocal}
|
||||
onErrorDismiss={refreshJobErrorsLocal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<CreateTaskModal
|
||||
isOpen={isNewCronModalOpen}
|
||||
onClose={() => setIsNewCronModalOpen(false)}
|
||||
onSubmit={handleNewCronSubmitLocal}
|
||||
<CronJobListModals
|
||||
cronJobs={cronJobs}
|
||||
scripts={scripts}
|
||||
form={newCronForm}
|
||||
onFormChange={(updates) =>
|
||||
|
||||
isNewCronModalOpen={isNewCronModalOpen}
|
||||
onNewCronModalClose={() => setIsNewCronModalOpen(false)}
|
||||
onNewCronSubmit={handleNewCronSubmitLocal}
|
||||
newCronForm={newCronForm}
|
||||
onNewCronFormChange={(updates) =>
|
||||
setNewCronForm((prev) => ({ ...prev, ...updates }))
|
||||
}
|
||||
/>
|
||||
|
||||
<EditTaskModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={() => setIsEditModalOpen(false)}
|
||||
onSubmit={handleEditSubmitLocal}
|
||||
form={editForm}
|
||||
onFormChange={(updates) =>
|
||||
isEditModalOpen={isEditModalOpen}
|
||||
onEditModalClose={() => setIsEditModalOpen(false)}
|
||||
onEditSubmit={handleEditSubmitLocal}
|
||||
editForm={editForm}
|
||||
onEditFormChange={(updates) =>
|
||||
setEditForm((prev) => ({ ...prev, ...updates }))
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteTaskModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
onConfirm={() =>
|
||||
isDeleteModalOpen={isDeleteModalOpen}
|
||||
onDeleteModalClose={() => setIsDeleteModalOpen(false)}
|
||||
onDeleteConfirm={() =>
|
||||
jobToDelete ? handleDeleteLocal(jobToDelete.id) : undefined
|
||||
}
|
||||
job={jobToDelete}
|
||||
/>
|
||||
jobToDelete={jobToDelete}
|
||||
|
||||
<CloneTaskModal
|
||||
cronJob={jobToClone}
|
||||
isOpen={isCloneModalOpen}
|
||||
onClose={() => setIsCloneModalOpen(false)}
|
||||
onConfirm={handleCloneLocal}
|
||||
isCloneModalOpen={isCloneModalOpen}
|
||||
onCloneModalClose={() => setIsCloneModalOpen(false)}
|
||||
onCloneConfirm={handleCloneLocal}
|
||||
jobToClone={jobToClone}
|
||||
isCloning={isCloning}
|
||||
/>
|
||||
|
||||
{errorModalOpen && selectedError && (
|
||||
<ErrorDetailsModal
|
||||
isOpen={errorModalOpen}
|
||||
onClose={() => {
|
||||
setErrorModalOpen(false);
|
||||
setSelectedError(null);
|
||||
}}
|
||||
error={{
|
||||
title: selectedError.title,
|
||||
message: selectedError.message,
|
||||
details: selectedError.details,
|
||||
command: selectedError.command,
|
||||
output: selectedError.output,
|
||||
stderr: selectedError.stderr,
|
||||
timestamp: selectedError.timestamp,
|
||||
jobId: selectedError.jobId,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
isErrorModalOpen={errorModalOpen}
|
||||
onErrorModalClose={() => {
|
||||
setErrorModalOpen(false);
|
||||
setSelectedError(null);
|
||||
}}
|
||||
selectedError={selectedError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
||||
import { Clock, Plus } from "lucide-react";
|
||||
|
||||
interface CronJobEmptyStateProps {
|
||||
selectedUser: string | null;
|
||||
onNewTaskClick: () => void;
|
||||
}
|
||||
|
||||
export const CronJobEmptyState = ({
|
||||
selectedUser,
|
||||
onNewTaskClick,
|
||||
}: CronJobEmptyStateProps) => {
|
||||
return (
|
||||
<div className="text-center py-16">
|
||||
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-primary/20 to-blue-500/20 rounded-full flex items-center justify-center mb-6">
|
||||
<Clock className="h-10 w-10 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3 brand-gradient">
|
||||
{selectedUser
|
||||
? `No tasks for user ${selectedUser}`
|
||||
: "No scheduled tasks yet"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
||||
{selectedUser
|
||||
? `No scheduled tasks found for user ${selectedUser}. Try selecting a different user or create a new task.`
|
||||
: "Create your first scheduled task to automate your system operations and boost productivity."}
|
||||
</p>
|
||||
<Button
|
||||
onClick={onNewTaskClick}
|
||||
className="btn-primary glow-primary"
|
||||
size="lg"
|
||||
>
|
||||
<Plus className="h-5 w-5 mr-2" />
|
||||
Create Your First Task
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
198
app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem.tsx
Normal file
198
app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
||||
import {
|
||||
Trash2,
|
||||
Edit,
|
||||
Files,
|
||||
User,
|
||||
Play,
|
||||
Pause,
|
||||
Code,
|
||||
Info,
|
||||
} from "lucide-react";
|
||||
import { CronJob } from "@/app/_utils/cronjob-utils";
|
||||
import { JobError } from "@/app/_utils/error-utils";
|
||||
import { ErrorBadge } from "@/app/_components/GlobalComponents/Badges/ErrorBadge";
|
||||
import { parseCronExpression, type CronExplanation } from "@/app/_utils/parser-utils";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface CronJobItemProps {
|
||||
job: CronJob;
|
||||
errors: JobError[];
|
||||
runningJobId: string | null;
|
||||
deletingId: string | null;
|
||||
onRun: (id: string) => void;
|
||||
onEdit: (job: CronJob) => void;
|
||||
onClone: (job: CronJob) => void;
|
||||
onResume: (id: string) => void;
|
||||
onPause: (id: string) => void;
|
||||
onDelete: (job: CronJob) => void;
|
||||
onErrorClick: (error: JobError) => void;
|
||||
onErrorDismiss: () => void;
|
||||
}
|
||||
|
||||
export const CronJobItem = ({
|
||||
job,
|
||||
errors,
|
||||
runningJobId,
|
||||
deletingId,
|
||||
onRun,
|
||||
onEdit,
|
||||
onClone,
|
||||
onResume,
|
||||
onPause,
|
||||
onDelete,
|
||||
onErrorClick,
|
||||
onErrorDismiss,
|
||||
}: CronJobItemProps) => {
|
||||
const [cronExplanation, setCronExplanation] = useState<CronExplanation | null>(null);
|
||||
const locale = useLocale();
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
if (job.schedule) {
|
||||
const explanation = parseCronExpression(job.schedule, locale);
|
||||
setCronExplanation(explanation);
|
||||
} else {
|
||||
setCronExplanation(null);
|
||||
}
|
||||
}, [job.schedule]);
|
||||
return (
|
||||
<div
|
||||
key={job.id}
|
||||
className="glass-card p-4 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
||||
<div className="flex-1 min-w-0 order-2 lg:order-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<code className="text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 px-2 py-1 rounded font-mono border border-purple-500/20">
|
||||
{job.schedule}
|
||||
</code>
|
||||
<div className="flex-1 min-w-0">
|
||||
<pre
|
||||
className="text-sm font-medium text-foreground truncate bg-muted/30 px-2 py-1 rounded border border-border/30"
|
||||
title={job.command}
|
||||
>
|
||||
{job.command}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cronExplanation?.isValid && (
|
||||
<div className="flex items-start gap-1.5 mb-1">
|
||||
<Info className="h-3 w-3 text-primary mt-0.5 flex-shrink-0" />
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{cronExplanation.humanReadable}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{job.user}</span>
|
||||
</div>
|
||||
{job.paused && (
|
||||
<span className="text-xs bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/20">
|
||||
{t("cronjobs.paused")}
|
||||
</span>
|
||||
)}
|
||||
<ErrorBadge
|
||||
errors={errors}
|
||||
onErrorClick={onErrorClick}
|
||||
onErrorDismiss={onErrorDismiss}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{job.comment && (
|
||||
<p
|
||||
className="text-xs text-muted-foreground italic truncate"
|
||||
title={job.comment}
|
||||
>
|
||||
{job.comment}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0 order-1 lg:order-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onRun(job.id)}
|
||||
disabled={runningJobId === job.id || job.paused}
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.runCronManually")}
|
||||
aria-label={t("cronjobs.runCronManually")}
|
||||
>
|
||||
{runningJobId === job.id ? (
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<Code className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEdit(job)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.editCronJob")}
|
||||
aria-label={t("cronjobs.editCronJob")}
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onClone(job)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.cloneCronJob")}
|
||||
aria-label={t("cronjobs.cloneCronJob")}
|
||||
>
|
||||
<Files className="h-3 w-3" />
|
||||
</Button>
|
||||
{job.paused ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onResume(job.id)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.resumeCronJob")}
|
||||
aria-label={t("cronjobs.resumeCronJob")}
|
||||
>
|
||||
<Play className="h-3 w-3" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPause(job.id)}
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.pauseCronJob")}
|
||||
aria-label={t("cronjobs.pauseCronJob")}
|
||||
>
|
||||
<Pause className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => onDelete(job)}
|
||||
disabled={deletingId === job.id}
|
||||
className="btn-destructive h-8 px-3"
|
||||
title={t("cronjobs.deleteCronJob")}
|
||||
aria-label={t("cronjobs.deleteCronJob")}
|
||||
>
|
||||
{deletingId === job.id ? (
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<Trash2 className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
HardDrive,
|
||||
Wifi,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export interface SidebarProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
defaultCollapsed?: boolean;
|
||||
quickStats?: {
|
||||
cpu: number;
|
||||
@@ -28,13 +28,13 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
title = "System Overview",
|
||||
defaultCollapsed = false,
|
||||
quickStats,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const t = useTranslations();
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
|
||||
@@ -113,7 +113,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
</div>
|
||||
{(!isCollapsed || !isCollapsed) && (
|
||||
<h2 className="text-sm font-semibold text-foreground truncate">
|
||||
{title}
|
||||
{t("sidebar.systemOverview")}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ScriptsManager } from "@/app/_components/FeatureComponents/Scripts/Scri
|
||||
import { CronJob } from "@/app/_utils/cronjob-utils";
|
||||
import { Script } from "@/app/_utils/scripts-utils";
|
||||
import { Clock, FileText } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface TabbedInterfaceProps {
|
||||
cronJobs: CronJob[];
|
||||
@@ -19,6 +20,7 @@ export const TabbedInterface = ({
|
||||
const [activeTab, setActiveTab] = useState<"cronjobs" | "scripts">(
|
||||
"cronjobs"
|
||||
);
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -32,7 +34,7 @@ export const TabbedInterface = ({
|
||||
}`}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
Cron Jobs
|
||||
{t("cronjobs.cronJobs")}
|
||||
<span className="ml-1 text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full font-medium">
|
||||
{cronJobs.length}
|
||||
</span>
|
||||
@@ -45,7 +47,7 @@ export const TabbedInterface = ({
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Scripts
|
||||
{t("scripts.scripts")}
|
||||
<span className="ml-1 text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full font-medium">
|
||||
{scripts.length}
|
||||
</span>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { UserSwitcher } from "@/app/_components/FeatureComponents/User/UserSwitc
|
||||
import { Plus, Terminal, FileText, X } from "lucide-react";
|
||||
import { getScriptContent } from "@/app/_server/actions/scripts";
|
||||
import { getHostScriptPath } from "@/app/_server/actions/scripts";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface Script {
|
||||
id: string;
|
||||
@@ -46,6 +47,7 @@ export const CreateTaskModal = ({
|
||||
useState<string>("");
|
||||
const [isSelectScriptModalOpen, setIsSelectScriptModalOpen] = useState(false);
|
||||
const selectedScript = scripts.find((s) => s.id === form.selectedScriptId);
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const loadScriptContent = async () => {
|
||||
@@ -86,13 +88,13 @@ export const CreateTaskModal = ({
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title="Create New Scheduled Task"
|
||||
title={t("cronjobs.createNewScheduledTask")}
|
||||
size="lg"
|
||||
>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
User
|
||||
{t("common.user")}
|
||||
</label>
|
||||
<UserSwitcher
|
||||
selectedUser={form.user}
|
||||
@@ -102,7 +104,7 @@ export const CreateTaskModal = ({
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Schedule
|
||||
{t("cronjobs.schedule")}
|
||||
</label>
|
||||
<CronExpressionHelper
|
||||
value={form.schedule}
|
||||
@@ -114,7 +116,7 @@ export const CreateTaskModal = ({
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Task Type
|
||||
{t("cronjobs.taskType")}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
@@ -128,8 +130,8 @@ export const CreateTaskModal = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<Terminal className="h-5 w-5" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Custom Command</div>
|
||||
<div className="text-xs opacity-70">Single command</div>
|
||||
<div className="font-medium">{t("cronjobs.customCommand")}</div>
|
||||
<div className="text-xs opacity-70">{t("cronjobs.singleCommand")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -145,9 +147,9 @@ export const CreateTaskModal = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="h-5 w-5" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Saved Script</div>
|
||||
<div className="font-medium">{t("scripts.savedScript")}</div>
|
||||
<div className="text-xs opacity-70">
|
||||
Select from library
|
||||
{t("scripts.selectFromLibrary")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,7 +184,7 @@ export const CreateTaskModal = ({
|
||||
onClick={() => setIsSelectScriptModalOpen(true)}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
Change
|
||||
{t("common.change")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -201,7 +203,7 @@ export const CreateTaskModal = ({
|
||||
{!form.selectedScriptId && !selectedScript && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Command
|
||||
{t("cronjobs.command")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
@@ -222,8 +224,7 @@ export const CreateTaskModal = ({
|
||||
</div>
|
||||
{form.selectedScriptId && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Script path is read-only. Edit the script in the Scripts
|
||||
Library.
|
||||
{t("scripts.scriptPathReadOnly")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -231,13 +232,13 @@ export const CreateTaskModal = ({
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">
|
||||
Description{" "}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
{t("common.description")}
|
||||
<span className="text-muted-foreground">({t("common.optional")})</span>
|
||||
</label>
|
||||
<Input
|
||||
value={form.comment}
|
||||
onChange={(e) => onFormChange({ comment: e.target.value })}
|
||||
placeholder="What does this task do?"
|
||||
placeholder={t("cronjobs.whatDoesThisTaskDo")}
|
||||
className="bg-muted/30 border-border/50 focus:border-primary/50"
|
||||
/>
|
||||
</div>
|
||||
@@ -249,11 +250,11 @@ export const CreateTaskModal = ({
|
||||
onClick={onClose}
|
||||
className="btn-outline"
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" className="btn-primary glow-primary">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Task
|
||||
{t("cronjobs.createTask")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
121
app/_components/FeatureComponents/Modals/CronJobListsModals.tsx
Normal file
121
app/_components/FeatureComponents/Modals/CronJobListsModals.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { CreateTaskModal } from "@/app/_components/FeatureComponents/Modals/CreateTaskModal";
|
||||
import { EditTaskModal } from "@/app/_components/FeatureComponents/Modals/EditTaskModal";
|
||||
import { DeleteTaskModal } from "@/app/_components/FeatureComponents/Modals/DeleteTaskModal";
|
||||
import { CloneTaskModal } from "@/app/_components/FeatureComponents/Modals/CloneTaskModal";
|
||||
import { ErrorDetailsModal } from "@/app/_components/FeatureComponents/Modals/ErrorDetailsModal";
|
||||
import { CronJob } from "@/app/_utils/cronjob-utils";
|
||||
import { Script } from "@/app/_utils/scripts-utils";
|
||||
import { JobError } from "@/app/_utils/error-utils";
|
||||
|
||||
interface CronJobListModalsProps {
|
||||
cronJobs: CronJob[];
|
||||
scripts: Script[];
|
||||
|
||||
isNewCronModalOpen: boolean;
|
||||
onNewCronModalClose: () => void;
|
||||
onNewCronSubmit: (e: React.FormEvent) => Promise<void>;
|
||||
newCronForm: any;
|
||||
onNewCronFormChange: (updates: any) => void;
|
||||
|
||||
isEditModalOpen: boolean;
|
||||
onEditModalClose: () => void;
|
||||
onEditSubmit: (e: React.FormEvent) => Promise<void>;
|
||||
editForm: any;
|
||||
onEditFormChange: (updates: any) => void;
|
||||
|
||||
isDeleteModalOpen: boolean;
|
||||
onDeleteModalClose: () => void;
|
||||
onDeleteConfirm: () => void;
|
||||
jobToDelete: CronJob | null;
|
||||
|
||||
isCloneModalOpen: boolean;
|
||||
onCloneModalClose: () => void;
|
||||
onCloneConfirm: (newComment: string) => Promise<void>;
|
||||
jobToClone: CronJob | null;
|
||||
isCloning: boolean;
|
||||
|
||||
isErrorModalOpen: boolean;
|
||||
onErrorModalClose: () => void;
|
||||
selectedError: JobError | null;
|
||||
}
|
||||
|
||||
export const CronJobListModals = ({
|
||||
scripts,
|
||||
isNewCronModalOpen,
|
||||
onNewCronModalClose,
|
||||
onNewCronSubmit,
|
||||
newCronForm,
|
||||
onNewCronFormChange,
|
||||
isEditModalOpen,
|
||||
onEditModalClose,
|
||||
onEditSubmit,
|
||||
editForm,
|
||||
onEditFormChange,
|
||||
isDeleteModalOpen,
|
||||
onDeleteModalClose,
|
||||
onDeleteConfirm,
|
||||
jobToDelete,
|
||||
isCloneModalOpen,
|
||||
onCloneModalClose,
|
||||
onCloneConfirm,
|
||||
jobToClone,
|
||||
isCloning,
|
||||
isErrorModalOpen,
|
||||
onErrorModalClose,
|
||||
selectedError,
|
||||
}: CronJobListModalsProps) => {
|
||||
return (
|
||||
<>
|
||||
<CreateTaskModal
|
||||
isOpen={isNewCronModalOpen}
|
||||
onClose={onNewCronModalClose}
|
||||
onSubmit={onNewCronSubmit}
|
||||
scripts={scripts}
|
||||
form={newCronForm}
|
||||
onFormChange={onNewCronFormChange}
|
||||
/>
|
||||
|
||||
<EditTaskModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={onEditModalClose}
|
||||
onSubmit={onEditSubmit}
|
||||
form={editForm}
|
||||
onFormChange={onEditFormChange}
|
||||
/>
|
||||
|
||||
<DeleteTaskModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={onDeleteModalClose}
|
||||
onConfirm={onDeleteConfirm}
|
||||
job={jobToDelete}
|
||||
/>
|
||||
|
||||
<CloneTaskModal
|
||||
cronJob={jobToClone}
|
||||
isOpen={isCloneModalOpen}
|
||||
onClose={onCloneModalClose}
|
||||
onConfirm={onCloneConfirm}
|
||||
isCloning={isCloning}
|
||||
/>
|
||||
|
||||
{isErrorModalOpen && selectedError && (
|
||||
<ErrorDetailsModal
|
||||
isOpen={isErrorModalOpen}
|
||||
onClose={onErrorModalClose}
|
||||
error={{
|
||||
title: selectedError.title,
|
||||
message: selectedError.message,
|
||||
details: selectedError.details,
|
||||
command: selectedError.command,
|
||||
output: selectedError.output,
|
||||
stderr: selectedError.stderr,
|
||||
timestamp: selectedError.timestamp,
|
||||
jobId: selectedError.jobId,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { FileText, Search, Check, Terminal } from "lucide-react";
|
||||
import { Script } from "@/app/_utils/scripts-utils";
|
||||
import { getScriptContent } from "@/app/_server/actions/scripts";
|
||||
import { getHostScriptPath } from "@/app/_server/actions/scripts";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface SelectScriptModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +25,7 @@ export const SelectScriptModal = ({
|
||||
onScriptSelect,
|
||||
selectedScriptId,
|
||||
}: SelectScriptModalProps) => {
|
||||
const t = useTranslations();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [previewScript, setPreviewScript] = useState<Script | null>(null);
|
||||
const [previewContent, setPreviewContent] = useState<string>("");
|
||||
@@ -77,7 +79,7 @@ export const SelectScriptModal = ({
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
title="Select Script"
|
||||
title={t("scripts.selectScript")}
|
||||
size="xl"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
@@ -86,7 +88,7 @@ export const SelectScriptModal = ({
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search scripts..."
|
||||
placeholder={t("scripts.searchScripts")}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
@@ -95,13 +97,13 @@ export const SelectScriptModal = ({
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<div className="bg-muted/30 px-4 py-2 border-b border-border">
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
Available Scripts ({filteredScripts.length})
|
||||
{t("scripts.availableScripts", { count: filteredScripts.length })}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-y-auto h-full max-h-80">
|
||||
{filteredScripts.length === 0 ? (
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
{searchQuery ? "No scripts found" : "No scripts available"}
|
||||
{searchQuery ? t("scripts.noScriptsFound") : t("scripts.noScriptsAvailable")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
@@ -143,7 +145,7 @@ export const SelectScriptModal = ({
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<div className="bg-muted/30 px-4 py-2 border-b border-border">
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
Script Preview
|
||||
{t("scripts.scriptPreview")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 h-full max-h-80 overflow-y-auto">
|
||||
@@ -162,7 +164,7 @@ export const SelectScriptModal = ({
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Command Preview
|
||||
{t("scripts.commandPreview")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-muted/30 p-3 rounded border border-border/30">
|
||||
@@ -174,7 +176,7 @@ export const SelectScriptModal = ({
|
||||
|
||||
<div>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Script Content
|
||||
{t("scripts.scriptContent")}
|
||||
</span>
|
||||
<div className="bg-muted/30 p-3 rounded border border-border/30 mt-2 max-h-32 overflow-auto">
|
||||
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap">
|
||||
@@ -186,7 +188,7 @@ export const SelectScriptModal = ({
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Select a script to preview</p>
|
||||
<p>{t("scripts.selectScriptToPreview")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -200,7 +202,7 @@ export const SelectScriptModal = ({
|
||||
onClick={handleClose}
|
||||
className="btn-outline"
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -209,7 +211,7 @@ export const SelectScriptModal = ({
|
||||
className="btn-primary glow-primary"
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Select Script
|
||||
{t("scripts.selectScript")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ChevronUp,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { useLocale } from "next-intl";
|
||||
|
||||
interface CronExpressionHelperProps {
|
||||
value: string;
|
||||
@@ -34,6 +35,7 @@ export const CronExpressionHelper = ({
|
||||
className = "",
|
||||
showPatterns = true,
|
||||
}: CronExpressionHelperProps) => {
|
||||
const locale = useLocale();
|
||||
const [explanation, setExplanation] = useState<CronExplanation | null>(null);
|
||||
const [showPatternsPanel, setShowPatternsPanel] = useState(false);
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
@@ -49,7 +51,7 @@ export const CronExpressionHelper = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedValue) {
|
||||
const result = parseCronExpression(debouncedValue);
|
||||
const result = parseCronExpression(debouncedValue, locale);
|
||||
setExplanation(result);
|
||||
} else {
|
||||
setExplanation(null);
|
||||
|
||||
@@ -26,6 +26,7 @@ import { EditScriptModal } from "@/app/_components/FeatureComponents/Modals/Edit
|
||||
import { DeleteScriptModal } from "@/app/_components/FeatureComponents/Modals/DeleteScriptModal";
|
||||
import { CloneScriptModal } from "@/app/_components/FeatureComponents/Modals/CloneScriptModal";
|
||||
import { showToast } from "@/app/_components/GlobalComponents/UIElements/Toast";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ScriptsManagerProps {
|
||||
scripts: Script[];
|
||||
@@ -43,6 +44,7 @@ export const ScriptsManager = ({
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isCloning, setIsCloning] = useState(false);
|
||||
const t = useTranslations();
|
||||
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: "",
|
||||
@@ -173,10 +175,10 @@ export const ScriptsManager = ({
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl brand-gradient">
|
||||
Scripts Library
|
||||
{t("scripts.scriptsLibrary")}
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{scripts.length} saved script{scripts.length !== 1 ? "s" : ""}
|
||||
{t("scripts.nOfNSavedScripts", { count: scripts.length })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,7 +187,7 @@ export const ScriptsManager = ({
|
||||
className="btn-primary glow-primary"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Script
|
||||
{t("scripts.newScript")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -196,10 +198,10 @@ export const ScriptsManager = ({
|
||||
<FileText className="h-10 w-10 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3 brand-gradient">
|
||||
No scripts yet
|
||||
{t("scripts.noScriptsYet")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
||||
Create reusable bash scripts to use in your scheduled tasks.
|
||||
{t("scripts.createReusableBashScripts")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
@@ -207,7 +209,7 @@ export const ScriptsManager = ({
|
||||
size="lg"
|
||||
>
|
||||
<Plus className="h-5 w-5 mr-2" />
|
||||
Create Your First Script
|
||||
{t("scripts.createYourFirstScript")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -233,7 +235,7 @@ export const ScriptsManager = ({
|
||||
</p>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
File: {script.filename}
|
||||
{t("scripts.file")}: {script.filename}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ interface SystemInfoType {
|
||||
};
|
||||
}
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface SystemInfoCardProps {
|
||||
systemInfo: SystemInfoType;
|
||||
@@ -67,6 +68,7 @@ export const SystemInfoCard = ({
|
||||
const [systemInfo, setSystemInfo] =
|
||||
useState<SystemInfoType>(initialSystemInfo);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const t = useTranslations();
|
||||
|
||||
|
||||
|
||||
@@ -126,7 +128,7 @@ export const SystemInfoCard = ({
|
||||
const basicInfoItems = [
|
||||
{
|
||||
icon: Clock,
|
||||
label: "Uptime",
|
||||
label: t("sidebar.uptime"),
|
||||
value: systemInfo.uptime,
|
||||
color: "text-orange-500",
|
||||
},
|
||||
@@ -135,7 +137,7 @@ export const SystemInfoCard = ({
|
||||
const performanceItems = [
|
||||
{
|
||||
icon: HardDrive,
|
||||
label: "Memory",
|
||||
label: t("sidebar.memory"),
|
||||
value: `${systemInfo.memory.used} / ${systemInfo.memory.total}`,
|
||||
detail: `${systemInfo.memory.free} free`,
|
||||
status: systemInfo.memory.status,
|
||||
@@ -145,7 +147,7 @@ export const SystemInfoCard = ({
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
label: "CPU",
|
||||
label: t("sidebar.cpu"),
|
||||
value: systemInfo.cpu.model,
|
||||
detail: `${systemInfo.cpu.cores} cores`,
|
||||
status: systemInfo.cpu.status,
|
||||
@@ -155,7 +157,7 @@ export const SystemInfoCard = ({
|
||||
},
|
||||
{
|
||||
icon: Monitor,
|
||||
label: "GPU",
|
||||
label: t("sidebar.gpu"),
|
||||
value: systemInfo.gpu.model,
|
||||
detail: systemInfo.gpu.memory
|
||||
? `${systemInfo.gpu.memory} VRAM`
|
||||
@@ -165,7 +167,7 @@ export const SystemInfoCard = ({
|
||||
},
|
||||
...(systemInfo.network ? [{
|
||||
icon: Wifi,
|
||||
label: "Network",
|
||||
label: t("sidebar.network"),
|
||||
value: `${systemInfo.network.latency}ms`,
|
||||
detail: `${systemInfo.network.latency}ms latency • ${systemInfo.network.speed}`,
|
||||
status: systemInfo.network.status,
|
||||
@@ -175,17 +177,17 @@ export const SystemInfoCard = ({
|
||||
|
||||
const performanceMetrics = [
|
||||
{
|
||||
label: "CPU Usage",
|
||||
label: t("sidebar.cpuUsage"),
|
||||
value: `${systemInfo.cpu.usage}%`,
|
||||
status: systemInfo.cpu.status,
|
||||
},
|
||||
{
|
||||
label: "Memory Usage",
|
||||
label: t("sidebar.memoryUsage"),
|
||||
value: `${systemInfo.memory.usage}%`,
|
||||
status: systemInfo.memory.status,
|
||||
},
|
||||
...(systemInfo.network ? [{
|
||||
label: "Network Latency",
|
||||
label: t("sidebar.networkLatency"),
|
||||
value: `${systemInfo.network.latency}ms`,
|
||||
status: systemInfo.network.status,
|
||||
}] : []),
|
||||
@@ -193,7 +195,6 @@ export const SystemInfoCard = ({
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
title="System Overview"
|
||||
defaultCollapsed={false}
|
||||
quickStats={quickStats}
|
||||
>
|
||||
@@ -206,7 +207,7 @@ export const SystemInfoCard = ({
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-foreground mb-2 uppercase tracking-wide">
|
||||
System Information
|
||||
{t("sidebar.systemInformation")}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{basicInfoItems.map((item) => (
|
||||
@@ -224,7 +225,7 @@ export const SystemInfoCard = ({
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-foreground mb-2 uppercase tracking-wide">
|
||||
Performance Metrics
|
||||
{t("sidebar.performanceMetrics")}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{performanceItems.map((item) => (
|
||||
@@ -247,14 +248,14 @@ export const SystemInfoCard = ({
|
||||
<PerformanceSummary metrics={performanceMetrics} />
|
||||
|
||||
<div className="text-xs text-muted-foreground text-center p-2 bg-muted/20 rounded-lg">
|
||||
💡 Stats update every{" "}
|
||||
{t("sidebar.statsUpdateEvery")}{" "}
|
||||
{Math.round(
|
||||
parseInt(process.env.NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL || "30000") /
|
||||
1000
|
||||
)}
|
||||
s • Network speed estimated from latency
|
||||
s • {t("sidebar.networkSpeedEstimatedFromLatency")}
|
||||
{isUpdating && (
|
||||
<span className="ml-2 animate-pulse">🔄 Updating...</span>
|
||||
<span className="ml-2 animate-pulse">{t("sidebar.updating")}...</span>
|
||||
)}
|
||||
</div>
|
||||
</Sidebar>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from "@/app/_utils/global-utils";
|
||||
import { HTMLAttributes, forwardRef } from "react";
|
||||
import { Activity } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export interface SystemStatusProps extends HTMLAttributes<HTMLDivElement> {
|
||||
status: string;
|
||||
@@ -14,6 +15,7 @@ export const SystemStatus = forwardRef<HTMLDivElement, SystemStatusProps>(
|
||||
{ className, status, details, timestamp, isUpdating = false, ...props },
|
||||
ref
|
||||
) => {
|
||||
const t = useTranslations();
|
||||
const getStatusConfig = (status: string) => {
|
||||
const lowerStatus = status.toLowerCase();
|
||||
|
||||
@@ -64,11 +66,11 @@ export const SystemStatus = forwardRef<HTMLDivElement, SystemStatusProps>(
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
System Status: {status}
|
||||
{t("system.systemStatus")}: {status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{details} • Last updated: {timestamp}
|
||||
{details} • {t("system.lastUpdated")}: {timestamp}
|
||||
{isUpdating && <span className="ml-2 animate-pulse">🔄</span>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
||||
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
||||
import { ChevronDown, User, X } from "lucide-react";
|
||||
import { fetchAvailableUsers } from "@/app/_server/actions/cronjobs";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface UserFilterProps {
|
||||
selectedUser: string | null;
|
||||
@@ -19,6 +20,7 @@ export const UserFilter = ({
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const loadUsers = async () => {
|
||||
@@ -56,7 +58,7 @@ export const UserFilter = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4" />
|
||||
<span className="text-sm">
|
||||
{selectedUser ? `User: ${selectedUser}` : "All users"}
|
||||
{selectedUser ? `${t("common.userWithUsername", { user: selectedUser })}` : t("common.allUsers")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -85,7 +87,7 @@ export const UserFilter = ({
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${!selectedUser ? "bg-accent text-accent-foreground" : ""
|
||||
}`}
|
||||
>
|
||||
All users
|
||||
{t("common.allUsers")}
|
||||
</button>
|
||||
{users.map((user) => (
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from "@/app/_utils/global-utils";
|
||||
import { HTMLAttributes, forwardRef } from "react";
|
||||
import { CheckCircle, AlertTriangle, XCircle, Activity } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export interface StatusBadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||
status: string;
|
||||
@@ -21,6 +22,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const t = useTranslations();
|
||||
const getStatusConfig = (status: string) => {
|
||||
const lowerStatus = status.toLowerCase();
|
||||
|
||||
@@ -33,7 +35,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
bgColor: "bg-emerald-500/10",
|
||||
borderColor: "border-emerald-500/20",
|
||||
icon: CheckCircle,
|
||||
label: "Optimal",
|
||||
label: t("system.optimal"),
|
||||
};
|
||||
case "moderate":
|
||||
case "warning":
|
||||
@@ -42,7 +44,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
bgColor: "bg-yellow-500/10",
|
||||
borderColor: "border-yellow-500/20",
|
||||
icon: AlertTriangle,
|
||||
label: "Warning",
|
||||
label: t("system.warning"),
|
||||
};
|
||||
case "high":
|
||||
case "slow":
|
||||
@@ -51,7 +53,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
bgColor: "bg-orange-500/10",
|
||||
borderColor: "border-orange-500/20",
|
||||
icon: AlertTriangle,
|
||||
label: "High",
|
||||
label: t("system.high"),
|
||||
};
|
||||
case "critical":
|
||||
case "poor":
|
||||
@@ -61,7 +63,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
bgColor: "bg-destructive/10",
|
||||
borderColor: "border-destructive/20",
|
||||
icon: XCircle,
|
||||
label: "Critical",
|
||||
label: t("system.critical"),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
@@ -69,7 +71,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
bgColor: "bg-muted",
|
||||
borderColor: "border-border",
|
||||
icon: Activity,
|
||||
label: "Unknown",
|
||||
label: t("system.unknown"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
4
app/_consts/global.ts
Normal file
4
app/_consts/global.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const Locales = [
|
||||
{ locale: "en", label: "English" },
|
||||
{ locale: "it", label: "Italian" },
|
||||
];
|
||||
202
app/_hooks/useCronJobState.ts
Normal file
202
app/_hooks/useCronJobState.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { CronJob } from "@/app/_utils/cronjob-utils";
|
||||
import { Script } from "@/app/_utils/scripts-utils";
|
||||
import {
|
||||
getJobErrorsByJobId,
|
||||
JobError,
|
||||
} from "@/app/_utils/error-utils";
|
||||
import {
|
||||
handleErrorClick,
|
||||
handleDelete,
|
||||
handleClone,
|
||||
handlePause,
|
||||
handleResume,
|
||||
handleRun,
|
||||
handleEditSubmit,
|
||||
handleNewCronSubmit,
|
||||
} from "@/app/_components/FeatureComponents/Cronjobs/helpers";
|
||||
|
||||
interface CronJobListProps {
|
||||
cronJobs: CronJob[];
|
||||
scripts: Script[];
|
||||
}
|
||||
|
||||
export const useCronJobState = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [editingJob, setEditingJob] = useState<CronJob | null>(null);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isNewCronModalOpen, setIsNewCronModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
|
||||
const [jobToDelete, setJobToDelete] = useState<CronJob | null>(null);
|
||||
const [jobToClone, setJobToClone] = useState<CronJob | null>(null);
|
||||
const [isCloning, setIsCloning] = useState(false);
|
||||
const [runningJobId, setRunningJobId] = useState<string | null>(null);
|
||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||
const [jobErrors, setJobErrors] = useState<Record<string, JobError[]>>({});
|
||||
const [errorModalOpen, setErrorModalOpen] = useState(false);
|
||||
const [selectedError, setSelectedError] = useState<JobError | null>(null);
|
||||
|
||||
const [editForm, setEditForm] = useState({
|
||||
schedule: "",
|
||||
command: "",
|
||||
comment: "",
|
||||
});
|
||||
const [newCronForm, setNewCronForm] = useState({
|
||||
schedule: "",
|
||||
command: "",
|
||||
comment: "",
|
||||
selectedScriptId: null as string | null,
|
||||
user: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const savedUser = localStorage.getItem("selectedCronUser");
|
||||
if (savedUser) {
|
||||
setSelectedUser(savedUser);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUser) {
|
||||
localStorage.setItem("selectedCronUser", selectedUser);
|
||||
} else {
|
||||
localStorage.removeItem("selectedCronUser");
|
||||
}
|
||||
}, [selectedUser]);
|
||||
|
||||
const filteredJobs = useMemo(() => {
|
||||
if (!selectedUser) return cronJobs;
|
||||
return cronJobs.filter((job) => job.user === selectedUser);
|
||||
}, [cronJobs, selectedUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const errors: Record<string, JobError[]> = {};
|
||||
filteredJobs.forEach((job) => {
|
||||
errors[job.id] = getJobErrorsByJobId(job.id);
|
||||
});
|
||||
setJobErrors(errors);
|
||||
}, [filteredJobs]);
|
||||
|
||||
|
||||
const refreshJobErrorsLocal = () => {
|
||||
const errors: Record<string, JobError[]> = {};
|
||||
filteredJobs.forEach((job) => {
|
||||
errors[job.id] = getJobErrorsByJobId(job.id);
|
||||
});
|
||||
setJobErrors(errors);
|
||||
};
|
||||
|
||||
const getHelperState = () => ({
|
||||
setDeletingId,
|
||||
setIsDeleteModalOpen,
|
||||
setJobToDelete,
|
||||
setIsCloneModalOpen,
|
||||
setJobToClone,
|
||||
setIsCloning,
|
||||
setIsEditModalOpen,
|
||||
setEditingJob,
|
||||
setIsNewCronModalOpen,
|
||||
setNewCronForm,
|
||||
setRunningJobId,
|
||||
refreshJobErrors: refreshJobErrorsLocal,
|
||||
jobToClone,
|
||||
editingJob,
|
||||
editForm,
|
||||
newCronForm,
|
||||
});
|
||||
|
||||
const handleErrorClickLocal = (error: JobError) => {
|
||||
handleErrorClick(error, setSelectedError, setErrorModalOpen);
|
||||
};
|
||||
|
||||
const handleDeleteLocal = async (id: string) => {
|
||||
await handleDelete(id, getHelperState());
|
||||
};
|
||||
|
||||
const handleCloneLocal = async (newComment: string) => {
|
||||
await handleClone(newComment, getHelperState());
|
||||
};
|
||||
|
||||
const handlePauseLocal = async (id: string) => {
|
||||
await handlePause(id);
|
||||
};
|
||||
|
||||
const handleResumeLocal = async (id: string) => {
|
||||
await handleResume(id);
|
||||
};
|
||||
|
||||
const handleRunLocal = async (id: string) => {
|
||||
await handleRun(id, getHelperState());
|
||||
};
|
||||
|
||||
const confirmDelete = (job: CronJob) => {
|
||||
setJobToDelete(job);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const confirmClone = (job: CronJob) => {
|
||||
setJobToClone(job);
|
||||
setIsCloneModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (job: CronJob) => {
|
||||
setEditingJob(job);
|
||||
setEditForm({
|
||||
schedule: job.schedule,
|
||||
command: job.command,
|
||||
comment: job.comment || "",
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditSubmitLocal = async (e: React.FormEvent) => {
|
||||
await handleEditSubmit(e, getHelperState());
|
||||
};
|
||||
|
||||
const handleNewCronSubmitLocal = async (e: React.FormEvent) => {
|
||||
await handleNewCronSubmit(e, getHelperState());
|
||||
};
|
||||
|
||||
return {
|
||||
deletingId,
|
||||
runningJobId,
|
||||
selectedUser,
|
||||
setSelectedUser,
|
||||
jobErrors,
|
||||
errorModalOpen,
|
||||
setErrorModalOpen,
|
||||
selectedError,
|
||||
setSelectedError,
|
||||
filteredJobs,
|
||||
isNewCronModalOpen,
|
||||
setIsNewCronModalOpen,
|
||||
isEditModalOpen,
|
||||
setIsEditModalOpen,
|
||||
isDeleteModalOpen,
|
||||
setIsDeleteModalOpen,
|
||||
isCloneModalOpen,
|
||||
setIsCloneModalOpen,
|
||||
jobToDelete,
|
||||
jobToClone,
|
||||
isCloning,
|
||||
editForm,
|
||||
setEditForm,
|
||||
newCronForm,
|
||||
setNewCronForm,
|
||||
handleErrorClickLocal,
|
||||
refreshJobErrorsLocal,
|
||||
handleDeleteLocal,
|
||||
handleCloneLocal,
|
||||
handlePauseLocal,
|
||||
handleResumeLocal,
|
||||
handleRunLocal,
|
||||
confirmDelete,
|
||||
confirmClone,
|
||||
handleEdit,
|
||||
handleEditSubmitLocal,
|
||||
handleNewCronSubmitLocal,
|
||||
};
|
||||
};
|
||||
94
app/_translations/en.json
Normal file
94
app/_translations/en.json
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"common": {
|
||||
"cronManagementMadeEasy": "Cron Management made easy",
|
||||
"allUsers": "All users",
|
||||
"userWithUsername": "User: {user}",
|
||||
"user": "User",
|
||||
"change": "Change",
|
||||
"description": "Description",
|
||||
"optional": "Optional",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"cronjobs": {
|
||||
"cronJobs": "Cron Jobs",
|
||||
"cronJob": "Cron Job",
|
||||
"scheduledTasks": "Scheduled Tasks",
|
||||
"nOfNJObs": "{filtered} of {total} scheduled tasks",
|
||||
"forUser": "for user {user}",
|
||||
"newTask": "New Task",
|
||||
"runCronManually": "Run cron job manually",
|
||||
"editCronJob": "Edit cron job",
|
||||
"cloneCronJob": "Clone cron job",
|
||||
"deleteCronJob": "Delete cron job",
|
||||
"pauseCronJob": "Pause cron job",
|
||||
"resumeCronJob": "Resume cron job",
|
||||
"runCronJob": "Run cron job",
|
||||
"runCronJobSuccess": "Cron job executed successfully",
|
||||
"runCronJobFailed": "Failed to execute cron job",
|
||||
"paused": "Paused",
|
||||
"createNewScheduledTask": "Create new scheduled task",
|
||||
"schedule": "Schedule",
|
||||
"taskType": "Task Type",
|
||||
"customCommand": "Custom Command",
|
||||
"singleCommand": "Single command",
|
||||
"command": "Command",
|
||||
"whatDoesThisTaskDo": "What does this task do?",
|
||||
"createTask": "Create Task"
|
||||
},
|
||||
"scripts": {
|
||||
"scripts": "Scripts",
|
||||
"scriptsLibrary": "Scripts Library",
|
||||
"file": "File",
|
||||
"newScript": "New Script",
|
||||
"noScriptsYet": "No scripts yet",
|
||||
"createReusableBashScripts": "Create reusable bash scripts to use in your scheduled tasks.",
|
||||
"createYourFirstScript": "Create Your First Script",
|
||||
"nOfNSavedScripts": "{count} saved scripts",
|
||||
"savedScript": "Saved Script",
|
||||
"selectFromLibrary": "Select from library",
|
||||
"scriptPathReadOnly": "Script path is read-only. Edit the script in the Scripts Library",
|
||||
"selectScript": "Select Script",
|
||||
"availableScripts": "{count} available scripts",
|
||||
"noScriptsFound": "No scripts found",
|
||||
"noScriptsAvailable": "No scripts available",
|
||||
"scriptPreview": "Script Preview",
|
||||
"commandPreview": "Command Preview",
|
||||
"scriptContent": "Script Content",
|
||||
"selectScriptToPreview": "Select a script to preview",
|
||||
"searchScripts": "Search scripts..."
|
||||
},
|
||||
"sidebar": {
|
||||
"systemOverview": "System Overview",
|
||||
"uptime": "Uptime",
|
||||
"memory": "Memory",
|
||||
"cpu": "CPU",
|
||||
"gpu": "GPU",
|
||||
"network": "Network",
|
||||
"networkLatency": "Network Latency",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"systemInformation": "System Information",
|
||||
"performanceMetrics": "Performance Metrics",
|
||||
"statsUpdateEvery": "Stats update every",
|
||||
"updating": "Updating",
|
||||
"networkSpeedEstimatedFromLatency": "Network speed estimated from latency"
|
||||
},
|
||||
"system": {
|
||||
"optimal": "Optimal",
|
||||
"critical": "Critical",
|
||||
"high": "High",
|
||||
"moderate": "Moderate",
|
||||
"warning": "Warning",
|
||||
"unknown": "Unknown",
|
||||
"connected": "Connected",
|
||||
"allSystemsRunningNormally": "All systems running normally",
|
||||
"highResourceUsageDetectedImmediateAttentionRequired": "High resource usage detected - immediate attention required",
|
||||
"moderateResourceUsageMonitoringRecommended": "Moderate resource usage - monitoring recommended",
|
||||
"unknownGPU": "Unknown GPU",
|
||||
"noGPUDetected": "No GPU detected",
|
||||
"gpuDetectionFailed": "GPU detection failed",
|
||||
"available": "Available",
|
||||
"systemStatus": "System Status",
|
||||
"lastUpdated": "Last updated"
|
||||
}
|
||||
}
|
||||
94
app/_translations/it.json
Normal file
94
app/_translations/it.json
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"common": {
|
||||
"cronManagementMadeEasy": "Gestione Cron semplificata",
|
||||
"allUsers": "Tutti gli utenti",
|
||||
"userWithUsername": "Utente: {user}",
|
||||
"user": "Utente",
|
||||
"change": "Modifica",
|
||||
"description": "Descrizione",
|
||||
"optional": "Opzionale",
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"cronjobs": {
|
||||
"cronJobs": "Operazioni Cron",
|
||||
"cronJob": "Operazione Cron",
|
||||
"scheduledTasks": "Operazioni Pianificate",
|
||||
"nOfNJObs": "{filtered} di {total} operazioni pianificate",
|
||||
"forUser": "per l'utente {user}",
|
||||
"newTask": "Nuova Operazione",
|
||||
"runCronManually": "Esegui operazione cron manualmente",
|
||||
"editCronJob": "Modifica operazione cron",
|
||||
"cloneCronJob": "Clona operazione cron",
|
||||
"deleteCronJob": "Elimina operazione cron",
|
||||
"pauseCronJob": "Pausa operazione cron",
|
||||
"resumeCronJob": "Riprendi operazione cron",
|
||||
"runCronJob": "Esegui operazione cron",
|
||||
"runCronJobSuccess": "Operazione cron eseguita con successo",
|
||||
"runCronJobFailed": "Esecuzione operazione cron fallita",
|
||||
"paused": "In pausa",
|
||||
"createNewScheduledTask": "Crea nuova operazione pianificata",
|
||||
"schedule": "Pianificazione",
|
||||
"taskType": "Tipo di Operazione",
|
||||
"customCommand": "Comando Personalizzato",
|
||||
"singleCommand": "Comando singolo",
|
||||
"command": "Comando",
|
||||
"whatDoesThisTaskDo": "Cosa fa questa operazione?",
|
||||
"createTask": "Crea Operazione"
|
||||
},
|
||||
"scripts": {
|
||||
"scripts": "Script",
|
||||
"scriptsLibrary": "Libreria Script",
|
||||
"file": "File",
|
||||
"newScript": "Nuovo Script",
|
||||
"noScriptsYet": "Ancora nessuno script",
|
||||
"createReusableBashScripts": "Crea script bash riutilizzabili da usare nelle tue operazioni pianificate.",
|
||||
"createYourFirstScript": "Crea il tuo primo script",
|
||||
"nOfNSavedScripts": "{count} script salvati",
|
||||
"savedScript": "Script Salvato",
|
||||
"selectFromLibrary": "Seleziona dalla libreria",
|
||||
"scriptPathReadOnly": "Il percorso dello script è di sola lettura. Modifica lo script nella Libreria Script",
|
||||
"selectScript": "Seleziona Script",
|
||||
"availableScripts": "{count} script disponibili",
|
||||
"noScriptsFound": "Nessuno script trovato",
|
||||
"noScriptsAvailable": "Nessuno script disponibile",
|
||||
"scriptPreview": "Anteprima Script",
|
||||
"commandPreview": "Anteprima Comando",
|
||||
"scriptContent": "Contenuto Script",
|
||||
"selectScriptToPreview": "Seleziona uno script per l'anteprima",
|
||||
"searchScripts": "Cerca script..."
|
||||
},
|
||||
"sidebar": {
|
||||
"systemOverview": "Panoramica del Sistema",
|
||||
"uptime": "Uptime",
|
||||
"memory": "Memoria",
|
||||
"cpu": "CPU",
|
||||
"gpu": "GPU",
|
||||
"network": "Rete",
|
||||
"networkLatency": "Latenza di Rete",
|
||||
"memoryUsage": "Utilizzo Memoria",
|
||||
"cpuUsage": "Utilizzo CPU",
|
||||
"systemInformation": "Informazioni di Sistema",
|
||||
"performanceMetrics": "Metriche delle Prestazioni",
|
||||
"statsUpdateEvery": "Statistiche aggiornate ogni",
|
||||
"updating": "Aggiornamento",
|
||||
"networkSpeedEstimatedFromLatency": "Velocità di rete stimata dalla latenza"
|
||||
},
|
||||
"system": {
|
||||
"optimal": "Ottimale",
|
||||
"critical": "Critico",
|
||||
"high": "Alto",
|
||||
"moderate": "Moderato",
|
||||
"warning": "Avviso",
|
||||
"unknown": "Sconosciuto",
|
||||
"connected": "Connesso",
|
||||
"allSystemsRunningNormally": "Tutti i sistemi funzionano normalmente",
|
||||
"highResourceUsageDetectedImmediateAttentionRequired": "Rilevato utilizzo elevato delle risorse - richiesta attenzione immediata",
|
||||
"moderateResourceUsageMonitoringRecommended": "Utilizzo moderato delle risorse - monitoraggio raccomandato",
|
||||
"unknownGPU": "GPU Sconosciuta",
|
||||
"noGPUDetected": "Nessuna GPU rilevata",
|
||||
"gpuDetectionFailed": "Rilevamento GPU fallito",
|
||||
"available": "Disponibile",
|
||||
"systemStatus": "Stato del Sistema",
|
||||
"lastUpdated": "Ultimo aggiornamento"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import cronstrue from "cronstrue";
|
||||
import cronstrue from 'cronstrue/i18n';
|
||||
|
||||
export interface CronExplanation {
|
||||
humanReadable: string;
|
||||
@@ -7,7 +7,7 @@ export interface CronExplanation {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const parseCronExpression = (expression: string): CronExplanation => {
|
||||
export const parseCronExpression = (expression: string, locale?: string): CronExplanation => {
|
||||
try {
|
||||
const cleanExpression = expression.trim();
|
||||
|
||||
@@ -23,6 +23,7 @@ export const parseCronExpression = (expression: string): CronExplanation => {
|
||||
const humanReadable = cronstrue.toString(cleanExpression, {
|
||||
verbose: true,
|
||||
throwExceptionOnParseError: false,
|
||||
locale: locale || "en",
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import * as si from 'systeminformation';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const t = await getTranslations();
|
||||
const [
|
||||
osInfo,
|
||||
memInfo,
|
||||
cpuInfo,
|
||||
diskInfo,
|
||||
@@ -13,7 +16,6 @@ export async function GET(request: NextRequest) {
|
||||
uptimeInfo,
|
||||
networkInfo
|
||||
] = await Promise.all([
|
||||
si.osInfo(),
|
||||
si.mem(),
|
||||
si.cpu(),
|
||||
si.fsSize(),
|
||||
@@ -46,32 +48,32 @@ export async function GET(request: NextRequest) {
|
||||
const actualUsed = memInfo.active || memInfo.used;
|
||||
const actualFree = memInfo.available || memInfo.free;
|
||||
const memUsage = ((actualUsed / memInfo.total) * 100);
|
||||
let memStatus = "Optimal";
|
||||
if (memUsage > 90) memStatus = "Critical";
|
||||
else if (memUsage > 80) memStatus = "High";
|
||||
else if (memUsage > 70) memStatus = "Moderate";
|
||||
let memStatus = t("system.optimal");
|
||||
if (memUsage > 90) memStatus = t("system.critical");
|
||||
else if (memUsage > 80) memStatus = t("system.high");
|
||||
else if (memUsage > 70) memStatus = t("system.moderate");
|
||||
|
||||
const rootDisk = diskInfo.find(disk => disk.mount === '/') || diskInfo[0];
|
||||
const diskUsage = rootDisk ? ((rootDisk.used / rootDisk.size) * 100) : 0;
|
||||
let diskStatus = "Optimal";
|
||||
if (diskUsage > 90) diskStatus = "Critical";
|
||||
else if (diskUsage > 80) diskStatus = "High";
|
||||
else if (diskUsage > 70) diskStatus = "Moderate";
|
||||
let diskStatus = t("system.optimal");
|
||||
if (diskUsage > 90) diskStatus = t("system.critical");
|
||||
else if (diskUsage > 80) diskStatus = t("system.high");
|
||||
else if (diskUsage > 70) diskStatus = t("system.moderate");
|
||||
|
||||
const cpuStatus = loadInfo.currentLoad > 80 ? "High" :
|
||||
loadInfo.currentLoad > 60 ? "Moderate" : "Optimal";
|
||||
const cpuStatus = loadInfo.currentLoad > 80 ? t("system.high") :
|
||||
loadInfo.currentLoad > 60 ? t("system.moderate") : t("system.optimal");
|
||||
|
||||
const criticalThreshold = 90;
|
||||
const warningThreshold = 80;
|
||||
let overallStatus = "Optimal";
|
||||
let statusDetails = "All systems running normally";
|
||||
let overallStatus = t("system.optimal");
|
||||
let statusDetails = t("system.allSystemsRunningNormally");
|
||||
|
||||
if (memUsage > criticalThreshold || loadInfo.currentLoad > criticalThreshold || diskUsage > criticalThreshold) {
|
||||
overallStatus = "Critical";
|
||||
statusDetails = "High resource usage detected - immediate attention required";
|
||||
overallStatus = t("system.critical");
|
||||
statusDetails = t("system.highResourceUsageDetectedImmediateAttentionRequired");
|
||||
} else if (memUsage > warningThreshold || loadInfo.currentLoad > warningThreshold || diskUsage > warningThreshold) {
|
||||
overallStatus = "Warning";
|
||||
statusDetails = "Moderate resource usage - monitoring recommended";
|
||||
overallStatus = t("system.warning");
|
||||
statusDetails = t("system.moderateResourceUsageMonitoringRecommended");
|
||||
}
|
||||
|
||||
let mainInterface: any = null;
|
||||
@@ -85,7 +87,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const networkSpeed = mainInterface && 'rx_sec' in mainInterface && 'tx_sec' in mainInterface
|
||||
? `${Math.round(((mainInterface.rx_sec || 0) + (mainInterface.tx_sec || 0)) / 1024 / 1024)} Mbps`
|
||||
: "Unknown";
|
||||
: t("system.unknown");
|
||||
|
||||
let latency = 0;
|
||||
try {
|
||||
@@ -117,9 +119,9 @@ export async function GET(request: NextRequest) {
|
||||
status: cpuStatus,
|
||||
},
|
||||
disk: {
|
||||
total: rootDisk ? formatBytes(rootDisk.size) : "Unknown",
|
||||
used: rootDisk ? formatBytes(rootDisk.used) : "Unknown",
|
||||
free: rootDisk ? formatBytes(rootDisk.available) : "Unknown",
|
||||
total: rootDisk ? formatBytes(rootDisk.size) : t("system.unknown"),
|
||||
used: rootDisk ? formatBytes(rootDisk.used) : t("system.unknown"),
|
||||
free: rootDisk ? formatBytes(rootDisk.available) : t("system.unknown"),
|
||||
usage: Math.round(diskUsage),
|
||||
status: diskStatus,
|
||||
},
|
||||
@@ -128,7 +130,7 @@ export async function GET(request: NextRequest) {
|
||||
latency: latency,
|
||||
downloadSpeed: mainInterface && 'rx_sec' in mainInterface ? Math.round((mainInterface.rx_sec || 0) / 1024 / 1024) : 0,
|
||||
uploadSpeed: mainInterface && 'tx_sec' in mainInterface ? Math.round((mainInterface.tx_sec || 0) / 1024 / 1024) : 0,
|
||||
status: mainInterface && 'operstate' in mainInterface && mainInterface.operstate === 'up' ? "Connected" : "Unknown",
|
||||
status: mainInterface && 'operstate' in mainInterface && mainInterface.operstate === 'up' ? t("system.connected") : t("system.unknown"),
|
||||
},
|
||||
systemStatus: {
|
||||
overall: overallStatus,
|
||||
@@ -141,20 +143,20 @@ export async function GET(request: NextRequest) {
|
||||
if (graphics.controllers && graphics.controllers.length > 0) {
|
||||
const gpu = graphics.controllers[0];
|
||||
systemStats.gpu = {
|
||||
model: gpu.model || "Unknown GPU",
|
||||
model: gpu.model || t("system.unknownGPU"),
|
||||
memory: gpu.vram ? `${gpu.vram} MB` : undefined,
|
||||
status: "Available",
|
||||
status: t("system.available"),
|
||||
};
|
||||
} else {
|
||||
systemStats.gpu = {
|
||||
model: "No GPU detected",
|
||||
status: "Unknown",
|
||||
model: t("system.noGPUDetected"),
|
||||
status: t("system.unknown"),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
systemStats.gpu = {
|
||||
model: "GPU detection failed",
|
||||
status: "Unknown",
|
||||
model: t("system.gpuDetectionFailed"),
|
||||
status: t("system.unknown"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
24
app/i18n.ts
Normal file
24
app/i18n.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { Locales } from "@/app/_consts/global";
|
||||
|
||||
const validLocales = Locales.map((item) => item.locale);
|
||||
|
||||
export default getRequestConfig(async ({ locale }) => {
|
||||
const safeLocale = locale && validLocales.includes(locale) ? locale : "en";
|
||||
|
||||
try {
|
||||
return {
|
||||
locale: safeLocale,
|
||||
messages: (await import(`./_translations/${safeLocale}.json`)).default,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to load translations for locale: ${safeLocale}`,
|
||||
error
|
||||
);
|
||||
return {
|
||||
locale: "en",
|
||||
messages: (await import("./_translations/en.json")).default,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,9 @@ import { JetBrains_Mono, Inter } from "next/font/google";
|
||||
import "@/app/globals.css";
|
||||
import { ThemeProvider } from "@/app/_providers/ThemeProvider";
|
||||
import { ServiceWorkerRegister } from "@/app/_components/FeatureComponents/PWA/ServiceWorkerRegister";
|
||||
import { Locales } from "@/app/_consts/global";
|
||||
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
@@ -43,11 +46,21 @@ export const viewport = {
|
||||
themeColor: "#3b82f6",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
let locale = process.env.LOCALE || "en";
|
||||
let messages;
|
||||
|
||||
|
||||
if (!Locales.some((item) => item.locale === locale)) {
|
||||
locale = "en";
|
||||
}
|
||||
|
||||
messages = (await import(`./_translations/${locale}.json`)).default;
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
@@ -59,15 +72,18 @@ export default function RootLayout({
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
</head>
|
||||
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
<ServiceWorkerRegister />
|
||||
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
<ServiceWorkerRegister />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -6,9 +6,12 @@ import { ThemeToggle } from "@/app/_components/FeatureComponents/Theme/ThemeTogg
|
||||
import { LogoutButton } from "@/app/_components/FeatureComponents/LoginForm/LogoutButton";
|
||||
import { ToastContainer } from "@/app/_components/GlobalComponents/UIElements/Toast";
|
||||
import { PWAInstallPrompt } from "@/app/_components/FeatureComponents/PWA/PWAInstallPrompt";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function Home() {
|
||||
const t = await getTranslations();
|
||||
const [cronJobs, scripts] = await Promise.all([
|
||||
getCronJobs(),
|
||||
fetchScripts(),
|
||||
@@ -65,7 +68,7 @@ export default async function Home() {
|
||||
Cr*nMaster
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground font-mono tracking-wide">
|
||||
Cron Management made easy
|
||||
{t("common.cronManagementMadeEasy")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const withNextIntl = require('next-intl/plugin')('./app/i18n.ts');
|
||||
|
||||
const withPWA = require('next-pwa')({
|
||||
dest: 'public',
|
||||
register: true,
|
||||
@@ -8,6 +10,13 @@ const withPWA = require('next-pwa')({
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (config) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'osx-temperature-sensor': false,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -27,4 +36,6 @@ const nextConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = withPWA(nextConfig)
|
||||
module.exports = withNextIntl({
|
||||
...withPWA(nextConfig)
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"lucide-react": "^0.294.0",
|
||||
"minimatch": "^10.0.3",
|
||||
"next": "14.0.4",
|
||||
"next-intl": "^4.4.0",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8",
|
||||
|
||||
93
yarn.lock
93
yarn.lock
@@ -955,6 +955,54 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
|
||||
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
|
||||
|
||||
"@formatjs/ecma402-abstract@2.3.6":
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz#d6ca9d3579054fe1e1a0a0b5e872e0d64922e4e1"
|
||||
integrity sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize" "2.2.7"
|
||||
"@formatjs/intl-localematcher" "0.6.2"
|
||||
decimal.js "^10.4.3"
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@formatjs/fast-memoize@2.2.7", "@formatjs/fast-memoize@^2.2.0":
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz#707f9ddaeb522a32f6715bb7950b0831f4cc7b15"
|
||||
integrity sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==
|
||||
dependencies:
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@formatjs/icu-messageformat-parser@2.11.4":
|
||||
version "2.11.4"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz#63bd2cd82d08ae2bef55adeeb86486df68826f32"
|
||||
integrity sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.3.6"
|
||||
"@formatjs/icu-skeleton-parser" "1.8.16"
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@formatjs/icu-skeleton-parser@1.8.16":
|
||||
version "1.8.16"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz#13f81f6845c7cf6599623006aacaf7d6b4ad2970"
|
||||
integrity sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.3.6"
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@formatjs/intl-localematcher@0.6.2":
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz#e9ebe0b4082d7d48e5b2d753579fb7ece4eaefea"
|
||||
integrity sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==
|
||||
dependencies:
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@formatjs/intl-localematcher@^0.5.4":
|
||||
version "0.5.10"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz#1e0bd3fc1332c1fe4540cfa28f07e9227b659a58"
|
||||
integrity sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==
|
||||
dependencies:
|
||||
tslib "2"
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
version "0.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
|
||||
@@ -1209,6 +1257,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz#326a7b46f6d4cfa54ae25bb888551697873069b4"
|
||||
integrity sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==
|
||||
|
||||
"@schummar/icu-type-parser@1.21.5":
|
||||
version "1.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz#75989085bbbf80ee325874a0137437bde77e9baf"
|
||||
integrity sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==
|
||||
|
||||
"@surma/rollup-plugin-off-main-thread@^2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
|
||||
@@ -2110,6 +2163,11 @@ debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
decimal.js@^10.4.3:
|
||||
version "10.6.0"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
|
||||
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
@@ -3026,6 +3084,16 @@ internal-slot@^1.1.0:
|
||||
hasown "^2.0.2"
|
||||
side-channel "^1.1.0"
|
||||
|
||||
intl-messageformat@^10.5.14:
|
||||
version "10.7.18"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.18.tgz#51a6f387afbca9b0f881b2ec081566db8c540b0d"
|
||||
integrity sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.3.6"
|
||||
"@formatjs/fast-memoize" "2.2.7"
|
||||
"@formatjs/icu-messageformat-parser" "2.11.4"
|
||||
tslib "^2.8.0"
|
||||
|
||||
is-alphabetical@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
|
||||
@@ -3673,6 +3741,20 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
negotiator@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
|
||||
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
|
||||
|
||||
next-intl@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/next-intl/-/next-intl-4.4.0.tgz#f8141153ba8029eddf118ad90fe938e08431a5af"
|
||||
integrity sha512-QHqnP9V9Pe7Tn0PdVQ7u1Z8k9yCkW5SJKeRy2g5gxzhSt/C01y3B9qNxuj3Fsmup/yreIHe6osxU6sFa+9WIkQ==
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher" "^0.5.4"
|
||||
negotiator "^1.0.0"
|
||||
use-intl "^4.4.0"
|
||||
|
||||
next-pwa@^5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4"
|
||||
@@ -4843,7 +4925,7 @@ tsconfig-paths@^3.15.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^2.4.0:
|
||||
tslib@2, tslib@^2.4.0, tslib@^2.8.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
@@ -5017,6 +5099,15 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-intl@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/use-intl/-/use-intl-4.4.0.tgz#f8d05c3835b71aca1249f34291242b30a9c0c6d0"
|
||||
integrity sha512-smFekJWtokDRBLC5/ZumlBREzdXOkw06+56Ifj2uRe9266Mk+yWQm2PcJO+EwlOE5sHIXHixOTzN6V8E0RGUbw==
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize" "^2.2.0"
|
||||
"@schummar/icu-type-parser" "1.21.5"
|
||||
intl-messageformat "^10.5.14"
|
||||
|
||||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
Reference in New Issue
Block a user