Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ I would like to thank the following members for raising issues and help test/deb
<a href="https://github.com/Navino16"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/22234867?v=4&size=100"><br />Navino16</a>
</td>
</tr>
<tr>
<td align="center" valign="top" width="20%">
<a href="https://github.com/ShadowTox"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/558536?v=4&size=100"><br />ShadowTox</a>
</td>
</tr>
</tbody>
</table>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ export const MinimalCronJobItem = ({
},
...(job.logsEnabled
? [
{
label: t("cronjobs.viewLogs"),
icon: <Code className="h-3 w-3" />,
onClick: () => onViewLogs(job),
},
]
{
label: t("cronjobs.viewLogs"),
icon: <Code className="h-3 w-3" />,
onClick: () => onViewLogs(job),
},
]
: []),
{
label: job.paused
Expand Down Expand Up @@ -139,12 +139,10 @@ export const MinimalCronJobItem = ({
return (
<div
key={job.id}
className={`glass-card p-3 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors ${
isDropdownOpen ? "relative z-10" : ""
}`}
className={`glass-card p-3 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors ${isDropdownOpen ? "relative z-10" : ""
}`}
>
<div className="flex items-center gap-3">
{/* Schedule display - minimal */}
<div className="flex items-center gap-1 flex-shrink-0">
{scheduleDisplayMode === "cron" && (
<code className="text-xs bg-purple-500/10 text-purple-600 dark:text-purple-400 px-1.5 py-0.5 rounded font-mono border border-purple-500/20">
Expand Down
42 changes: 39 additions & 3 deletions app/_components/FeatureComponents/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import { Input } from "@/app/_components/GlobalComponents/FormElements/Input";
Expand All @@ -12,26 +12,44 @@ import {
CardDescription,
CardContent,
} from "@/app/_components/GlobalComponents/Cards/Card";
import { Lock, Eye, EyeOff, Shield, AlertTriangle } from "lucide-react";
import { Lock, Eye, EyeOff, Shield, AlertTriangle, Loader2 } from "lucide-react";

interface LoginFormProps {
hasPassword?: boolean;
hasOIDC?: boolean;
oidcAutoRedirect?: boolean;
version?: string;
}

export const LoginForm = ({
hasPassword = false,
hasOIDC = false,
oidcAutoRedirect = false,
version,
}: LoginFormProps) => {
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [isRedirecting, setIsRedirecting] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations();

useEffect(() => {
const errorParam = searchParams.get("error");

if (errorParam) {
setError(decodeURIComponent(errorParam));
return;
}

if (oidcAutoRedirect && !hasPassword && hasOIDC) {
setIsRedirecting(true);
window.location.href = "/api/oidc/login";
}
}, [oidcAutoRedirect, hasPassword, hasOIDC, searchParams]);

const handlePasswordSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
Expand Down Expand Up @@ -65,6 +83,24 @@ export const LoginForm = ({
window.location.href = "/api/oidc/login";
};

if (isRedirecting) {
return (
<Card className="w-full max-w-md shadow-xl">
<CardContent className="pt-6">
<div className="flex flex-col items-center justify-center py-8 space-y-4">
<Loader2 className="w-12 h-12 text-primary animate-spin" />
<div className="text-center">
<p className="text-lg font-medium">{t("login.redirectingToOIDC")}</p>
<p className="text-sm text-muted-foreground mt-1">
{t("login.pleaseWait")}
</p>
</div>
</div>
</CardContent>
</Card>
);
}

return (
<Card className="w-full max-w-md shadow-xl">
<CardHeader className="text-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface CreateScriptModalProps {
content: string;
};
onFormChange: (updates: Partial<CreateScriptModalProps["form"]>) => void;
isDraft?: boolean;
onClearDraft?: () => void;
}

export const CreateScriptModal = ({
Expand All @@ -23,6 +25,8 @@ export const CreateScriptModal = ({
onSubmit,
form,
onFormChange,
isDraft,
onClearDraft,
}: CreateScriptModalProps) => {
return (
<ScriptModal
Expand All @@ -34,6 +38,8 @@ export const CreateScriptModal = ({
submitButtonIcon={<Plus className="h-4 w-4 mr-2" />}
form={form}
onFormChange={onFormChange}
isDraft={isDraft}
onClearDraft={onClearDraft}
/>
);
}
146 changes: 130 additions & 16 deletions app/_components/FeatureComponents/Modals/LiveLogModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import { useState, useEffect, useRef } from "react";
import { Loader2, CheckCircle2, XCircle } from "lucide-react";
import { Loader2, CheckCircle2, XCircle, AlertTriangle, Minimize2, Maximize2 } from "lucide-react";
import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import { useSSEContext } from "@/app/_contexts/SSEContext";
import { SSEEvent } from "@/app/_utils/sse-events";
import { usePageVisibility } from "@/app/_hooks/usePageVisibility";
Expand All @@ -15,6 +16,9 @@ interface LiveLogModalProps {
jobComment?: string;
}

const MAX_LINES_FULL_RENDER = 10000;
const TAIL_LINES = 5000;

export const LiveLogModal = ({
isOpen,
onClose,
Expand All @@ -27,17 +31,29 @@ export const LiveLogModal = ({
"running"
);
const [exitCode, setExitCode] = useState<number | null>(null);
const [tailMode, setTailMode] = useState<boolean>(false);
const [showSizeWarning, setShowSizeWarning] = useState<boolean>(false);
const logEndRef = useRef<HTMLDivElement>(null);
const { subscribe } = useSSEContext();
const isPageVisible = usePageVisibility();
const lastOffsetRef = useRef<number>(0);
const abortControllerRef = useRef<AbortController | null>(null);
const [fileSize, setFileSize] = useState<number>(0);
const [lineCount, setLineCount] = useState<number>(0);

useEffect(() => {
if (!isOpen || !runId || !isPageVisible) return;
if (isOpen) {
lastOffsetRef.current = 0;
setLogContent("");
setTailMode(false);
setShowSizeWarning(false);
setFileSize(0);
setLineCount(0);
}
}, [isOpen, runId]);

lastOffsetRef.current = 0;
setLogContent("");
useEffect(() => {
if (!isOpen || !runId || !isPageVisible) return;

const fetchLogs = async () => {
if (abortControllerRef.current) {
Expand All @@ -56,12 +72,51 @@ export const LiveLogModal = ({

if (data.fileSize !== undefined) {
lastOffsetRef.current = data.fileSize;
setFileSize(data.fileSize);

if (data.fileSize > 10 * 1024 * 1024 && !showSizeWarning) {
setShowSizeWarning(true);
}
}

if (lastOffsetRef.current === 0 && data.content) {
setLogContent(data.content);
const lines = data.content.split("\n");
setLineCount(lines.length);

if (lines.length > MAX_LINES_FULL_RENDER) {
setTailMode(true);
setShowSizeWarning(true);
setLogContent(lines.slice(-TAIL_LINES).join("\n"));
} else {
setLogContent(data.content);
}
} else if (data.newContent) {
setLogContent((prev) => prev + data.newContent);
setLogContent((prev) => {
const newContent = prev + data.newContent;
const lines = newContent.split("\n");
setLineCount(lines.length);

if (lines.length > MAX_LINES_FULL_RENDER && !tailMode) {
setTailMode(true);
setShowSizeWarning(true);
return lines.slice(-TAIL_LINES).join("\n");
}

if (tailMode && lines.length > TAIL_LINES) {
return lines.slice(-TAIL_LINES).join("\n");
}

const maxLength = 50 * 1024 * 1024;
if (newContent.length > maxLength) {
setTailMode(true);
setShowSizeWarning(true);
const truncated = newContent.slice(-maxLength + 200);
const truncatedLines = truncated.split("\n");
return truncatedLines.slice(-TAIL_LINES).join("\n");
}

return newContent;
});
}

setStatus(data.status || "running");
Expand All @@ -80,7 +135,7 @@ export const LiveLogModal = ({

let interval: NodeJS.Timeout | null = null;
if (isPageVisible) {
interval = setInterval(fetchLogs, 2000);
interval = setInterval(fetchLogs, 3000);
}

return () => {
Expand All @@ -91,7 +146,7 @@ export const LiveLogModal = ({
abortControllerRef.current.abort();
}
};
}, [isOpen, runId, isPageVisible]);
}, [isOpen, runId, isPageVisible, showSizeWarning, tailMode]);

useEffect(() => {
if (!isOpen) return;
Expand All @@ -101,34 +156,64 @@ export const LiveLogModal = ({
setStatus("completed");
setExitCode(event.data.exitCode);

fetch(`/api/logs/stream?runId=${runId}`)
fetch(`/api/logs/stream?runId=${runId}&offset=0`)
.then((res) => res.json())
.then((data) => {
if (data.content) {
setLogContent(data.content);
const lines = data.content.split("\n");
setLineCount(lines.length);
if (tailMode && lines.length > TAIL_LINES) {
setLogContent(lines.slice(-TAIL_LINES).join("\n"));
} else {
setLogContent(data.content);
}
}
});
} else if (event.type === "job-failed" && event.data.runId === runId) {
setStatus("failed");
setExitCode(event.data.exitCode);

fetch(`/api/logs/stream?runId=${runId}`)
fetch(`/api/logs/stream?runId=${runId}&offset=0`)
.then((res) => res.json())
.then((data) => {
if (data.content) {
setLogContent(data.content);
const lines = data.content.split("\n");
setLineCount(lines.length);
if (tailMode && lines.length > TAIL_LINES) {
setLogContent(lines.slice(-TAIL_LINES).join("\n"));
} else {
setLogContent(data.content);
}
}
});
}
});

return unsubscribe;
}, [isOpen, runId, subscribe]);
}, [isOpen, runId, subscribe, tailMode]);

useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
if (logEndRef.current) {
logEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [logContent]);

const toggleTailMode = () => {
setTailMode(!tailMode);
if (!tailMode) {
const lines = logContent.split("\n");
if (lines.length > TAIL_LINES) {
setLogContent(lines.slice(-TAIL_LINES).join("\n"));
}
}
};

const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};

const titleWithStatus = (
<div className="flex items-center gap-3">
<span>Live Job Execution{jobComment && `: ${jobComment}`}</span>
Expand Down Expand Up @@ -162,6 +247,28 @@ export const LiveLogModal = ({
preventCloseOnClickOutside={status === "running"}
>
<div className="space-y-4">
{showSizeWarning && (
<div className="bg-orange-500/10 border border-orange-500/30 rounded-lg p-3 flex items-start gap-3">
<AlertTriangle className="h-4 w-4 text-orange-500 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm text-foreground">
<span className="font-medium">Large log file detected</span> ({formatFileSize(fileSize)})
{tailMode && ` - Tail mode enabled, showing last ${TAIL_LINES.toLocaleString()} lines`}
</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={toggleTailMode}
className="text-orange-500 hover:text-orange-400 hover:bg-orange-500/10 h-auto py-1 px-2 text-xs"
title={tailMode ? "Show all lines" : "Enable tail mode"}
>
{tailMode ? <Maximize2 className="h-3 w-3" /> : <Minimize2 className="h-3 w-3" />}
</Button>
</div>
)}

<div className="bg-black/90 dark:bg-black/60 rounded-lg p-4 max-h-[60vh] overflow-auto">
<pre className="text-xs font-mono text-green-400 whitespace-pre-wrap break-words">
{logContent ||
Expand All @@ -170,8 +277,15 @@ export const LiveLogModal = ({
</pre>
</div>

<div className="text-xs text-muted-foreground">
Run ID: {runId} | Job ID: {jobId}
<div className="flex justify-between items-center text-xs text-muted-foreground">
<span>
Run ID: {runId} | Job ID: {jobId}
</span>
<span>
{lineCount.toLocaleString()} lines
{tailMode && ` (showing last ${TAIL_LINES.toLocaleString()})`}
{fileSize > 0 && ` • ${formatFileSize(fileSize)}`}
</span>
</div>
</div>
</Modal>
Expand Down
Loading