mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
191 lines
5.8 KiB
TypeScript
191 lines
5.8 KiB
TypeScript
import {isValidElement, useEffect, useRef, useState} from 'react';
|
|
import type {RouteComponentProps} from 'react-router';
|
|
import {browserHistory} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
|
|
import {Button} from 'sentry/components/button';
|
|
import * as Layout from 'sentry/components/layouts/thirds';
|
|
import {IconClose, IconMenu} from 'sentry/icons';
|
|
import {t} from 'sentry/locale';
|
|
import {fadeIn, slideInLeft} from 'sentry/styles/animations';
|
|
import {space} from 'sentry/styles/space';
|
|
|
|
import SettingsBreadcrumb from './settingsBreadcrumb';
|
|
import SettingsHeader from './settingsHeader';
|
|
import SettingsSearch from './settingsSearch';
|
|
|
|
type Props = {
|
|
children: React.ReactNode;
|
|
renderNavigation?: (opts: {isMobileNavVisible: boolean}) => React.ReactNode;
|
|
} & RouteComponentProps<{}, {}>;
|
|
|
|
function SettingsLayout(props: Props) {
|
|
// This is used when the screen is small enough that the navigation should be
|
|
// hidden. This state is only used when the media query matches.
|
|
//
|
|
// [!!] On large screens this state is totally unused!
|
|
const [isMobileNavVisible, setMobileNavVisible] = useState(false);
|
|
|
|
// Offset mobile settings navigation by the height of main navigation,
|
|
// settings breadcrumbs and optional warnings.
|
|
const [navOffsetTop, setNavOffsetTop] = useState(0);
|
|
|
|
const headerRef = useRef<HTMLDivElement>(null);
|
|
|
|
function toggleNav(visible: boolean) {
|
|
const bodyElement = document.getElementsByTagName('body')[0];
|
|
|
|
window.scrollTo?.(0, 0);
|
|
bodyElement.classList[visible ? 'add' : 'remove']('scroll-lock');
|
|
|
|
setMobileNavVisible(visible);
|
|
setNavOffsetTop(headerRef.current?.getBoundingClientRect().bottom ?? 0);
|
|
}
|
|
|
|
// Close menu when navigating away
|
|
useEffect(() => browserHistory.listen(() => toggleNav(false)), []);
|
|
|
|
const {renderNavigation, children, params, routes, route} = props;
|
|
|
|
// We want child's view's props
|
|
const childProps = children && isValidElement(children) ? children.props : props;
|
|
const childRoutes = childProps.routes || routes || [];
|
|
const childRoute = childProps.route || route || {};
|
|
const shouldRenderNavigation = typeof renderNavigation === 'function';
|
|
|
|
return (
|
|
<SettingsColumn>
|
|
<SettingsHeader ref={headerRef}>
|
|
<HeaderContent>
|
|
{shouldRenderNavigation && (
|
|
<NavMenuToggle
|
|
priority="link"
|
|
aria-label={isMobileNavVisible ? t('Close the menu') : t('Open the menu')}
|
|
icon={
|
|
isMobileNavVisible ? <IconClose aria-hidden /> : <IconMenu aria-hidden />
|
|
}
|
|
onClick={() => toggleNav(!isMobileNavVisible)}
|
|
/>
|
|
)}
|
|
<StyledSettingsBreadcrumb
|
|
params={params}
|
|
routes={childRoutes}
|
|
route={childRoute}
|
|
/>
|
|
<SettingsSearch />
|
|
</HeaderContent>
|
|
</SettingsHeader>
|
|
|
|
<MaxWidthContainer>
|
|
{shouldRenderNavigation && (
|
|
<SidebarWrapper
|
|
aria-label={t('Settings Navigation')}
|
|
isVisible={isMobileNavVisible}
|
|
offsetTop={navOffsetTop}
|
|
>
|
|
{renderNavigation({isMobileNavVisible})}
|
|
</SidebarWrapper>
|
|
)}
|
|
<NavMask isVisible={isMobileNavVisible} onClick={() => toggleNav(false)} />
|
|
<Content>{children}</Content>
|
|
</MaxWidthContainer>
|
|
</SettingsColumn>
|
|
);
|
|
}
|
|
|
|
const SettingsColumn = styled('div')`
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1; /* so this stretches vertically so that footer is fixed at bottom */
|
|
min-width: 0; /* fixes problem when child content stretches beyond layout width */
|
|
footer {
|
|
margin-top: 0;
|
|
}
|
|
`;
|
|
|
|
const HeaderContent = styled('div')`
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
`;
|
|
|
|
const NavMenuToggle = styled(Button)`
|
|
display: none;
|
|
margin: -${space(1)} ${space(1)} -${space(1)} -${space(1)};
|
|
padding: ${space(1)};
|
|
color: ${p => p.theme.subText};
|
|
&:hover,
|
|
&:focus,
|
|
&:active {
|
|
color: ${p => p.theme.textColor};
|
|
}
|
|
@media (max-width: ${p => p.theme.breakpoints.medium}) {
|
|
display: block;
|
|
}
|
|
`;
|
|
|
|
const StyledSettingsBreadcrumb = styled(SettingsBreadcrumb)`
|
|
flex: 1;
|
|
`;
|
|
|
|
const MaxWidthContainer = styled('div')`
|
|
display: flex;
|
|
max-width: ${p => p.theme.settings.containerWidth};
|
|
flex: 1;
|
|
`;
|
|
|
|
const SidebarWrapper = styled('nav')<{isVisible: boolean; offsetTop: number}>`
|
|
flex-shrink: 0;
|
|
width: ${p => p.theme.settings.sidebarWidth};
|
|
background: ${p => p.theme.background};
|
|
border-right: 1px solid ${p => p.theme.border};
|
|
|
|
@media (max-width: ${p => p.theme.breakpoints.medium}) {
|
|
display: ${p => (p.isVisible ? 'block' : 'none')};
|
|
position: fixed;
|
|
top: ${p => p.offsetTop}px;
|
|
bottom: 0;
|
|
overflow-y: auto;
|
|
animation: ${slideInLeft} 100ms ease-in-out;
|
|
z-index: ${p => p.theme.zIndex.settingsSidebarNav};
|
|
box-shadow: ${p => p.theme.dropShadowHeavy};
|
|
}
|
|
`;
|
|
|
|
const NavMask = styled('div')<{isVisible: boolean}>`
|
|
display: none;
|
|
@media (max-width: ${p => p.theme.breakpoints.medium}) {
|
|
display: ${p => (p.isVisible ? 'block' : 'none')};
|
|
background: rgba(0, 0, 0, 0.35);
|
|
height: 100%;
|
|
width: 100%;
|
|
position: absolute;
|
|
z-index: ${p => p.theme.zIndex.settingsSidebarNavMask};
|
|
animation: ${fadeIn} 250ms ease-in-out;
|
|
}
|
|
`;
|
|
|
|
/**
|
|
* Note: `overflow: hidden` will cause some buttons in `SettingsPageHeader` to be cut off because it has negative margin.
|
|
* Will also cut off tooltips.
|
|
*/
|
|
const Content = styled('div')`
|
|
flex: 1;
|
|
padding: ${space(4)};
|
|
min-width: 0; /* keep children from stretching container */
|
|
|
|
@media (max-width: ${p => p.theme.breakpoints.medium}) {
|
|
padding: ${space(2)};
|
|
}
|
|
|
|
/**
|
|
* Layout.Page is not normally used in settings but <PermissionDenied /> uses
|
|
* it under the hood. This prevents double padding.
|
|
*/
|
|
${Layout.Page} {
|
|
padding: 0;
|
|
}
|
|
`;
|
|
|
|
export default SettingsLayout;
|