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: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

**/.classpath
**/.dockerignore
# **/.env
**/.env
**/.git
**/.gitignore
**/.project
Expand Down
50 changes: 43 additions & 7 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { ReactNode, useState } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';

import Navbar from './components/Navbar';
Expand All @@ -7,28 +7,64 @@ import Home from './pages/Home';
import EventsDashboard from './pages/EventsDashboard';
import Login from './pages/Login';
import SignUp from './pages/SignUp';
import { UserDetails } from './types';

const App: React.FC = () => {
const [isDarkMode, setIsDarkMode] = useState(false); // Dark mode state
const [user, setUser] = useState<Record<string, string> | null>(null);
const [user, setUser] = useState<UserDetails | null>(null);

const toggleDarkMode = () => {
setIsDarkMode((prev) => !prev);
document.body.classList.toggle('dark-mode', !isDarkMode); // Toggle class based on state
};

function checkLogin(component: ReactNode): ReactNode {
return user ? component : <p>You must login to see this page</p>;
}

function checkAWSCreds(component: ReactNode): ReactNode {
if (
user?.aws_access_key?.length &&
user?.aws_region?.length > 0 &&
user?.aws_secret_access_key?.length > 0
) {
return component;
}
return (
<p>
You must enter your AWS credentials in the profile page to see any data
here.
</p>
);
}

return (
<Router>
<Navbar toggleDarkMode={toggleDarkMode} isDarkMode={isDarkMode} username={user?.username ?? null} setUser={setUser} />
<Navbar
toggleDarkMode={toggleDarkMode}
isDarkMode={isDarkMode}
username={user?.username ?? null}
setUser={setUser}
/>
<Routes>
<Route path="/login" element={<Login setUser={setUser} />} />
<Route path="/signup" element={<SignUp />} />
{/* {user !== null && <> */}
<Route path="/" element={<Home isDarkMode={isDarkMode} />} />
<Route path="/profile" element={<Profile isDarkMode={isDarkMode} user={user} />} />

<Route
path="/"
element={checkAWSCreds(checkLogin(<Home isDarkMode={isDarkMode} />))}
/>
<Route
path="/profile"
element={checkLogin(
<Profile isDarkMode={isDarkMode} user={user} setUser={setUser} />
)}
/>
<Route
path="/events-dashboard"
element={<EventsDashboard isDarkMode={isDarkMode} />}
element={checkAWSCreds(
checkLogin(<EventsDashboard isDarkMode={isDarkMode} />)
)}
/>
{/* </>} */}
</Routes>
Expand Down
10 changes: 6 additions & 4 deletions client/src/components/IpAccessCombined.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ export default function IpAccessCombined({

useEffect(() => {
fetch('/events?countOn=source_ip&includeLocation=true')
.then((response) => response.json())
.then((data: (IPLocation & CountedEvent)[] | { err: string }) => {
if (!Object.prototype.hasOwnProperty.call(Object, 'err'))
setIpLocCounts(() => data as (IPLocation & CountedEvent)[]);
.then((response) => {
if (response.ok) return response.json();
throw new Error(response.status + ': ' + response.statusText);
})
.then((data: (IPLocation & CountedEvent)[] | { err: string }) =>
setIpLocCounts(() => data as (IPLocation & CountedEvent)[])
)
.catch((error) =>
console.warn('IpAccessCombined: fetch error: ' + error)
);
Expand Down
34 changes: 22 additions & 12 deletions client/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { NavbarProps } from '../types';
import LOGO from '../assets/RAILGUIDE.png';
//import '../index.scss';

const Navbar: React.FC<NavbarProps> = ({ toggleDarkMode, isDarkMode, username, setUser }) => {
const Navbar: React.FC<NavbarProps> = ({
toggleDarkMode,
isDarkMode,
username,
setUser,
}) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
Expand All @@ -16,12 +21,16 @@ const Navbar: React.FC<NavbarProps> = ({ toggleDarkMode, isDarkMode, username, s
const handleLogout = () => {
console.log('User logged out');
setUser(null);
window.localStorage.removeItem('user');
navigate('/login');
};

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setDropdownOpen(false);
}
};
Expand All @@ -44,16 +53,17 @@ const Navbar: React.FC<NavbarProps> = ({ toggleDarkMode, isDarkMode, username, s
<button onClick={toggleDarkMode} className="nav-button">
{isDarkMode ? 'LIGHT MODE' : 'DARK MODE'}
</button>

<div
className="nav-button"
onClick={toggleDropdown}
aria-haspopup="true"
aria-expanded={dropdownOpen}
>
{username && typeof username === 'string' ? username.toUpperCase() : "USER"}
</div>


<div
className="nav-button"
onClick={toggleDropdown}
aria-haspopup="true"
aria-expanded={dropdownOpen}
>
{username && typeof username === 'string'
? username.toUpperCase()
: 'USER'}
</div>
</div>
{dropdownOpen && (
<div className="dropdown" ref={dropdownRef}>
Expand Down
8 changes: 5 additions & 3 deletions client/src/components/charts/EventSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ export default function EventSourceChart() {
useEffect(() => {
setLoading(true);
fetch('/events?countOn=source')
.then((response) => response.json())
.then((response) => {
if (response.ok) return response.json();
throw new Error(response.status + ': ' + response.statusText);
})
.then((data: CountedEvent[] | { err: string }) => {
if (!Object.prototype.hasOwnProperty.call(Object, 'err'))
setEvents(data as CountedEvent[]);
setEvents(data as CountedEvent[]);
setLoading(false);
})
.catch((error) =>
Expand Down
25 changes: 16 additions & 9 deletions client/src/components/charts/EventType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@ export default function EventTypeChart() {
useEffect(() => {
setLoading(true);
fetch('/events?countOn=type')
.then((response) => response.json())
.then((response) => {
if (response.ok) return response.json();
throw new Error(response.status + ': ' + response.statusText);
})
.then((data: CountedEvent[] | { err: string }) => {
if (!Object.prototype.hasOwnProperty.call(Object, 'err'))
setEvents(
(data as CountedEvent[]).map((event) => ({
...event,
type: event.type.replace(/([A-Z])/g, ' $1'),
}))
);
setEvents(
(data as CountedEvent[]).map((event) => ({
...event,
type: event.type.replace(/([A-Z])/g, ' $1'),
}))
);
setLoading(false);
})
.catch((error) =>
Expand Down Expand Up @@ -64,7 +66,12 @@ export default function EventTypeChart() {
angle={-30}
textAnchor="end"
/>
<Bar dataKey="count" maxBarSize={35} minPointSize={5} style={{ cursor: 'pointer' }}>
<Bar
dataKey="count"
maxBarSize={35}
minPointSize={5}
style={{ cursor: 'pointer' }}
>
{events.map((data, index) => (
<Cell
key={`cell-${index}`}
Expand Down
10 changes: 6 additions & 4 deletions client/src/components/charts/HeatMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ const HeatMap: React.FC = () => {
.catch((error) => console.error('Error fetching geoJSON:', error));

fetch('/events?countOn=source_ip&includeLocation=true')
.then((response) => response.json())
.then((data: (IPLocation & CountedEvent)[] | { err: string }) => {
if (!Object.prototype.hasOwnProperty.call(Object, 'err'))
setIpData(() => data as (IPLocation & CountedEvent)[]);
.then((response) => {
if (response.ok) return response.json();
throw new Error(response.status + ': ' + response.statusText);
})
.then((data: (IPLocation & CountedEvent)[] | { err: string }) =>
setIpData(() => data as (IPLocation & CountedEvent)[])
)
.catch((error) =>
console.warn('Could not fetch event ip counts and locations: ', error)
);
Expand Down
8 changes: 5 additions & 3 deletions client/src/components/charts/IpAccessOverTime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ export default function IpAccessOverTimeChart({
useEffect(() => {
setLoading(true); // Set loading to true before fetching data
fetch('/events?countOn=time&groupTimeBy=minute')
.then((response) => response.json())
.then((response) => {
if (response.ok) return response.json();
throw new Error(response.status + ': ' + response.statusText);
})
.then((data: CountedEvent[] | { err: string }) => {
if (!Object.prototype.hasOwnProperty.call(Object, 'err'))
setIpTimes(() => data as CountedEvent[]);
setIpTimes(() => data as CountedEvent[]);
setLoading(false); // Set loading to true before fetching data
})
.catch((error) =>
Expand Down
26 changes: 19 additions & 7 deletions client/src/components/charts/UserActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import React, { useState, useEffect } from 'react';
import { CartesianGrid, XAxis, YAxis, AreaChart, Area, Tooltip } from 'recharts';
import {
CartesianGrid,
XAxis,
YAxis,
AreaChart,
Area,
Tooltip,
} from 'recharts';
import { SimplifiedEvent } from '../../types';

const UserActivityChart: React.FC = () => {
const [data, setData] = useState<SimplifiedEvent[]>([]);

useEffect(() => {
fetch('/events?countOn=time&groupTimeBy=minute')
.then((response) => response.json())
.then((data: { time: string; count: number }[]) => {
.then((response) => {
if (response.ok) return response.json();
throw new Error(response.status + ': ' + response.statusText);
})
.then((data: { time: string; count: number }[]) =>
setData(
data.map((event) => ({
(data as { time: string; count: number }[]).map((event) => ({
localTime: new Date(event.time).toLocaleString(),
count: event.count,
}))
);
})
.catch((error) => console.warn('Could not fetch event time counts: ', error));
)
)
.catch((error) =>
console.warn('Could not fetch event time counts: ', error)
);
}, []);

// Format for the X-axis to display Mon, Tue, etc.
Expand Down
14 changes: 11 additions & 3 deletions client/src/pages/EventsDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@ const EventsDashboard: React.FC<EventsDashboardProps> = ({ isDarkMode }) => {
const [selectedEvent, setSelectedEvent] = useState<TGEvent | null>(null);
const [events, setEvents] = useState<TGEvent[]>([]);
const [loading, setLoading] = useState(true);
const [error] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
fetch('/events')
.then((response) => response.json())
.then((response) => {
if (response.ok) return response.json();
throw new Error(response.status + ': ' + response.statusText);
})
.then((data: TGEvent[]) => setEvents(() => data))
.catch((error) => console.warn('Could not fetch events: ', error));
.catch((error) => {
if (error === '403: Forbidden')
setError('Please enter AWS Credentials to view events');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

else console.warn('Could not fetch events: ', error);
});

setLoading(false);
}, []);

Expand Down
19 changes: 8 additions & 11 deletions client/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import React, { lazy, useState } from 'react';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm the lazy loading didnt turn out to be a home run. I wonder how many other issues were stemming from it. Maybe there's a better use case for it in larger applications.

import React, { useState } from 'react';
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
} from '@hello-pangea/dnd';
import { CardState } from '../types';

const Card = lazy(() => import('../components/Card'));
const UserActivityChart = lazy(
() => import('../components/charts/UserActivity')
);
const HeatMap = lazy(() => import('../components/charts/HeatMap'));
const IpAccessCombined = lazy(() => import('../components/IpAccessCombined'));
const EventTypeChart = lazy(() => import('../components/charts/EventType'));
const EventSourceChart = lazy(() => import('../components/charts/EventSource'));
const AnomalyChart = lazy(() => import('../components/charts/AnomalyChart'));
import UserActivityChart from '../components/charts/UserActivity';
import EventTypeChart from '../components/charts/EventType';
import EventSourceChart from '../components/charts/EventSource';
import HeatMap from '../components/charts/HeatMap';
import IpAccessCombined from '../components/IpAccessCombined';
import AnomalyChart from '../components/charts/AnomalyChart';
import Card from '../components/Card';

const Home: React.FC<{ isDarkMode: boolean }> = ({ isDarkMode }) => {
// State to track the current IP (null means no IP selected)
Expand Down
Loading