mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-13 02:51:27 +08:00

* feat: build overlay scrollbar component for Virtuoso elements * feat: apply overlay scroll to Virtuoso components * feat: build overlay scrollbar component for normal scrollable sections * feat: apply overlay scrollbar to normal scrollable sections * feat: add dark mode UI support to overlay scrollbars * chore: rename OverlayScrollbar to OverlayScrollbarForTypicalChildren * chore: move inline style to scss file * chore: rename VirtuosoOverlayScrollbar to OverlayScrollbarForVirtuosoChildren * chore: move OverlayScrollbarForTypicalChildren to components folder * chore: create a common component for handling Virtuoso and Typical scroll sections * chore: rename Virtuoso and Typical Overlay Scrollbar components * fix: fix the overlay scrollbar initialization flickering * fix: remove calculated height from typical overlay scrollbar + remove the explicit height: 100%
343 lines
9.1 KiB
TypeScript
343 lines
9.1 KiB
TypeScript
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
|
import './AppLayout.styles.scss';
|
|
|
|
import * as Sentry from '@sentry/react';
|
|
import { Flex } from 'antd';
|
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
|
import getUserLatestVersion from 'api/user/getLatestVersion';
|
|
import getUserVersion from 'api/user/getVersion';
|
|
import cx from 'classnames';
|
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
|
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
|
import ROUTES from 'constants/routes';
|
|
import SideNav from 'container/SideNav';
|
|
import TopNav from 'container/TopNav';
|
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
import useLicense from 'hooks/useLicense';
|
|
import { useNotifications } from 'hooks/useNotifications';
|
|
import history from 'lib/history';
|
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
|
import {
|
|
ReactNode,
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { Helmet } from 'react-helmet-async';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useQueries } from 'react-query';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { useLocation } from 'react-router-dom';
|
|
import { Dispatch } from 'redux';
|
|
import { sideBarCollapse } from 'store/actions';
|
|
import { AppState } from 'store/reducers';
|
|
import AppActions from 'types/actions';
|
|
import {
|
|
UPDATE_CURRENT_ERROR,
|
|
UPDATE_CURRENT_VERSION,
|
|
UPDATE_LATEST_VERSION,
|
|
UPDATE_LATEST_VERSION_ERROR,
|
|
} from 'types/actions/app';
|
|
import AppReducer from 'types/reducer/app';
|
|
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
|
|
|
import { ChildrenContainer, Layout, LayoutContent } from './styles';
|
|
import { getRouteKey } from './utils';
|
|
|
|
function AppLayout(props: AppLayoutProps): JSX.Element {
|
|
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
|
|
(state) => state.app,
|
|
);
|
|
|
|
const [collapsed, setCollapsed] = useState<boolean>(
|
|
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
|
);
|
|
|
|
const isDarkMode = useIsDarkMode();
|
|
|
|
const { data: licenseData, isFetching } = useLicense();
|
|
|
|
const { pathname } = useLocation();
|
|
const { t } = useTranslation(['titles']);
|
|
|
|
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
|
{
|
|
queryFn: getUserVersion,
|
|
queryKey: ['getUserVersion', user?.accessJwt],
|
|
enabled: isLoggedIn,
|
|
},
|
|
{
|
|
queryFn: getUserLatestVersion,
|
|
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
|
enabled: isLoggedIn,
|
|
},
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (getUserLatestVersionResponse.status === 'idle' && isLoggedIn) {
|
|
getUserLatestVersionResponse.refetch();
|
|
}
|
|
|
|
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
|
getUserVersionResponse.refetch();
|
|
}
|
|
}, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]);
|
|
|
|
const { children } = props;
|
|
|
|
const dispatch = useDispatch<Dispatch<AppActions | any>>();
|
|
|
|
const latestCurrentCounter = useRef(0);
|
|
const latestVersionCounter = useRef(0);
|
|
|
|
const { notifications } = useNotifications();
|
|
|
|
const onCollapse = useCallback(() => {
|
|
setCollapsed((collapsed) => !collapsed);
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
dispatch(sideBarCollapse(collapsed));
|
|
}, [collapsed, dispatch]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
getUserLatestVersionResponse.isFetched &&
|
|
getUserLatestVersionResponse.isError &&
|
|
latestCurrentCounter.current === 0
|
|
) {
|
|
latestCurrentCounter.current = 1;
|
|
|
|
dispatch({
|
|
type: UPDATE_LATEST_VERSION_ERROR,
|
|
payload: {
|
|
isError: true,
|
|
},
|
|
});
|
|
notifications.error({
|
|
message: t('oops_something_went_wrong_version'),
|
|
});
|
|
}
|
|
|
|
if (
|
|
getUserVersionResponse.isFetched &&
|
|
getUserVersionResponse.isError &&
|
|
latestVersionCounter.current === 0
|
|
) {
|
|
latestVersionCounter.current = 1;
|
|
|
|
dispatch({
|
|
type: UPDATE_CURRENT_ERROR,
|
|
payload: {
|
|
isError: true,
|
|
},
|
|
});
|
|
notifications.error({
|
|
message: t('oops_something_went_wrong_version'),
|
|
});
|
|
}
|
|
|
|
if (
|
|
getUserVersionResponse.isFetched &&
|
|
getUserLatestVersionResponse.isSuccess &&
|
|
getUserVersionResponse.data &&
|
|
getUserVersionResponse.data.payload
|
|
) {
|
|
dispatch({
|
|
type: UPDATE_CURRENT_VERSION,
|
|
payload: {
|
|
currentVersion: getUserVersionResponse.data.payload.version,
|
|
ee: getUserVersionResponse.data.payload.ee,
|
|
setupCompleted: getUserVersionResponse.data.payload.setupCompleted,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (
|
|
getUserLatestVersionResponse.isFetched &&
|
|
getUserLatestVersionResponse.isSuccess &&
|
|
getUserLatestVersionResponse.data &&
|
|
getUserLatestVersionResponse.data.payload
|
|
) {
|
|
dispatch({
|
|
type: UPDATE_LATEST_VERSION,
|
|
payload: {
|
|
latestVersion: getUserLatestVersionResponse.data.payload.tag_name,
|
|
},
|
|
});
|
|
}
|
|
}, [
|
|
dispatch,
|
|
isLoggedIn,
|
|
pathname,
|
|
t,
|
|
getUserLatestVersionResponse.isLoading,
|
|
getUserLatestVersionResponse.isError,
|
|
getUserLatestVersionResponse.data,
|
|
getUserVersionResponse.isLoading,
|
|
getUserVersionResponse.isError,
|
|
getUserVersionResponse.data,
|
|
getUserLatestVersionResponse.isFetched,
|
|
getUserVersionResponse.isFetched,
|
|
getUserLatestVersionResponse.isSuccess,
|
|
notifications,
|
|
]);
|
|
|
|
const isToDisplayLayout = isLoggedIn;
|
|
|
|
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
|
const pageTitle = t(routeKey);
|
|
const renderFullScreen =
|
|
pathname === ROUTES.GET_STARTED ||
|
|
pathname === ROUTES.WORKSPACE_LOCKED ||
|
|
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
|
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
|
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
|
pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
|
|
pathname === ROUTES.GET_STARTED_AZURE_MONITORING;
|
|
|
|
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!isFetching &&
|
|
licenseData?.payload?.onTrial &&
|
|
!licenseData?.payload?.trialConvertedToSubscription &&
|
|
!licenseData?.payload?.workSpaceBlock &&
|
|
getRemainingDays(licenseData?.payload.trialEnd) < 7
|
|
) {
|
|
setShowTrialExpiryBanner(true);
|
|
}
|
|
}, [licenseData, isFetching]);
|
|
|
|
const handleUpgrade = (): void => {
|
|
if (role === 'ADMIN') {
|
|
history.push(ROUTES.BILLING);
|
|
}
|
|
};
|
|
|
|
const isLogsView = (): boolean =>
|
|
routeKey === 'LOGS' ||
|
|
routeKey === 'LOGS_EXPLORER' ||
|
|
routeKey === 'LOGS_PIPELINES' ||
|
|
routeKey === 'LOGS_SAVE_VIEWS';
|
|
|
|
const isTracesView = (): boolean =>
|
|
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
|
|
|
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
|
const isDashboardView = (): boolean => {
|
|
/**
|
|
* need to match using regex here as the getRoute function will not work for
|
|
* routes with id
|
|
*/
|
|
const regex = /^\/dashboard\/[a-zA-Z0-9_-]+$/;
|
|
return regex.test(pathname);
|
|
};
|
|
|
|
const isDashboardWidgetView = (): boolean => {
|
|
const regex = /^\/dashboard\/[a-zA-Z0-9_-]+\/new$/;
|
|
return regex.test(pathname);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isDarkMode) {
|
|
document.body.classList.remove('lightMode');
|
|
document.body.classList.add('darkMode');
|
|
} else {
|
|
document.body.classList.add('lightMode');
|
|
document.body.classList.remove('darkMode');
|
|
}
|
|
}, [isDarkMode]);
|
|
|
|
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
|
|
|
|
return (
|
|
<Layout
|
|
className={cx(
|
|
isDarkMode ? 'darkMode' : 'lightMode',
|
|
isSideNavCollapsed ? 'sidebarCollapsed' : '',
|
|
)}
|
|
>
|
|
<Helmet>
|
|
<title>{pageTitle}</title>
|
|
</Helmet>
|
|
|
|
{showTrialExpiryBanner && (
|
|
<div className="trial-expiry-banner">
|
|
You are in free trial period. Your free trial will end on{' '}
|
|
<span>
|
|
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
|
</span>
|
|
{role === 'ADMIN' ? (
|
|
<span>
|
|
{' '}
|
|
Please{' '}
|
|
<a className="upgrade-link" onClick={handleUpgrade}>
|
|
upgrade
|
|
</a>
|
|
to continue using SigNoz features.
|
|
</span>
|
|
) : (
|
|
'Please contact your administrator for upgrading to a paid plan.'
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Flex
|
|
className={cx(
|
|
'app-layout',
|
|
isDarkMode ? 'darkMode' : 'lightMode',
|
|
!collapsed && !renderFullScreen ? 'docked' : '',
|
|
)}
|
|
>
|
|
{isToDisplayLayout && !renderFullScreen && (
|
|
<SideNav
|
|
licenseData={licenseData}
|
|
isFetching={isFetching}
|
|
onCollapse={onCollapse}
|
|
collapsed={collapsed}
|
|
/>
|
|
)}
|
|
<div
|
|
className={cx('app-content', collapsed ? 'collapsed' : '')}
|
|
data-overlayscrollbars-initialize
|
|
>
|
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
|
<LayoutContent data-overlayscrollbars-initialize>
|
|
<OverlayScrollbar>
|
|
<ChildrenContainer
|
|
style={{
|
|
margin:
|
|
isLogsView() ||
|
|
isTracesView() ||
|
|
isDashboardView() ||
|
|
isDashboardWidgetView() ||
|
|
isDashboardListView()
|
|
? 0
|
|
: '0 1rem',
|
|
}}
|
|
>
|
|
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
|
{children}
|
|
</ChildrenContainer>
|
|
</OverlayScrollbar>
|
|
</LayoutContent>
|
|
</Sentry.ErrorBoundary>
|
|
</div>
|
|
</Flex>
|
|
</Layout>
|
|
);
|
|
}
|
|
|
|
interface AppLayoutProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export default AppLayout;
|