Introduced a toast notification system to display new alerts and warnings. Updated the handling of alert status by centralizing it in the auth context and removing redundant API calls from individual pages. Improved styling for better user experience with alert notifications.
202 lines
5.9 KiB
JavaScript
202 lines
5.9 KiB
JavaScript
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
import { API_URL } from "./api";
|
|
|
|
const AuthCtx = createContext(null);
|
|
const UI_MODE_KEY = "nexapg_ui_mode";
|
|
|
|
function loadStorage() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem("nexapg_auth") || "null");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function loadUiMode() {
|
|
try {
|
|
const value = localStorage.getItem(UI_MODE_KEY);
|
|
if (value === "easy" || value === "dba") return value;
|
|
} catch {
|
|
// ignore storage errors
|
|
}
|
|
return "dba";
|
|
}
|
|
|
|
export function AuthProvider({ children }) {
|
|
const initial = loadStorage();
|
|
const [tokens, setTokens] = useState(initial?.tokens || null);
|
|
const [me, setMe] = useState(initial?.me || null);
|
|
const [uiMode, setUiModeState] = useState(loadUiMode);
|
|
const [alertStatus, setAlertStatus] = useState({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
|
|
const [alertToasts, setAlertToasts] = useState([]);
|
|
const knownAlertKeysRef = useRef(new Set());
|
|
const hasAlertSnapshotRef = useRef(false);
|
|
|
|
const persist = (nextTokens, nextMe) => {
|
|
if (nextTokens && nextMe) {
|
|
localStorage.setItem("nexapg_auth", JSON.stringify({ tokens: nextTokens, me: nextMe }));
|
|
} else {
|
|
localStorage.removeItem("nexapg_auth");
|
|
}
|
|
};
|
|
|
|
const refresh = async () => {
|
|
if (!tokens?.refreshToken) return null;
|
|
const res = await fetch(`${API_URL}/auth/refresh`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ refresh_token: tokens.refreshToken }),
|
|
});
|
|
if (!res.ok) {
|
|
setTokens(null);
|
|
setMe(null);
|
|
persist(null, null);
|
|
return null;
|
|
}
|
|
const data = await res.json();
|
|
const nextTokens = { accessToken: data.access_token, refreshToken: data.refresh_token };
|
|
setTokens(nextTokens);
|
|
persist(nextTokens, me);
|
|
return nextTokens;
|
|
};
|
|
|
|
const login = async (email, password) => {
|
|
const res = await fetch(`${API_URL}/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
if (!res.ok) throw new Error("Login failed");
|
|
const data = await res.json();
|
|
const nextTokens = { accessToken: data.access_token, refreshToken: data.refresh_token };
|
|
const meRes = await fetch(`${API_URL}/me`, {
|
|
headers: { Authorization: `Bearer ${nextTokens.accessToken}` },
|
|
});
|
|
if (!meRes.ok) throw new Error("Could not load user profile");
|
|
const profile = await meRes.json();
|
|
setTokens(nextTokens);
|
|
setMe(profile);
|
|
persist(nextTokens, profile);
|
|
};
|
|
|
|
const logout = async () => {
|
|
try {
|
|
if (tokens?.accessToken) {
|
|
await fetch(`${API_URL}/auth/logout`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
|
});
|
|
}
|
|
} finally {
|
|
setTokens(null);
|
|
setMe(null);
|
|
persist(null, null);
|
|
}
|
|
};
|
|
|
|
const dismissAlertToast = (toastId) => {
|
|
setAlertToasts((prev) => prev.filter((t) => t.id !== toastId));
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!tokens?.accessToken) {
|
|
setAlertStatus({ warnings: [], alerts: [], warning_count: 0, alert_count: 0 });
|
|
setAlertToasts([]);
|
|
knownAlertKeysRef.current = new Set();
|
|
hasAlertSnapshotRef.current = false;
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
|
|
const pushToastsForNewItems = (items) => {
|
|
if (!items.length) return;
|
|
const createdAt = Date.now();
|
|
const nextToasts = items.slice(0, 4).map((item, idx) => ({
|
|
id: `${createdAt}-${idx}-${item.alert_key}`,
|
|
severity: item.severity,
|
|
title: item.name,
|
|
target: item.target_name,
|
|
message: item.message,
|
|
}));
|
|
setAlertToasts((prev) => [...nextToasts, ...prev].slice(0, 6));
|
|
for (const toast of nextToasts) {
|
|
setTimeout(() => {
|
|
if (!mounted) return;
|
|
dismissAlertToast(toast.id);
|
|
}, 8000);
|
|
}
|
|
};
|
|
|
|
const loadAlertStatus = async () => {
|
|
const request = async (accessToken) =>
|
|
fetch(`${API_URL}/alerts/status`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
let res = await request(tokens.accessToken);
|
|
if (res.status === 401 && tokens.refreshToken) {
|
|
const refreshed = await refresh();
|
|
if (refreshed?.accessToken) {
|
|
res = await request(refreshed.accessToken);
|
|
}
|
|
}
|
|
if (!res.ok) return;
|
|
|
|
const payload = await res.json();
|
|
if (!mounted) return;
|
|
setAlertStatus(payload);
|
|
|
|
const currentItems = [...(payload.warnings || []), ...(payload.alerts || [])];
|
|
const currentKeys = new Set(currentItems.map((item) => item.alert_key));
|
|
if (!hasAlertSnapshotRef.current) {
|
|
knownAlertKeysRef.current = currentKeys;
|
|
hasAlertSnapshotRef.current = true;
|
|
return;
|
|
}
|
|
const newItems = currentItems.filter((item) => !knownAlertKeysRef.current.has(item.alert_key));
|
|
knownAlertKeysRef.current = currentKeys;
|
|
pushToastsForNewItems(newItems);
|
|
};
|
|
|
|
loadAlertStatus().catch(() => {});
|
|
const timer = setInterval(() => {
|
|
loadAlertStatus().catch(() => {});
|
|
}, 8000);
|
|
|
|
return () => {
|
|
mounted = false;
|
|
clearInterval(timer);
|
|
};
|
|
}, [tokens?.accessToken, tokens?.refreshToken]);
|
|
|
|
const setUiMode = (nextMode) => {
|
|
const mode = nextMode === "easy" ? "easy" : "dba";
|
|
setUiModeState(mode);
|
|
localStorage.setItem(UI_MODE_KEY, mode);
|
|
};
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
tokens,
|
|
me,
|
|
login,
|
|
logout,
|
|
refresh,
|
|
uiMode,
|
|
setUiMode,
|
|
alertStatus,
|
|
alertToasts,
|
|
dismissAlertToast,
|
|
}),
|
|
[tokens, me, uiMode, alertStatus, alertToasts]
|
|
);
|
|
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
|
}
|
|
|
|
export function useAuth() {
|
|
const ctx = useContext(AuthCtx);
|
|
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
|
|
return ctx;
|
|
}
|