Merge branch 'frontend' of gitea.d3m0k1d.ru:d3m0k1d/HellreigN into HEAD
ci-front / build (push) Successful in 1m59s

This commit is contained in:
nikita
2026-04-05 00:56:55 +03:00
5 changed files with 165 additions and 69 deletions
@@ -1,12 +1,12 @@
import { create } from "zustand"; import { create } from "zustand";
export type LogLevel = "INFO" | "WARNING" | "ERROR" | "FATAL"; export type LogLevel = "info" | "warning" | "error" | "fatal";
interface LogFilterState { interface LogFilterState {
searchQuery: string; searchQuery: string;
startDate: Date | null; startDate: Date | null;
endDate: Date | null; endDate: Date | null;
selectedLogLevels: LogLevel[]; selectedLogLevel: LogLevel | null;
selectedService: string; selectedService: string;
selectedAgent: string; selectedAgent: string;
limit: number; limit: number;
@@ -15,7 +15,7 @@ interface LogFilterState {
setSearchQuery: (query: string) => void; setSearchQuery: (query: string) => void;
setStartDate: (date: Date | null) => void; setStartDate: (date: Date | null) => void;
setEndDate: (date: Date | null) => void; setEndDate: (date: Date | null) => void;
toggleLogLevel: (level: LogLevel) => void; setSelectedLogLevel: (level: LogLevel | null) => void;
setSelectedService: (service: string) => void; setSelectedService: (service: string) => void;
setSelectedAgent: (agent: string) => void; setSelectedAgent: (agent: string) => void;
setLimit: (limit: number) => void; setLimit: (limit: number) => void;
@@ -36,7 +36,7 @@ export const useLogFilterStore = create<LogFilterState>((set, get) => ({
searchQuery: "", searchQuery: "",
startDate: null, startDate: null,
endDate: null, endDate: null,
selectedLogLevels: ["INFO", "WARNING", "ERROR", "FATAL"], selectedLogLevel: null,
selectedService: "", selectedService: "",
selectedAgent: "", selectedAgent: "",
limit: 100, limit: 100,
@@ -45,14 +45,7 @@ export const useLogFilterStore = create<LogFilterState>((set, get) => ({
setSearchQuery: (query) => set({ searchQuery: query }), setSearchQuery: (query) => set({ searchQuery: query }),
setStartDate: (date) => set({ startDate: date }), setStartDate: (date) => set({ startDate: date }),
setEndDate: (date) => set({ endDate: date }), setEndDate: (date) => set({ endDate: date }),
toggleLogLevel: (level) => { setSelectedLogLevel: (level) => set({ selectedLogLevel: level }),
const { selectedLogLevels } = get();
if (selectedLogLevels.includes(level)) {
set({ selectedLogLevels: selectedLogLevels.filter((l) => l !== level) });
} else {
set({ selectedLogLevels: [...selectedLogLevels, level] });
}
},
setSelectedService: (service) => set({ selectedService: service }), setSelectedService: (service) => set({ selectedService: service }),
setSelectedAgent: (agent) => set({ selectedAgent: agent }), setSelectedAgent: (agent) => set({ selectedAgent: agent }),
setLimit: (limit) => set({ limit }), setLimit: (limit) => set({ limit }),
@@ -63,7 +56,7 @@ export const useLogFilterStore = create<LogFilterState>((set, get) => ({
searchQuery: "", searchQuery: "",
startDate: null, startDate: null,
endDate: null, endDate: null,
selectedLogLevels: ["INFO", "WARNING", "ERROR", "FATAL"], selectedLogLevel: null,
selectedService: "", selectedService: "",
selectedAgent: "", selectedAgent: "",
limit: 100, limit: 100,
@@ -72,9 +65,17 @@ export const useLogFilterStore = create<LogFilterState>((set, get) => ({
}, },
getFilters: () => { getFilters: () => {
const { selectedLogLevels, selectedService, selectedAgent, startDate, endDate, limit, offset } = get(); const {
selectedLogLevel,
selectedService,
selectedAgent,
startDate,
endDate,
limit,
offset,
} = get();
return { return {
level: selectedLogLevels.length > 0 ? selectedLogLevels.join(",") : undefined, level: selectedLogLevel || undefined,
service: selectedService || undefined, service: selectedService || undefined,
agent: selectedAgent || undefined, agent: selectedAgent || undefined,
date_from: startDate ? startDate.toISOString() : undefined, date_from: startDate ? startDate.toISOString() : undefined,
@@ -42,11 +42,11 @@ export interface TokenUser {
} }
export interface LogEntry { export interface LogEntry {
agent: string; Agent: string;
level: string; Level: string;
message: string; Message: string;
service: string; Service: string;
timestamp: string; Timestamp: string;
} }
export interface InsertLogRequest { export interface InsertLogRequest {
@@ -62,7 +62,7 @@ export interface InsertLogsRequest {
} }
export interface LogFilters { export interface LogFilters {
level?: string; level?: string | string[];
service?: string; service?: string;
agent?: string; agent?: string;
date_from?: string; date_from?: string;
+82 -24
View File
@@ -13,25 +13,25 @@ const logLevelColors: Record<
LogLevel, LogLevel,
{ bg: string; text: string; border: string } { bg: string; text: string; border: string }
> = { > = {
INFO: { info: {
bg: "var(--info-bg)", bg: "rgba(59, 130, 246, 0.1)",
text: "var(--info-text)", text: "#3b82f6",
border: "var(--info-border)", border: "rgba(59, 130, 246, 0.3)",
}, },
WARNING: { warning: {
bg: "var(--warning-bg)", bg: "rgba(245, 158, 11, 0.1)",
text: "var(--warning-text)", text: "#f59e0b",
border: "var(--warning-border)", border: "rgba(245, 158, 11, 0.3)",
}, },
ERROR: { error: {
bg: "var(--error-bg)", bg: "var(--error-bg)",
text: "var(--error-text)", text: "var(--error-text)",
border: "var(--error-border)", border: "var(--error-border)",
}, },
FATAL: { fatal: {
bg: "var(--fatal-bg)", bg: "rgba(168, 85, 247, 0.1)",
text: "var(--fatal-text)", text: "#a855f7",
border: "var(--fatal-border)", border: "rgba(168, 85, 247, 0.3)",
}, },
}; };
@@ -50,13 +50,13 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
searchQuery, searchQuery,
startDate, startDate,
endDate, endDate,
selectedLogLevels, selectedLogLevel,
selectedService, selectedService,
selectedAgent, selectedAgent,
setSearchQuery, setSearchQuery,
setStartDate, setStartDate,
setEndDate, setEndDate,
toggleLogLevel, setSelectedLogLevel,
setSelectedService, setSelectedService,
setSelectedAgent, setSelectedAgent,
resetFilters, resetFilters,
@@ -67,6 +67,9 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
const [localEndDate, setLocalEndDate] = useState<Date | null>(endDate); const [localEndDate, setLocalEndDate] = useState<Date | null>(endDate);
const [localService, setLocalService] = useState(selectedService); const [localService, setLocalService] = useState(selectedService);
const [localAgent, setLocalAgent] = useState(selectedAgent); const [localAgent, setLocalAgent] = useState(selectedAgent);
const [localLevel, setLocalLevel] = useState<LogLevel | null>(
selectedLogLevel,
);
useEffect(() => { useEffect(() => {
setLocalSearchQuery(searchQuery); setLocalSearchQuery(searchQuery);
@@ -88,10 +91,15 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
setLocalAgent(selectedAgent); setLocalAgent(selectedAgent);
}, [selectedAgent]); }, [selectedAgent]);
useEffect(() => {
setLocalLevel(selectedLogLevel);
}, [selectedLogLevel]);
const handleApply = useCallback(() => { const handleApply = useCallback(() => {
setSearchQuery(localSearchQuery); setSearchQuery(localSearchQuery);
setStartDate(localStartDate); setStartDate(localStartDate);
setEndDate(localEndDate); setEndDate(localEndDate);
setSelectedLogLevel(localLevel);
setSelectedService(localService); setSelectedService(localService);
setSelectedAgent(localAgent); setSelectedAgent(localAgent);
onApply(); onApply();
@@ -99,6 +107,7 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
localSearchQuery, localSearchQuery,
localStartDate, localStartDate,
localEndDate, localEndDate,
localLevel,
localService, localService,
localAgent, localAgent,
onApply, onApply,
@@ -108,6 +117,7 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
setLocalSearchQuery(""); setLocalSearchQuery("");
setLocalStartDate(null); setLocalStartDate(null);
setLocalEndDate(null); setLocalEndDate(null);
setLocalLevel(null);
setLocalService(""); setLocalService("");
setLocalAgent(""); setLocalAgent("");
resetFilters(); resetFilters();
@@ -121,7 +131,7 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
if (endDate) count++; if (endDate) count++;
if (selectedService) count++; if (selectedService) count++;
if (selectedAgent) count++; if (selectedAgent) count++;
if (selectedLogLevels.length < 4) count++; if (selectedLogLevel) count++;
return count; return count;
}; };
@@ -265,21 +275,18 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
className="text-xs font-medium" className="text-xs font-medium"
style={{ color: "var(--text-secondary)" }} style={{ color: "var(--text-secondary)" }}
> >
Уровни логов Уровень логов
</span>
<span className="text-xs" style={{ color: "var(--text-muted)" }}>
({selectedLogLevels.length}/4)
</span> </span>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{(["INFO", "WARNING", "ERROR", "FATAL"] as LogLevel[]).map( {(["info", "warning", "error", "fatal"] as LogLevel[]).map(
(level) => { (level) => {
const isSelected = selectedLogLevels.includes(level); const isSelected = localLevel === level;
const colors = logLevelColors[level]; const colors = logLevelColors[level];
return ( return (
<button <button
key={level} key={level}
onClick={() => toggleLogLevel(level)} onClick={() => setLocalLevel(isSelected ? null : level)}
className="px-3 py-2 rounded-lg text-xs font-medium transition-all border flex-shrink-0" className="px-3 py-2 rounded-lg text-xs font-medium transition-all border flex-shrink-0"
style={{ style={{
backgroundColor: isSelected ? colors.bg : "transparent", backgroundColor: isSelected ? colors.bg : "transparent",
@@ -287,11 +294,29 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
borderColor: isSelected ? colors.border : "var(--border)", borderColor: isSelected ? colors.border : "var(--border)",
minHeight: "36px", minHeight: "36px",
}} }}
onMouseEnter={(e) => {
if (isSelected) {
e.currentTarget.style.backgroundColor = colors.text;
e.currentTarget.style.color = "#fff";
} else {
e.currentTarget.style.backgroundColor =
"rgba(128, 128, 128, 0.08)";
e.currentTarget.style.color = "var(--text-primary)";
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isSelected
? colors.bg
: "transparent";
e.currentTarget.style.color = isSelected
? colors.text
: "var(--text-secondary)";
}}
> >
{isSelected && ( {isSelected && (
<FiCheck size={10} className="inline mr-1" /> <FiCheck size={10} className="inline mr-1" />
)} )}
{level} {level.toUpperCase()}
</button> </button>
); );
}, },
@@ -402,6 +427,39 @@ export const LogFilters: React.FC<LogFiltersProps> = ({
</button> </button>
</div> </div>
)} )}
{selectedLogLevel &&
(() => {
const colors = logLevelColors[selectedLogLevel];
return (
<div
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
style={{
backgroundColor: colors.bg,
borderColor: colors.border,
}}
>
<FiTag size={10} style={{ color: colors.text }} />
<span style={{ color: colors.text }}>
Уровень: {selectedLogLevel.toUpperCase()}
</span>
<button
onClick={() => {
setLocalLevel(null);
setSelectedLogLevel(null);
onApply();
}}
style={{
background: "none",
border: "none",
cursor: "pointer",
color: colors.text,
}}
>
<FiX size={10} />
</button>
</div>
);
})()}
{selectedAgent && ( {selectedAgent && (
<div <div
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs" className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
+34 -24
View File
@@ -15,35 +15,35 @@ import {
} from "react-icons/fi"; } from "react-icons/fi";
const logLevelIcons: Record<string, React.ReactNode> = { const logLevelIcons: Record<string, React.ReactNode> = {
INFO: <FiInfo size={14} />, info: <FiInfo size={14} />,
WARNING: <FiAlertTriangle size={14} />, warning: <FiAlertTriangle size={14} />,
ERROR: <FiAlertCircle size={14} />, error: <FiAlertCircle size={14} />,
FATAL: <FiXOctagon size={14} />, fatal: <FiXOctagon size={14} />,
}; };
const logLevelColors: Record< const logLevelColors: Record<
string, string,
{ bg: string; text: string; border: string } { bg: string; text: string; border: string }
> = { > = {
INFO: { info: {
bg: "var(--info-bg)", bg: "rgba(59, 130, 246, 0.1)",
text: "var(--info-text)", text: "#3b82f6",
border: "var(--info-border)", border: "rgba(59, 130, 246, 0.3)",
}, },
WARNING: { warning: {
bg: "var(--warning-bg)", bg: "rgba(245, 158, 11, 0.1)",
text: "var(--warning-text)", text: "#f59e0b",
border: "var(--warning-border)", border: "rgba(245, 158, 11, 0.3)",
}, },
ERROR: { error: {
bg: "var(--error-bg)", bg: "var(--error-bg)",
text: "var(--error-text)", text: "var(--error-text)",
border: "var(--error-border)", border: "var(--error-border)",
}, },
FATAL: { fatal: {
bg: "var(--fatal-bg)", bg: "rgba(168, 85, 247, 0.1)",
text: "var(--fatal-text)", text: "#a855f7",
border: "var(--fatal-border)", border: "rgba(168, 85, 247, 0.3)",
}, },
}; };
@@ -306,13 +306,13 @@ export const LogsPage: React.FC = () => {
</thead> </thead>
<tbody> <tbody>
{logs.map((log, index) => { {logs.map((log, index) => {
const level = log.level || "INFO"; const level = log.Level?.toLowerCase() || "info";
const colors = const colors =
logLevelColors[level] || logLevelColors.INFO; logLevelColors[level] || logLevelColors.info;
return ( return (
<tr <tr
key={index} key={index}
className="border-t" className="border-t transition-colors"
style={{ style={{
borderColor: "var(--border)", borderColor: "var(--border)",
backgroundColor: backgroundColor:
@@ -320,12 +320,22 @@ export const LogsPage: React.FC = () => {
? "var(--card-bg)" ? "var(--card-bg)"
: "var(--bg-secondary)", : "var(--bg-secondary)",
}} }}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor =
"var(--border)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor =
index % 2 === 0
? "var(--card-bg)"
: "var(--bg-secondary)";
}}
> >
<td <td
className="px-4 py-3 text-sm font-mono whitespace-nowrap" className="px-4 py-3 text-sm font-mono whitespace-nowrap"
style={{ color: "var(--text-secondary)" }} style={{ color: "var(--text-secondary)" }}
> >
{formatTimestamp(log.timestamp)} {formatTimestamp(log.Timestamp)}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span <span
@@ -344,19 +354,19 @@ export const LogsPage: React.FC = () => {
className="px-4 py-3 text-sm" className="px-4 py-3 text-sm"
style={{ color: "var(--text-primary)" }} style={{ color: "var(--text-primary)" }}
> >
{log.service || "—"} {log.Service || "—"}
</td> </td>
<td <td
className="px-4 py-3 text-sm font-mono" className="px-4 py-3 text-sm font-mono"
style={{ color: "var(--text-primary)" }} style={{ color: "var(--text-primary)" }}
> >
{log.agent || "—"} {log.Agent || "—"}
</td> </td>
<td <td
className="px-4 py-3 text-sm" className="px-4 py-3 text-sm"
style={{ color: "var(--text-primary)" }} style={{ color: "var(--text-primary)" }}
> >
{log.message || "—"} {log.Message || "—"}
</td> </td>
</tr> </tr>
); );
+27
View File
@@ -24,6 +24,33 @@ class ApiClient {
validateStatus: (status) => { validateStatus: (status) => {
return status >= 200 && status < 400; return status >= 200 && status < 400;
}, },
// Добавляем кастомный сериализатор параметров
paramsSerializer: {
serialize: (params) => {
const parts: string[] = [];
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) return;
if (Array.isArray(value)) {
// Преобразуем массив в множественные параметры: level=info&level=warning
value.forEach((item) => {
if (item !== undefined && item !== null) {
parts.push(
`${encodeURIComponent(key)}=${encodeURIComponent(item)}`,
);
}
});
} else {
parts.push(
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
);
}
});
return parts.join("&");
},
},
}); });
this.setupInterceptors(); this.setupInterceptors();