mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-05-06 08:46:58 +00:00
Add badge to alerts (#593)
* first pass at alert notifications * sp alerts * add all accounts alerts * fix polling timer * update style * show count and change polling to 60s * check alerts on login * Delete settings.local.json * poll for max 10 results in 30days and use last viewed * make polling interval configurable uses VITE_APP_ALERT_POLL_INTERVAL default is 60s setting to 0 will disable polling * don't sent days and start * add to .env
This commit is contained in:
@@ -34,3 +34,5 @@
|
||||
# VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD=true
|
||||
# hides controlls to add Carrier and Phone number from non Admin/SP Users (also need to set flag on API server to block API calls)
|
||||
#VITE_ADMIN_CARRIER=1
|
||||
# Controls alert polling interval time in seconds, default 60, set to 0 to disable polling
|
||||
#VITE_APP_ALERT_POLL_INTERVAL=60
|
||||
@@ -920,6 +920,19 @@ export const getAlerts = (sid: string, query: Partial<PageQuery>) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getServiceProviderAlerts = (
|
||||
sid: string,
|
||||
query: Partial<PageQuery>,
|
||||
) => {
|
||||
const qryStr = getQuery<Partial<PageQuery>>(query);
|
||||
|
||||
return getFetch<PagedResponse<Alert>>(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/ServiceProviders/${sid}/Alerts?${qryStr}`
|
||||
: `${API_SERVICE_PROVIDERS}/${sid}/Alerts?${qryStr}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getPrice = () => {
|
||||
return getFetch<PriceInfo[]>(API_PRICE);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
setActiveSP,
|
||||
} from "src/store/localStore";
|
||||
import { postServiceProviders } from "src/api";
|
||||
import { useAlertStatus } from "src/hooks/useAlertStatus";
|
||||
|
||||
import type { NaviItem } from "./items";
|
||||
|
||||
@@ -37,9 +38,10 @@ type ItemProps = CommonProps & {
|
||||
item: NaviItem;
|
||||
user?: UserData;
|
||||
lcr?: Lcr;
|
||||
badge?: number;
|
||||
};
|
||||
|
||||
const Item = ({ item, user, lcr, handleMenu }: ItemProps) => {
|
||||
const Item = ({ item, user, lcr, handleMenu, badge }: ItemProps) => {
|
||||
const location = useLocation();
|
||||
const active = location.pathname.includes(item.route(user));
|
||||
|
||||
@@ -52,6 +54,9 @@ const Item = ({ item, user, lcr, handleMenu }: ItemProps) => {
|
||||
>
|
||||
<item.icon />
|
||||
<span>{item.label}</span>
|
||||
{badge !== undefined && badge > 0 && (
|
||||
<span className="navi__badge">{badge >= 10 ? "9+" : badge}</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
@@ -71,10 +76,13 @@ export const Navi = ({
|
||||
const accessControl = useSelectState("accessControl");
|
||||
const serviceProviders = useSelectState("serviceProviders");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const unreadAlerts = useSelectState("unreadAlerts");
|
||||
const [sid, setSid] = useState("");
|
||||
const [modal, setModal] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
|
||||
useAlertStatus();
|
||||
|
||||
const naviByoFiltered = useMemo(() => {
|
||||
return naviByo.filter(
|
||||
(item) => !item.acl || (item.acl && accessControl[item.acl]),
|
||||
@@ -213,6 +221,7 @@ export const Navi = ({
|
||||
user={user}
|
||||
item={item}
|
||||
handleMenu={handleMenu}
|
||||
badge={item.label === "Alerts" ? unreadAlerts : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -129,6 +129,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
margin-left: auto;
|
||||
background-color: ui-vars.$jambonz;
|
||||
color: ui-vars.$white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__byo {
|
||||
margin-top: auto;
|
||||
color: ui-vars.$grey;
|
||||
|
||||
@@ -2,13 +2,17 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { ButtonGroup, H1, M, MS } from "@jambonz/ui-kit";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { getAlerts, useServiceProviderData } from "src/api";
|
||||
import {
|
||||
getAlerts,
|
||||
getServiceProviderAlerts,
|
||||
useServiceProviderData,
|
||||
} from "src/api";
|
||||
import {
|
||||
DATE_SELECTION,
|
||||
PER_PAGE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { useSelectState } from "src/store";
|
||||
import { useSelectState, useDispatch } from "src/store";
|
||||
import { hasLength, hasValue } from "src/utils";
|
||||
import {
|
||||
AccountFilter,
|
||||
@@ -25,13 +29,16 @@ import {
|
||||
getAccountFilter,
|
||||
getQueryFilter,
|
||||
setLocation,
|
||||
setAlertsLastViewed,
|
||||
} from "src/store/localStore";
|
||||
import AlertDetailItem from "./alert-detail-item";
|
||||
import { useToast } from "src/components/toast/toast-provider";
|
||||
|
||||
export const Alerts = () => {
|
||||
const { toastError } = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [dateFilter, setDateFilter] = useState("today");
|
||||
@@ -57,7 +64,18 @@ export const Alerts = () => {
|
||||
: { days: Number(dateFilter) }),
|
||||
};
|
||||
|
||||
getAlerts(accountSid, payload)
|
||||
const fetchAlerts = accountSid
|
||||
? getAlerts(accountSid, payload)
|
||||
: currentServiceProvider?.service_provider_sid
|
||||
? getServiceProviderAlerts(
|
||||
currentServiceProvider.service_provider_sid,
|
||||
payload,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!fetchAlerts) return;
|
||||
|
||||
fetchAlerts
|
||||
.then(({ json }) => {
|
||||
setAlerts(json.data);
|
||||
setAlertsTotal(json.total);
|
||||
@@ -78,16 +96,21 @@ export const Alerts = () => {
|
||||
}
|
||||
}, [accountSid]);
|
||||
|
||||
useEffect(() => {
|
||||
setAlertsLastViewed(new Date().toISOString());
|
||||
dispatch({ type: "unreadAlerts", payload: 0 });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
}
|
||||
|
||||
if (accountSid) {
|
||||
if (accountSid || currentServiceProvider?.service_provider_sid) {
|
||||
handleFilterChange();
|
||||
}
|
||||
}, [user, accountSid, pageNumber, dateFilter]);
|
||||
}, [user, accountSid, pageNumber, dateFilter, currentServiceProvider]);
|
||||
|
||||
/** Reset page number when filters change */
|
||||
useEffect(() => {
|
||||
@@ -104,6 +127,7 @@ export const Alerts = () => {
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
accounts={accounts}
|
||||
defaultOption
|
||||
/>
|
||||
</ScopedAccess>
|
||||
<SelectFilter
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
|
||||
import { getAlerts, getServiceProviderAlerts } from "src/api";
|
||||
import { useSelectState, useDispatch } from "src/store";
|
||||
import { getAlertsLastViewed } from "src/store/localStore";
|
||||
import { Scope } from "src/store/types";
|
||||
|
||||
const DEFAULT_POLL_INTERVAL = 60; // seconds
|
||||
const envPollInterval = import.meta.env.VITE_APP_ALERT_POLL_INTERVAL;
|
||||
const POLL_INTERVAL =
|
||||
envPollInterval !== undefined
|
||||
? Number(envPollInterval) * 1000
|
||||
: DEFAULT_POLL_INTERVAL * 1000;
|
||||
|
||||
export const useAlertStatus = () => {
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchRef = useRef(dispatch);
|
||||
|
||||
useEffect(() => {
|
||||
dispatchRef.current = dispatch;
|
||||
}, [dispatch]);
|
||||
|
||||
const checkAlerts = useCallback(() => {
|
||||
const lastViewed = getAlertsLastViewed();
|
||||
const query = {
|
||||
page: 1,
|
||||
count: 10,
|
||||
...(lastViewed ? { start: lastViewed } : { days: 30 }),
|
||||
};
|
||||
|
||||
// Account-scoped users: check their own account
|
||||
if (user?.access === Scope.account && user.account_sid) {
|
||||
getAlerts(user.account_sid, query)
|
||||
.then(({ json }) => {
|
||||
dispatchRef.current({ type: "unreadAlerts", payload: json.total });
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin/SP users: check all alerts under the service provider
|
||||
if (currentServiceProvider?.service_provider_sid) {
|
||||
getServiceProviderAlerts(
|
||||
currentServiceProvider.service_provider_sid,
|
||||
query,
|
||||
)
|
||||
.then(({ json }) => {
|
||||
dispatchRef.current({ type: "unreadAlerts", payload: json.total });
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}, [
|
||||
user?.access,
|
||||
user?.account_sid,
|
||||
currentServiceProvider?.service_provider_sid,
|
||||
]);
|
||||
|
||||
// Check immediately when user or SP changes
|
||||
useEffect(() => {
|
||||
checkAlerts();
|
||||
}, [checkAlerts]);
|
||||
|
||||
// Set up polling interval (stable, doesn't restart on state changes)
|
||||
const checkAlertsRef = useRef(checkAlerts);
|
||||
|
||||
useEffect(() => {
|
||||
checkAlertsRef.current = checkAlerts;
|
||||
}, [checkAlerts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!POLL_INTERVAL) return;
|
||||
|
||||
const interval = setInterval(() => checkAlertsRef.current(), POLL_INTERVAL);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export const initialState: State = {
|
||||
hasMSTeamsFqdn: false,
|
||||
},
|
||||
serviceProviders: [],
|
||||
unreadAlerts: 0,
|
||||
};
|
||||
|
||||
const reducer: React.Reducer<State, Action<keyof State>> = (state, action) => {
|
||||
@@ -36,6 +37,7 @@ const reducer: React.Reducer<State, Action<keyof State>> = (state, action) => {
|
||||
case "user":
|
||||
case "lcr":
|
||||
case "toast":
|
||||
case "unreadAlerts":
|
||||
return genericAction(state, action);
|
||||
case "serviceProviders":
|
||||
return serviceProvidersAction(state, action);
|
||||
|
||||
@@ -100,6 +100,20 @@ export const removeRootDomain = () => {
|
||||
return localStorage.removeItem(rootDomainKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods to get/set the alerts last viewed timestamp from local storage
|
||||
*/
|
||||
|
||||
const storeAlertsLastViewed = "alertsLastViewed";
|
||||
|
||||
export const getAlertsLastViewed = () => {
|
||||
return localStorage.getItem(storeAlertsLastViewed) || "";
|
||||
};
|
||||
|
||||
export const setAlertsLastViewed = (timestamp: string) => {
|
||||
localStorage.setItem(storeAlertsLastViewed, timestamp);
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods to get/set the location from local storage
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface State {
|
||||
lcr?: Lcr;
|
||||
/** current selected service provider */
|
||||
currentServiceProvider?: ServiceProvider;
|
||||
/** count of unread alerts */
|
||||
unreadAlerts: number;
|
||||
}
|
||||
|
||||
/** Generic interface enforces type-safety with global dispatch */
|
||||
|
||||
Reference in New Issue
Block a user