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
2 changes: 2 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import getStats from './routes/getStats';
import logtypeStatus from './routes/logTypeStatus';
import connectToDatabase from './db/connect';
import severityInfo from './routes/severityInfo';
import getLogAnalytics from './routes/getLogAnalytics';

dotenv.config();

Expand All @@ -30,6 +31,7 @@ app.use('/api', allLogs);
app.use('/api', getStats)
app.use('/api', logtypeStatus)
app.use('/api', severityInfo)
app.use('/api', getLogAnalytics)

app.listen(PORT, (): void => {
console.log(`Server is running on http://localhost:${PORT}`);
Expand Down
14 changes: 10 additions & 4 deletions backend/src/models/LinuxLogModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,30 @@ interface LinuxLogModelType {
processId?: number | string;
userId?: string;
rawLine: string;
analyzed?: boolean;
uploadDate?: Date;
users?: string;
}

const LinuxLogSchema = new Schema<LinuxLogModelType & Document>({
logType: {
type: String,
logType: {
type: String,
validate: {
validator: function(v: string) {
validator: function (v: string) {
return ["SYSLOG", "AUTH", "KERNEL", "APPLICATION", "UNKNOWN"].includes(v) || v.startsWith("UNKNOWN-");
}
},
required: true
required: true
},
timestamp: { type: Date, required: true },
severity: { type: String, enum: ["INFO", "WARNING", "ERROR", "CRITICAL"], required: true },
eventId: { type: String, required: true },
message: { type: String, required: true },
processId: { type: Schema.Types.Mixed },
userId: { type: String },
analyzed: { type: Boolean, default: false },
uploadDate: { type: Date, default: Date.now },
users: { type: String },
rawLine: { type: String, required: true },
});

Expand Down
37 changes: 37 additions & 0 deletions backend/src/routes/getLogAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import express from 'express';
import { Request, Response } from 'express';
import { LinuxLogModel } from '../models/LinuxLogModel';

const router = express.Router();

router.get('/getLogAnalytics', async (req: Request, res: Response) => {
try {
const logs = await LinuxLogModel.find({}).sort({ timestamp: -1 }).limit(1000); // Fetch the latest 10 logs

const rows = logs.map(log => ({
id: log._id,
timeStamp: log.timestamp.toUTCString(),
severity: log.severity,
users: log.users ? log.users : "N/A",
rawLine: log.rawLine,
message: log.message,
uploadDate: log.uploadDate ? log.uploadDate.toISOString() : null,
analyzed: log.analyzed,
EventId: log.eventId,
logType: log.logType,
}));

res.json(rows);

} catch (error) {
console.error(error);
res.status(500).json({ error: "Something went wrong." });
}
});



export default router;



2 changes: 1 addition & 1 deletion frontend/src/components/Analytics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function Analytics() {
return (
<Box sx={{ width: "100%", maxWidth: { sm: "100%", md: "1700px" } }}>
<Typography component="h2" variant="h6" sx={{ mb: 2 }}>
Details
Total Log Analytics Details
</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 12, lg: 12 }}>
Expand Down
121 changes: 85 additions & 36 deletions frontend/src/components/CustomizedDataGrid.jsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,97 @@
import * as React from "react";
import { DataGrid } from "@mui/x-data-grid";
import { columns, rows } from "./internals/data/gridData";
import { columns } from "./internals/data/gridData";
import fetchAllLogs from "../utils/api/fetchLogAnalytics";
import LogDetailsDialog from "./LogDetailsDialog";

export default function CustomizedDataGrid() {
return (
<DataGrid
autoSN
rows={rows}
columns={columns}
getRowClassName={(params) =>
params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd"
const [rows, setRows] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
// State to track the currently selected row for the overlay
const [selectedRow, setSelectedRow] = React.useState(null);

React.useEffect(() => {
const fetchData = async () => {
try {
const data = await fetchAllLogs();
if (!data) {
throw new Error("No data found");
}
setRows(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
initialState={{
pagination: { paginationModel: { pageSize: 20 } },
}}
pageSizeOptions={[10, 20, 50]}
// disableColumnResize
density="compact"
slotProps={{
filterPanel: {
filterFormProps: {
logicOperatorInputProps: {
variant: "outlined",
size: "small",
},
columnInputProps: {
variant: "outlined",
size: "small",
sx: { mt: "auto" },
},
operatorInputProps: {
variant: "outlined",
size: "small",
sx: { mt: "auto" },
},
valueInputProps: {
InputComponentProps: {
};
fetchData();
}, []);

// Handle row click event to open the overlay with details
const handleRowClick = (params) => {
setSelectedRow(params.row);
};

// Close the overlay
const handleCloseOverlay = () => {
setSelectedRow(null);
};

if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}

return (
<>
<DataGrid
rows={rows}
columns={columns}
getRowClassName={(params) =>
params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd"
}
initialState={{
pagination: { paginationModel: { pageSize: 20 } },
}}
getRowId={(row) => row.id}
pageSizeOptions={[10, 20, 50]}
density="compact"
onRowClick={handleRowClick}
slotProps={{
filterPanel: {
filterFormProps: {
logicOperatorInputProps: {
variant: "outlined",
size: "small",
},
columnInputProps: {
variant: "outlined",
size: "small",
sx: { mt: "auto" },
},
operatorInputProps: {
variant: "outlined",
size: "small",
sx: { mt: "auto" },
},
valueInputProps: {
InputComponentProps: {
variant: "outlined",
size: "small",
},
},
},
},
},
}}
/>
}}
/>

<LogDetailsDialog
selectedRow={selectedRow}
onClose={handleCloseOverlay}
/>
</>
);
}
3 changes: 3 additions & 0 deletions frontend/src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Chat from "./Chat";
import Analytics from "./Analytics";
import { useRecoilValue } from "recoil";
import { activeViewState } from "../utils/state";
import Upload from "./Upload";

const xThemeComponents = {
...chartsCustomizations,
Expand All @@ -37,6 +38,8 @@ export default function Dashboard(props) {
return <Analytics />;
case "chat":
return <Chat />;
case "upload":
return <Upload />;
default:
return <MainGrid />;
}
Expand Down
146 changes: 146 additions & 0 deletions frontend/src/components/LogDetailsDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import * as React from "react";
import {
Dialog,
DialogTitle,
DialogContent,
IconButton,
Button,
Grid,
Typography,
Box,
Paper,
Stack,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import ChatIcon from "@mui/icons-material/Chat";

export default function LogDetailsDialog({ selectedRow, onClose }) {
if (!selectedRow) return null;

// Helper function to format values based on type
const formatValue = (key, value) => {
if (value === null || value === undefined) {
return (
<Typography
variant="body2"
sx={{ color: "text.secondary", fontStyle: "italic" }}
>
None
</Typography>
);
} else if (typeof value === "object") {
return (
<Box
component="pre"
sx={{
backgroundColor: (theme) => theme.palette.grey[100],
p: 2,
borderRadius: 1,
overflow: "auto",
maxHeight: 200,
fontSize: "0.875rem",
}}
>
{JSON.stringify(value, null, 2)}
</Box>
);
} else if (typeof value === "boolean") {
return <Typography variant="body2">{value ? "Yes" : "No"}</Typography>;
} else if (
key.toLowerCase().includes("time") ||
key.toLowerCase().includes("date")
) {
try {
return (
<Typography variant="body2">
{new Date(value).toLocaleString()}
</Typography>
);
} catch {
return <Typography variant="body2">{value}</Typography>;
}
}
return <Typography variant="body2">{value}</Typography>;
};

// Convert key strings to a more readable format
const formatLabel = (key) =>
key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());

// Prepare entries from the selected row
const entries = Object.entries(selectedRow).map(([key, value]) => ({
key,
value,
isWide: typeof value === "object",
}));

return (
<Dialog
open={Boolean(selectedRow)}
onClose={onClose}
fullWidth
maxWidth="md"
PaperProps={{ sx: { borderRadius: 2 } }}
>
<DialogTitle
sx={{
bgcolor: "primary.light",
color: "white",
py: 2,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Log Details #{selectedRow.id}
</Typography>
{/* Conditionally render the Chat button if analyzed is false */}
{selectedRow.analyzed === false && (
<Button
variant="contained"
color="secondary"
startIcon={<ChatIcon />}
sx={{ mr: 2 }}
>
Chat
</Button>
)}
<IconButton
aria-label="close"
onClick={onClose}
sx={{ color: "white" }}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers sx={{ p: 3 }}>
<Grid container spacing={2}>
{entries.map(({ key, value, isWide }) => (
<Grid item xs={12} md={isWide ? 12 : 6} key={key}>
<Paper
variant="outlined"
sx={{
p: 2,
borderRadius: 1,
boxShadow: (theme) => theme.shadows[1],
height: "100%",
}}
>
<Stack spacing={1}>
<Typography
variant="subtitle2"
sx={{ color: "text.secondary" }}
>
{formatLabel(key)}
</Typography>
{formatValue(key, value)}
</Stack>
</Paper>
</Grid>
))}
</Grid>
</DialogContent>
</Dialog>
);
}
Loading