@@ -0,0 +1,86 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
export type LogLevel = "INFO" | "WARNING" | "ERROR" | "FATAL";
|
||||
|
||||
interface LogFilterState {
|
||||
searchQuery: string;
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
selectedLogLevels: LogLevel[];
|
||||
selectedService: string;
|
||||
selectedAgent: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
|
||||
setSearchQuery: (query: string) => void;
|
||||
setStartDate: (date: Date | null) => void;
|
||||
setEndDate: (date: Date | null) => void;
|
||||
toggleLogLevel: (level: LogLevel) => void;
|
||||
setSelectedService: (service: string) => void;
|
||||
setSelectedAgent: (agent: string) => void;
|
||||
setLimit: (limit: number) => void;
|
||||
setOffset: (offset: number) => void;
|
||||
resetFilters: () => void;
|
||||
getFilters: () => {
|
||||
level?: string;
|
||||
service?: string;
|
||||
agent?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const useLogFilterStore = create<LogFilterState>((set, get) => ({
|
||||
searchQuery: "",
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
selectedLogLevels: ["INFO", "WARNING", "ERROR", "FATAL"],
|
||||
selectedService: "",
|
||||
selectedAgent: "",
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
|
||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||
setStartDate: (date) => set({ startDate: date }),
|
||||
setEndDate: (date) => set({ endDate: date }),
|
||||
toggleLogLevel: (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 }),
|
||||
setSelectedAgent: (agent) => set({ selectedAgent: agent }),
|
||||
setLimit: (limit) => set({ limit }),
|
||||
setOffset: (offset) => set({ offset }),
|
||||
|
||||
resetFilters: () => {
|
||||
set({
|
||||
searchQuery: "",
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
selectedLogLevels: ["INFO", "WARNING", "ERROR", "FATAL"],
|
||||
selectedService: "",
|
||||
selectedAgent: "",
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
});
|
||||
},
|
||||
|
||||
getFilters: () => {
|
||||
const { selectedLogLevels, selectedService, selectedAgent, startDate, endDate, limit, offset } = get();
|
||||
return {
|
||||
level: selectedLogLevels.length > 0 ? selectedLogLevels.join(",") : undefined,
|
||||
service: selectedService || undefined,
|
||||
agent: selectedAgent || undefined,
|
||||
date_from: startDate ? startDate.toISOString() : undefined,
|
||||
date_to: endDate ? endDate.toISOString() : undefined,
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,399 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
FiSearch,
|
||||
FiX,
|
||||
FiFilter,
|
||||
FiCalendar,
|
||||
FiTag,
|
||||
FiCheck,
|
||||
} from "react-icons/fi";
|
||||
import { useLogFilterStore, type LogLevel } from "../store/logFilter.store";
|
||||
|
||||
const logLevelColors: Record<LogLevel, { bg: string; text: string; border: string }> = {
|
||||
INFO: { bg: "var(--info-bg)", text: "var(--info-text)", border: "var(--info-border)" },
|
||||
WARNING: { bg: "var(--warning-bg)", text: "var(--warning-text)", border: "var(--warning-border)" },
|
||||
ERROR: { bg: "var(--error-bg)", text: "var(--error-text)", border: "var(--error-border)" },
|
||||
FATAL: { bg: "var(--fatal-bg)", text: "var(--fatal-text)", border: "var(--fatal-border)" },
|
||||
};
|
||||
|
||||
interface LogFiltersProps {
|
||||
onApply: () => void;
|
||||
availableServices: string[];
|
||||
availableAgents: string[];
|
||||
}
|
||||
|
||||
export const LogFilters: React.FC<LogFiltersProps> = ({ onApply, availableServices, availableAgents }) => {
|
||||
const {
|
||||
searchQuery,
|
||||
startDate,
|
||||
endDate,
|
||||
selectedLogLevels,
|
||||
selectedService,
|
||||
selectedAgent,
|
||||
setSearchQuery,
|
||||
setStartDate,
|
||||
setEndDate,
|
||||
toggleLogLevel,
|
||||
setSelectedService,
|
||||
setSelectedAgent,
|
||||
resetFilters,
|
||||
} = useLogFilterStore();
|
||||
|
||||
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
|
||||
const [localStartDate, setLocalStartDate] = useState<Date | null>(startDate);
|
||||
const [localEndDate, setLocalEndDate] = useState<Date | null>(endDate);
|
||||
const [localService, setLocalService] = useState(selectedService);
|
||||
const [localAgent, setLocalAgent] = useState(selectedAgent);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSearchQuery(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStartDate(startDate);
|
||||
}, [startDate]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalEndDate(endDate);
|
||||
}, [endDate]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalService(selectedService);
|
||||
}, [selectedService]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalAgent(selectedAgent);
|
||||
}, [selectedAgent]);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
setSearchQuery(localSearchQuery);
|
||||
setStartDate(localStartDate);
|
||||
setEndDate(localEndDate);
|
||||
setSelectedService(localService);
|
||||
setSelectedAgent(localAgent);
|
||||
onApply();
|
||||
}, [localSearchQuery, localStartDate, localEndDate, localService, localAgent, onApply]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setLocalSearchQuery("");
|
||||
setLocalStartDate(null);
|
||||
setLocalEndDate(null);
|
||||
setLocalService("");
|
||||
setLocalAgent("");
|
||||
resetFilters();
|
||||
onApply();
|
||||
}, [resetFilters, onApply]);
|
||||
|
||||
const getActiveFiltersCount = () => {
|
||||
let count = 0;
|
||||
if (searchQuery) count++;
|
||||
if (startDate) count++;
|
||||
if (endDate) count++;
|
||||
if (selectedService) count++;
|
||||
if (selectedAgent) count++;
|
||||
if (selectedLogLevels.length < 4) count++;
|
||||
return count;
|
||||
};
|
||||
|
||||
const formatDate = (date: Date | null) => {
|
||||
if (!date) return null;
|
||||
return date.toLocaleDateString("ru-RU");
|
||||
};
|
||||
|
||||
const activeFiltersCount = getActiveFiltersCount();
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
padding: "8px 12px",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "var(--input-bg)",
|
||||
color: "var(--text-primary)",
|
||||
fontSize: "13px",
|
||||
};
|
||||
|
||||
const selectStyle: React.CSSProperties = {
|
||||
...inputStyle,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl border"
|
||||
style={{
|
||||
backgroundColor: "var(--card-bg)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<div className="p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<FiFilter size={14} style={{ color: "var(--accent)" }} />
|
||||
<h3 className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>
|
||||
Фильтры логов
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>
|
||||
Активно: {activeFiltersCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Filters Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<FiSearch
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "10px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
color: "var(--text-muted)",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={localSearchQuery}
|
||||
onChange={(e) => setLocalSearchQuery(e.target.value)}
|
||||
placeholder="Поиск по сообщению..."
|
||||
style={{ ...inputStyle, paddingLeft: "32px" }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleApply()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Service Select */}
|
||||
<select
|
||||
value={localService}
|
||||
onChange={(e) => setLocalService(e.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Все сервисы</option>
|
||||
{availableServices.map((service) => (
|
||||
<option key={service} value={service}>
|
||||
{service}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Agent Select */}
|
||||
<select
|
||||
value={localAgent}
|
||||
onChange={(e) => setLocalAgent(e.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Все агенты</option>
|
||||
{availableAgents.map((agent) => (
|
||||
<option key={agent} value={agent}>
|
||||
{agent}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Date Range */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={localStartDate ? localStartDate.toISOString().split("T")[0] : ""}
|
||||
onChange={(e) => setLocalStartDate(e.target.value ? new Date(e.target.value) : null)}
|
||||
style={inputStyle}
|
||||
placeholder="Дата от"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={localEndDate ? localEndDate.toISOString().split("T")[0] : ""}
|
||||
onChange={(e) => setLocalEndDate(e.target.value ? new Date(e.target.value) : null)}
|
||||
style={inputStyle}
|
||||
placeholder="Дата до"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log Levels */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FiTag size={12} style={{ color: "var(--text-secondary)" }} />
|
||||
<span className="text-xs font-medium" style={{ color: "var(--text-secondary)" }}>
|
||||
Уровни логов
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: "var(--text-muted)" }}>
|
||||
({selectedLogLevels.length}/4)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["INFO", "WARNING", "ERROR", "FATAL"] as LogLevel[]).map((level) => {
|
||||
const isSelected = selectedLogLevels.includes(level);
|
||||
const colors = logLevelColors[level];
|
||||
return (
|
||||
<button
|
||||
key={level}
|
||||
onClick={() => toggleLogLevel(level)}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all border"
|
||||
style={{
|
||||
backgroundColor: isSelected ? colors.bg : "transparent",
|
||||
color: isSelected ? colors.text : "var(--text-secondary)",
|
||||
borderColor: isSelected ? colors.border : "var(--border)",
|
||||
}}
|
||||
>
|
||||
{isSelected && <FiCheck size={10} className="inline mr-1" />}
|
||||
{level}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleApply}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg transition-all text-sm font-medium"
|
||||
style={{
|
||||
backgroundColor: "var(--button-primary)",
|
||||
color: "var(--button-primary-text)",
|
||||
}}
|
||||
>
|
||||
<FiCheck size={14} />
|
||||
Применить
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-4 py-2 rounded-lg transition-all text-sm font-medium border"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--text-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<FiX size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active Filters Display */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border)" }}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FiFilter size={10} style={{ color: "var(--accent)" }} />
|
||||
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>
|
||||
Активные фильтры:
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{searchQuery && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<FiSearch size={10} />
|
||||
<span style={{ color: "var(--text-primary)" }}>Поиск: {searchQuery}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocalSearchQuery("");
|
||||
setSearchQuery("");
|
||||
onApply();
|
||||
}}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||
>
|
||||
<FiX size={10} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedService && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<FiTag size={10} />
|
||||
<span style={{ color: "var(--text-primary)" }}>Сервис: {selectedService}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocalService("");
|
||||
setSelectedService("");
|
||||
onApply();
|
||||
}}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||
>
|
||||
<FiX size={10} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedAgent && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<FiTag size={10} />
|
||||
<span style={{ color: "var(--text-primary)" }}>Агент: {selectedAgent}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocalAgent("");
|
||||
setSelectedAgent("");
|
||||
onApply();
|
||||
}}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||
>
|
||||
<FiX size={10} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{startDate && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<FiCalendar size={10} />
|
||||
<span style={{ color: "var(--text-primary)" }}>С: {formatDate(startDate)}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocalStartDate(null);
|
||||
setStartDate(null);
|
||||
onApply();
|
||||
}}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||
>
|
||||
<FiX size={10} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{endDate && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderColor: "var(--border)",
|
||||
}}
|
||||
>
|
||||
<FiCalendar size={10} />
|
||||
<span style={{ color: "var(--text-primary)" }}>По: {formatDate(endDate)}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocalEndDate(null);
|
||||
setEndDate(null);
|
||||
onApply();
|
||||
}}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||
>
|
||||
<FiX size={10} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user