mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
refactor(app-nav): reorganize AppNav component structure and improve maintainability (#1621)
This commit is contained in:
parent
bf553d68d9
commit
824a19a7b9
10 changed files with 739 additions and 565 deletions
5
.changeset/purple-doors-sleep.md
Normal file
5
.changeset/purple-doors-sleep.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
refactor(app-nav): reorganize AppNav component structure and improve maintainability
|
||||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import Link from 'next/link';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ActionIcon,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
|
|
@ -27,12 +26,10 @@ import {
|
|||
IconUserCog,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import { IS_LOCAL_MODE } from '@/config';
|
||||
import InstallInstructionModal from '@/InstallInstructionsModal';
|
||||
import { useSources } from '@/source';
|
||||
|
||||
import { IS_LOCAL_MODE } from './config';
|
||||
|
||||
import styles from '../styles/AppNav.module.scss';
|
||||
import styles from './AppNav.module.scss';
|
||||
|
||||
export const AppNavContext = React.createContext<{
|
||||
isCollapsed: boolean;
|
||||
|
|
@ -84,8 +81,8 @@ export const AppNavUserMenu = ({
|
|||
<Menu.Target>
|
||||
<Paper
|
||||
data-testid="user-menu-trigger"
|
||||
className={cx(styles.userMenuTrigger, {
|
||||
[styles.userMenuTriggerCollapsed]: isCollapsed,
|
||||
className={cx(styles.userMenu, {
|
||||
[styles.userMenuCollapsed]: isCollapsed,
|
||||
})}
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap" miw={0}>
|
||||
|
|
@ -107,29 +104,16 @@ export const AppNavUserMenu = ({
|
|||
}
|
||||
openDelay={250}
|
||||
>
|
||||
<div style={{ flex: 1, overflow: 'hidden' }}>
|
||||
<div className={styles.userMenuInfo}>
|
||||
<Text
|
||||
size="xs"
|
||||
fw="bold"
|
||||
lh={1.1}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
className={styles.userMenuName}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
style={{
|
||||
fontSize: 11,
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxHeight: 16,
|
||||
}}
|
||||
>
|
||||
<Text size="xs" className={styles.userMenuTeam}>
|
||||
{teamName}
|
||||
</Text>
|
||||
</div>
|
||||
|
|
@ -179,18 +163,6 @@ export const AppNavUserMenu = ({
|
|||
);
|
||||
};
|
||||
|
||||
const useIsTeamHasNoData = () => {
|
||||
const now = React.useMemo(() => new Date(), []);
|
||||
const ago14d = React.useMemo(
|
||||
() => new Date(Date.now() - 1000 * 60 * 60 * 24 * 14),
|
||||
[],
|
||||
);
|
||||
|
||||
const { data: sources } = useSources();
|
||||
|
||||
return Array.isArray(sources) && sources?.length > 0 ? false : true;
|
||||
};
|
||||
|
||||
export const AppNavHelpMenu = ({
|
||||
version,
|
||||
onAddDataClick,
|
||||
|
|
@ -202,16 +174,14 @@ export const AppNavHelpMenu = ({
|
|||
|
||||
const [
|
||||
installModalOpen,
|
||||
{ close: closeInstallModal, open: openInstallModal },
|
||||
{ close: closeInstallModal, open: _openInstallModal },
|
||||
] = useDisclosure(false);
|
||||
|
||||
// const isTeamHasNoData = useIsTeamHasNoData();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper
|
||||
className={cx(styles.helpMenuTrigger, {
|
||||
[styles.helpMenuTriggerCollapsed]: isCollapsed,
|
||||
className={cx(styles.helpButton, {
|
||||
[styles.helpButtonCollapsed]: isCollapsed,
|
||||
})}
|
||||
>
|
||||
<Menu
|
||||
|
|
@ -291,45 +261,58 @@ export const AppNavLink = ({
|
|||
}) => {
|
||||
const { pathname, isCollapsed } = React.useContext(AppNavContext);
|
||||
|
||||
// Create a test id based on the href
|
||||
const testId = `nav-link-${href.replace(/^\//, '').replace(/\//g, '-') || 'home'}`;
|
||||
|
||||
const handleToggleClick = (e: React.MouseEvent) => {
|
||||
// Clicking chevron only toggles submenu, doesn't navigate
|
||||
// This separates navigation (clicking link) from expand/collapse (clicking chevron)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onToggle?.();
|
||||
};
|
||||
|
||||
// Check if current path matches this nav item
|
||||
// Use exact match or startsWith to avoid partial matches (e.g., /search matching /search-settings)
|
||||
const isActive = pathname === href || pathname?.startsWith(href + '/');
|
||||
|
||||
return (
|
||||
<Group justify="space-between" px="md" py="6px" h="34px">
|
||||
<Link
|
||||
data-testid={testId}
|
||||
href={href}
|
||||
className={cx(
|
||||
styles.listLink,
|
||||
{ [styles.listLinkActive]: pathname?.includes(href) },
|
||||
className,
|
||||
)}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span className={styles.linkIcon}>{icon}</span>
|
||||
{!isCollapsed && <span>{label}</span>}
|
||||
</span>
|
||||
</Link>
|
||||
<Link
|
||||
data-testid={testId}
|
||||
href={href}
|
||||
className={cx(
|
||||
styles.navItem,
|
||||
{ [styles.navItemActive]: isActive },
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className={styles.navItemContent}>
|
||||
<span className={styles.navItemIcon}>{icon}</span>
|
||||
{!isCollapsed && <span>{label}</span>}
|
||||
</span>
|
||||
{!isCollapsed && isBeta && (
|
||||
<Badge size="xs" radius="sm" color="gray" style={{ marginRight: 8 }}>
|
||||
<Badge
|
||||
size="xs"
|
||||
radius="sm"
|
||||
color="gray"
|
||||
className={styles.navItemBadge}
|
||||
>
|
||||
Beta
|
||||
</Badge>
|
||||
)}
|
||||
{!isCollapsed && onToggle && (
|
||||
<ActionIcon
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`${testId}-toggle`}
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onToggle}
|
||||
className={styles.navItemToggle}
|
||||
onClick={handleToggleClick}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<IconChevronUp size={14} className="text-muted-hover" />
|
||||
) : (
|
||||
<IconChevronDown size={14} className="text-muted-hover" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</button>
|
||||
)}
|
||||
</Group>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
403
packages/app/src/components/AppNav/AppNav.module.scss
Normal file
403
packages/app/src/components/AppNav/AppNav.module.scss
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 12px;
|
||||
$spacing-lg: 16px;
|
||||
$nav-item-height: 34px;
|
||||
$header-height: 58px;
|
||||
$help-button-size: 28px;
|
||||
$search-input-height: 28px;
|
||||
$radius-sm: 4px;
|
||||
$radius-md: 8px;
|
||||
$radius-lg: 10px;
|
||||
$font-size-xs: 10px;
|
||||
$font-size-sm: 11px;
|
||||
$font-size-md: 13px;
|
||||
$font-size-lg: 14px;
|
||||
$transition-fast: 0.1s ease;
|
||||
$transition-normal: 0.15s ease;
|
||||
$transition-slow: 0.2s ease;
|
||||
|
||||
/* Mixins */
|
||||
@mixin text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin interactive-press {
|
||||
&:active {
|
||||
transform: translate(0, 1px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
.nav {
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-between;
|
||||
border-right: 1px solid var(--color-border);
|
||||
background: var(--color-bg-sidenav);
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
&Fixed {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-inline: $spacing-lg;
|
||||
padding-block: $spacing-md;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&Expanded {
|
||||
height: $header-height;
|
||||
}
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logoIconWrapper {
|
||||
margin-left: -0.15rem;
|
||||
}
|
||||
|
||||
.collapseButton {
|
||||
margin-right: -$spacing-xs;
|
||||
margin-left: -$spacing-xs;
|
||||
|
||||
&Collapsed {
|
||||
margin-top: $spacing-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--color-bg-sidenav) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
/* Navigation Links */
|
||||
|
||||
.navLinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding-inline: $spacing-sm;
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-sidenav-link);
|
||||
font-size: $font-size-lg;
|
||||
|
||||
@include text-truncate;
|
||||
|
||||
user-select: none;
|
||||
gap: 10px;
|
||||
padding-inline: $spacing-sm;
|
||||
height: $nav-item-height;
|
||||
border-radius: $radius-sm;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-sidenav-link-active);
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
|
||||
&Active {
|
||||
color: var(--color-text-sidenav-link-active);
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
}
|
||||
|
||||
.navItemContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navItemIcon {
|
||||
margin-right: $spacing-sm;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
transition: color $transition-slow;
|
||||
}
|
||||
|
||||
.navItemBadge {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.navItemToggle {
|
||||
@include flex-center;
|
||||
|
||||
padding: $spacing-xs;
|
||||
margin-right: -$spacing-xs;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: $radius-sm;
|
||||
color: var(--color-text-muted);
|
||||
transition:
|
||||
background-color $transition-normal,
|
||||
color $transition-normal,
|
||||
transform $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-sidenav-link-hover);
|
||||
color: var(--color-text-sidenav-link-active);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sub-menu (Collapsible sections) */
|
||||
|
||||
.subMenu {
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
padding: $spacing-xs $spacing-xs;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.subMenuItem {
|
||||
display: block;
|
||||
color: var(--color-text-sidenav-link);
|
||||
text-decoration: none;
|
||||
font-size: $font-size-md;
|
||||
padding-left: $spacing-lg;
|
||||
padding-block: 2px;
|
||||
transition: color $transition-slow;
|
||||
|
||||
@include text-truncate;
|
||||
|
||||
border-radius: $radius-sm;
|
||||
margin-bottom: 2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-sidenav-link-active);
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
|
||||
i {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&Active {
|
||||
color: var(--color-text-sidenav-link-active);
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
}
|
||||
|
||||
/* Groups (Saved Searches, Dashboards) */
|
||||
|
||||
.groupLabel {
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
font-size: $font-size-sm;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 6px;
|
||||
padding-left: $spacing-lg;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-sidenav-link);
|
||||
}
|
||||
}
|
||||
|
||||
.groupDragOver {
|
||||
opacity: 0.7;
|
||||
background: #ffffff1a;
|
||||
border-radius: $radius-lg;
|
||||
}
|
||||
|
||||
/* Search Input */
|
||||
|
||||
.searchInput {
|
||||
input {
|
||||
min-height: $search-input-height;
|
||||
height: $search-input-height;
|
||||
line-height: $search-input-height;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcutHint {
|
||||
white-space: nowrap;
|
||||
letter-spacing: -1px;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg-surface);
|
||||
border-radius: $radius-sm;
|
||||
padding: 0 $spacing-xs;
|
||||
font-size: $font-size-xs;
|
||||
margin-right: $spacing-lg;
|
||||
|
||||
&Ctrl {
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty States */
|
||||
|
||||
.emptyMessage {
|
||||
color: var(--color-text-muted);
|
||||
font-size: $font-size-md - 1px;
|
||||
margin: $spacing-sm $spacing-lg;
|
||||
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
/* User Menu */
|
||||
|
||||
.userMenu {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
margin: $spacing-sm;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-md;
|
||||
transition:
|
||||
background-color $transition-slow,
|
||||
transform $transition-fast;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
|
||||
@include interactive-press;
|
||||
|
||||
&Collapsed {
|
||||
padding: 2px;
|
||||
margin: $spacing-sm auto 0.875rem;
|
||||
background: transparent;
|
||||
border: none !important;
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
|
||||
@include interactive-press;
|
||||
}
|
||||
}
|
||||
|
||||
.userMenuInfo {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.userMenuName {
|
||||
max-width: 100%;
|
||||
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
.userMenuTeam {
|
||||
font-size: $font-size-sm;
|
||||
max-width: 100%;
|
||||
max-height: $spacing-lg;
|
||||
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
/* Help Button */
|
||||
|
||||
.helpButton {
|
||||
@include flex-center;
|
||||
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
margin: 0 $spacing-sm $spacing-sm;
|
||||
width: $help-button-size;
|
||||
height: $help-button-size;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-border);
|
||||
transition:
|
||||
background-color $transition-slow,
|
||||
transform $transition-fast,
|
||||
border-color $transition-slow;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-sidenav-link);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
@include interactive-press;
|
||||
|
||||
&Collapsed {
|
||||
margin: 0 auto $spacing-sm;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-sidenav-link);
|
||||
}
|
||||
|
||||
@include interactive-press;
|
||||
}
|
||||
}
|
||||
|
||||
/* Onboarding Section */
|
||||
|
||||
.onboardingSection {
|
||||
padding-left: $spacing-sm;
|
||||
padding-right: $spacing-md;
|
||||
margin-bottom: $spacing-sm;
|
||||
margin-top: $spacing-lg;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
/* Scrollbar Customization */
|
||||
|
||||
.scrollbar {
|
||||
.thumb {
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { Meta } from '@storybook/nextjs';
|
||||
import type { StoryObj } from '@storybook/nextjs';
|
||||
|
||||
import AppNav from '../AppNav';
|
||||
import { AppNavUserMenu } from '../AppNav.components';
|
||||
import AppNav from './AppNav';
|
||||
import { AppNavUserMenu } from './AppNav.components';
|
||||
|
||||
const meta: Meta = {
|
||||
component: AppNav,
|
||||
|
|
@ -15,7 +15,6 @@ import { AlertState } from '@hyperdx/common-utils/dist/types';
|
|||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
CloseButton,
|
||||
Collapse,
|
||||
|
|
@ -41,20 +40,25 @@ import {
|
|||
IconTable,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import api from '@/api';
|
||||
import { IS_K8S_DASHBOARD_ENABLED, IS_LOCAL_MODE } from '@/config';
|
||||
import {
|
||||
useCreateDashboard,
|
||||
useDashboards,
|
||||
useUpdateDashboard,
|
||||
} from '@/dashboard';
|
||||
import Icon from '@/Icon';
|
||||
import InstallInstructionModal from '@/InstallInstructionsModal';
|
||||
import Logo from '@/Logo';
|
||||
import OnboardingChecklist from '@/OnboardingChecklist';
|
||||
import { useSavedSearches, useUpdateSavedSearch } from '@/savedSearch';
|
||||
import type { SavedSearch, ServerDashboard } from '@/types';
|
||||
import { UserPreferencesModal } from '@/UserPreferencesModal';
|
||||
import { useUserPreferences } from '@/useUserPreferences';
|
||||
import { useWindowSize } from '@/utils';
|
||||
|
||||
import packageJson from '../package.json';
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
// Expose the same value Next injected at build time; fall back to package.json for dev tooling
|
||||
const APP_VERSION =
|
||||
process.env.NEXT_PUBLIC_APP_VERSION ?? packageJson.version ?? 'dev';
|
||||
|
||||
import api from './api';
|
||||
import {
|
||||
AppNavCloudBanner,
|
||||
AppNavContext,
|
||||
|
|
@ -62,21 +66,55 @@ import {
|
|||
AppNavLink,
|
||||
AppNavUserMenu,
|
||||
} from './AppNav.components';
|
||||
import { IS_K8S_DASHBOARD_ENABLED, IS_LOCAL_MODE } from './config';
|
||||
import Icon from './Icon';
|
||||
import InstallInstructionModal from './InstallInstructionsModal';
|
||||
import Logo from './Logo';
|
||||
import OnboardingChecklist from './OnboardingChecklist';
|
||||
import { useSavedSearches, useUpdateSavedSearch } from './savedSearch';
|
||||
import type { SavedSearch, ServerDashboard } from './types';
|
||||
import { UserPreferencesModal } from './UserPreferencesModal';
|
||||
import { useWindowSize } from './utils';
|
||||
|
||||
import styles from '../styles/AppNav.module.scss';
|
||||
import styles from './AppNav.module.scss';
|
||||
|
||||
// Expose the same value Next injected at build time; fall back to package.json for dev tooling
|
||||
const APP_VERSION =
|
||||
process.env.NEXT_PUBLIC_APP_VERSION ?? packageJson.version ?? 'dev';
|
||||
|
||||
const UNTAGGED_SEARCHES_GROUP_NAME = 'Saved Searches';
|
||||
const UNTAGGED_DASHBOARDS_GROUP_NAME = 'Saved Dashboards';
|
||||
|
||||
// Navigation link configuration
|
||||
type NavLinkConfig = {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
isBeta?: boolean;
|
||||
cloudOnly?: boolean; // Only show when not in local mode
|
||||
};
|
||||
|
||||
const NAV_LINKS: NavLinkConfig[] = [
|
||||
{
|
||||
id: 'chart',
|
||||
label: 'Chart Explorer',
|
||||
href: '/chart',
|
||||
icon: <IconChartDots size={16} />,
|
||||
},
|
||||
{
|
||||
id: 'alerts',
|
||||
label: 'Alerts',
|
||||
href: '/alerts',
|
||||
icon: <IconBell size={16} />,
|
||||
cloudOnly: true,
|
||||
},
|
||||
{
|
||||
id: 'sessions',
|
||||
label: 'Client Sessions',
|
||||
href: '/sessions',
|
||||
icon: <IconDeviceLaptop size={16} />,
|
||||
},
|
||||
{
|
||||
id: 'service-map',
|
||||
label: 'Service Map',
|
||||
href: '/service-map',
|
||||
icon: <IconSitemap size={16} />,
|
||||
isBeta: true,
|
||||
},
|
||||
];
|
||||
|
||||
function NewDashboardButton() {
|
||||
const createDashboard = useCreateDashboard();
|
||||
|
||||
|
|
@ -138,11 +176,11 @@ function SearchInput({
|
|||
}) {
|
||||
const kbdShortcut = useMemo(() => {
|
||||
return (
|
||||
<div className={styles.kbd}>
|
||||
<div className={styles.shortcutHint}>
|
||||
{window.navigator.platform?.toUpperCase().includes('MAC') ? (
|
||||
<IconCommand size={8} />
|
||||
) : (
|
||||
<span style={{ letterSpacing: -2 }}>Ctrl</span>
|
||||
<span className={styles.shortcutHintCtrl}>Ctrl</span>
|
||||
)}
|
||||
K
|
||||
</div>
|
||||
|
|
@ -212,7 +250,7 @@ const AppNavGroupLabel = ({
|
|||
onClick: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.listGroupName} onClick={onClick}>
|
||||
<div className={styles.groupLabel} onClick={onClick}>
|
||||
{collapsed ? (
|
||||
<IconChevronRight size={14} />
|
||||
) : (
|
||||
|
|
@ -260,9 +298,7 @@ const AppNavLinkGroups = <T extends AppNavLinkItem>({
|
|||
{groups.map(group => (
|
||||
<div
|
||||
key={group.name}
|
||||
className={cx(
|
||||
draggingOver === group.name && styles.listGroupDragEnter,
|
||||
)}
|
||||
className={cx(draggingOver === group.name && styles.groupDragOver)}
|
||||
onDragOver={e => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
|
@ -424,7 +460,6 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
const isCollapsed = isSmallScreen || isPreferCollapsed;
|
||||
|
||||
const navWidth = isCollapsed ? 50 : 230;
|
||||
const navHeaderStyle = isCollapsed ? undefined : { height: 58 };
|
||||
|
||||
useEffect(() => {
|
||||
HyperDX.addAction('user navigated', {
|
||||
|
|
@ -488,8 +523,8 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
key={savedSearch.id}
|
||||
tabIndex={0}
|
||||
className={cx(
|
||||
styles.nestedLink,
|
||||
savedSearch.id === query.savedSearchId && styles.nestedLinkActive,
|
||||
styles.subMenuItem,
|
||||
savedSearch.id === query.savedSearchId && styles.subMenuItemActive,
|
||||
)}
|
||||
title={savedSearch.name}
|
||||
draggable
|
||||
|
|
@ -556,8 +591,8 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
href={`/dashboards/${dashboard.id}`}
|
||||
key={dashboard.id}
|
||||
tabIndex={0}
|
||||
className={cx(styles.nestedLink, {
|
||||
[styles.nestedLinkActive]: dashboard.id === query.dashboardId,
|
||||
className={cx(styles.subMenuItem, {
|
||||
[styles.subMenuItemActive]: dashboard.id === query.dashboardId,
|
||||
})}
|
||||
draggable
|
||||
data-dashboardid={dashboard.id}
|
||||
|
|
@ -618,20 +653,19 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
onHide={closeInstallInstructions}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.wrapper}`}
|
||||
style={{
|
||||
position: fixed ? 'fixed' : 'initial',
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
className={cx(styles.nav, {
|
||||
[styles.navFixed]: fixed,
|
||||
})}
|
||||
>
|
||||
<div style={{ width: navWidth }}>
|
||||
<div
|
||||
className="p-3 d-flex flex-wrap justify-content-between align-items-center"
|
||||
style={navHeaderStyle}
|
||||
className={cx(styles.header, {
|
||||
[styles.headerExpanded]: !isCollapsed,
|
||||
})}
|
||||
>
|
||||
<Link href="/search" className="text-decoration-none">
|
||||
<Link href="/search" className={styles.logoLink}>
|
||||
{isCollapsed ? (
|
||||
<div style={{ marginLeft: '-0.15rem' }}>
|
||||
<div className={styles.logoIconWrapper}>
|
||||
<Icon size={22} />
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -654,8 +688,9 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
className={isCollapsed ? 'mt-4' : ''}
|
||||
style={{ marginRight: -4, marginLeft: -4 }}
|
||||
className={cx(styles.collapseButton, {
|
||||
[styles.collapseButtonCollapsed]: isCollapsed,
|
||||
})}
|
||||
title="Collapse/Expand Navigation"
|
||||
onClick={() => setIsPreferCollapsed((v: boolean) => !v)}
|
||||
>
|
||||
|
|
@ -667,240 +702,209 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
type="scroll"
|
||||
scrollbarSize={6}
|
||||
scrollHideDelay={100}
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
classNames={styles}
|
||||
className="d-flex flex-column justify-content-between"
|
||||
className={styles.scrollContainer}
|
||||
>
|
||||
<div style={{ width: navWidth }}>
|
||||
<div className="mt-2">
|
||||
<AppNavLink
|
||||
label="Search"
|
||||
icon={<IconTable size={16} />}
|
||||
href="/search"
|
||||
className={cx({
|
||||
'text-success fw-600':
|
||||
pathname.includes('/search') && query.savedSearchId == null,
|
||||
'fw-600':
|
||||
pathname.includes('/search') && query.savedSearchId != null,
|
||||
})}
|
||||
isExpanded={isSearchExpanded}
|
||||
onToggle={
|
||||
!IS_LOCAL_MODE
|
||||
? () => setIsSearchExpanded(!isSearchExpanded)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div style={{ width: navWidth }} className={styles.navLinks}>
|
||||
{/* Search */}
|
||||
<AppNavLink
|
||||
label="Search"
|
||||
icon={<IconTable size={16} />}
|
||||
href="/search"
|
||||
isExpanded={isSearchExpanded}
|
||||
onToggle={
|
||||
!IS_LOCAL_MODE
|
||||
? () => setIsSearchExpanded(!isSearchExpanded)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{!isCollapsed && (
|
||||
<Collapse in={isSearchExpanded}>
|
||||
<div className={styles.list}>
|
||||
{isLogViewsLoading ? (
|
||||
<Loader variant="dots" mx="md" my="xs" size="sm" />
|
||||
) : (
|
||||
!IS_LOCAL_MODE && (
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder="Saved Searches"
|
||||
value={searchesListQ}
|
||||
onChange={setSearchesListQ}
|
||||
onEnterDown={() => {
|
||||
(
|
||||
savedSearchesResultsRef?.current
|
||||
?.firstChild as HTMLAnchorElement
|
||||
)?.focus?.();
|
||||
}}
|
||||
/>
|
||||
{!isCollapsed && (
|
||||
<Collapse in={isSearchExpanded}>
|
||||
<div className={styles.subMenu}>
|
||||
{isLogViewsLoading ? (
|
||||
<Loader variant="dots" mx="md" my="xs" size="sm" />
|
||||
) : (
|
||||
!IS_LOCAL_MODE && (
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder="Saved Searches"
|
||||
value={searchesListQ}
|
||||
onChange={setSearchesListQ}
|
||||
onEnterDown={() => {
|
||||
(
|
||||
savedSearchesResultsRef?.current
|
||||
?.firstChild as HTMLAnchorElement
|
||||
)?.focus?.();
|
||||
}}
|
||||
/>
|
||||
|
||||
{logViews.length === 0 && (
|
||||
<div className={styles.listEmptyMsg}>
|
||||
No saved searches
|
||||
</div>
|
||||
)}
|
||||
<div ref={savedSearchesResultsRef}>
|
||||
<AppNavLinkGroups
|
||||
name="saved-searches"
|
||||
groups={groupedFilteredSearchesList}
|
||||
renderLink={renderLogViewLink}
|
||||
forceExpandGroups={!!searchesListQ}
|
||||
onDragEnd={handleLogViewDragEnd}
|
||||
/>
|
||||
{logViews.length === 0 && (
|
||||
<div className={styles.emptyMessage}>
|
||||
No saved searches
|
||||
</div>
|
||||
|
||||
{searchesListQ &&
|
||||
filteredSearchesList.length === 0 ? (
|
||||
<div className={styles.listEmptyMsg}>
|
||||
No results matching <i>{searchesListQ}</i>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Collapse>
|
||||
)}
|
||||
<AppNavLink
|
||||
label="Chart Explorer"
|
||||
href="/chart"
|
||||
icon={<IconChartDots size={16} />}
|
||||
/>
|
||||
{!IS_LOCAL_MODE && (
|
||||
<AppNavLink
|
||||
label="Alerts"
|
||||
href="/alerts"
|
||||
icon={<IconBell size={16} />}
|
||||
/>
|
||||
)}
|
||||
<AppNavLink
|
||||
label="Client Sessions"
|
||||
href="/sessions"
|
||||
icon={<IconDeviceLaptop size={16} />}
|
||||
/>
|
||||
|
||||
<AppNavLink
|
||||
label="Service Map"
|
||||
href="/service-map"
|
||||
icon={<IconSitemap size={16} />}
|
||||
isBeta
|
||||
/>
|
||||
|
||||
<AppNavLink
|
||||
label="Dashboards"
|
||||
href="/dashboards"
|
||||
icon={<IconLayoutGrid size={16} />}
|
||||
isExpanded={isDashboardsExpanded}
|
||||
onToggle={() => setIsDashboardExpanded(!isDashboardsExpanded)}
|
||||
/>
|
||||
|
||||
{!isCollapsed && (
|
||||
<Collapse in={isDashboardsExpanded}>
|
||||
<div className={styles.list}>
|
||||
<NewDashboardButton />
|
||||
|
||||
{isDashboardsLoading ? (
|
||||
<Loader variant="dots" mx="md" my="xs" size="sm" />
|
||||
) : (
|
||||
!IS_LOCAL_MODE && (
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder="Saved Dashboards"
|
||||
value={dashboardsListQ}
|
||||
onChange={setDashboardsListQ}
|
||||
onEnterDown={() => {
|
||||
(
|
||||
dashboardsResultsRef?.current
|
||||
?.firstChild as HTMLAnchorElement
|
||||
)?.focus?.();
|
||||
}}
|
||||
/>
|
||||
|
||||
)}
|
||||
<div ref={savedSearchesResultsRef}>
|
||||
<AppNavLinkGroups
|
||||
name="dashboards"
|
||||
/* @ts-ignore */
|
||||
groups={groupedFilteredDashboardsList}
|
||||
renderLink={renderDashboardLink}
|
||||
forceExpandGroups={!!dashboardsListQ}
|
||||
onDragEnd={handleDashboardDragEnd}
|
||||
name="saved-searches"
|
||||
groups={groupedFilteredSearchesList}
|
||||
renderLink={renderLogViewLink}
|
||||
forceExpandGroups={!!searchesListQ}
|
||||
onDragEnd={handleLogViewDragEnd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{dashboards.length === 0 && (
|
||||
<div className={styles.listEmptyMsg}>
|
||||
No saved dashboards
|
||||
</div>
|
||||
)}
|
||||
{searchesListQ && filteredSearchesList.length === 0 ? (
|
||||
<div className={styles.emptyMessage}>
|
||||
No results matching <i>{searchesListQ}</i>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Collapse>
|
||||
)}
|
||||
{/* Simple nav links from config */}
|
||||
{NAV_LINKS.filter(link => !link.cloudOnly || !IS_LOCAL_MODE).map(
|
||||
link => (
|
||||
<AppNavLink
|
||||
key={link.id}
|
||||
label={link.label}
|
||||
href={link.href}
|
||||
icon={link.icon}
|
||||
isBeta={link.isBeta}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{dashboardsListQ &&
|
||||
filteredDashboardsList.length === 0 ? (
|
||||
<div className={styles.listEmptyMsg}>
|
||||
No results matching <i>{dashboardsListQ}</i>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
{/* Dashboards */}
|
||||
<AppNavLink
|
||||
label="Dashboards"
|
||||
href="/dashboards"
|
||||
icon={<IconLayoutGrid size={16} />}
|
||||
isExpanded={isDashboardsExpanded}
|
||||
onToggle={() => setIsDashboardExpanded(!isDashboardsExpanded)}
|
||||
/>
|
||||
|
||||
{!isCollapsed && (
|
||||
<Collapse in={isDashboardsExpanded}>
|
||||
<div className={styles.subMenu}>
|
||||
<NewDashboardButton />
|
||||
|
||||
{isDashboardsLoading ? (
|
||||
<Loader variant="dots" mx="md" my="xs" size="sm" />
|
||||
) : (
|
||||
!IS_LOCAL_MODE && (
|
||||
<>
|
||||
<SearchInput
|
||||
placeholder="Saved Dashboards"
|
||||
value={dashboardsListQ}
|
||||
onChange={setDashboardsListQ}
|
||||
onEnterDown={() => {
|
||||
(
|
||||
dashboardsResultsRef?.current
|
||||
?.firstChild as HTMLAnchorElement
|
||||
)?.focus?.();
|
||||
}}
|
||||
/>
|
||||
|
||||
<AppNavLinkGroups
|
||||
name="dashboards"
|
||||
groups={groupedFilteredDashboardsList}
|
||||
renderLink={renderDashboardLink}
|
||||
forceExpandGroups={!!dashboardsListQ}
|
||||
onDragEnd={handleDashboardDragEnd}
|
||||
/>
|
||||
|
||||
{dashboards.length === 0 && (
|
||||
<div className={styles.emptyMessage}>
|
||||
No saved dashboards
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dashboardsListQ &&
|
||||
filteredDashboardsList.length === 0 ? (
|
||||
<div className={styles.emptyMessage}>
|
||||
No results matching <i>{dashboardsListQ}</i>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
<AppNavGroupLabel
|
||||
name="Presets"
|
||||
collapsed={isDashboardsPresetsCollapsed}
|
||||
onClick={() =>
|
||||
setDashboardsPresetsCollapsed(
|
||||
!isDashboardsPresetsCollapsed,
|
||||
)
|
||||
)}
|
||||
|
||||
<AppNavGroupLabel
|
||||
name="Presets"
|
||||
collapsed={isDashboardsPresetsCollapsed}
|
||||
onClick={() =>
|
||||
setDashboardsPresetsCollapsed(
|
||||
!isDashboardsPresetsCollapsed,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Collapse in={!isDashboardsPresetsCollapsed}>
|
||||
<Link
|
||||
href={`/clickhouse`}
|
||||
tabIndex={0}
|
||||
className={cx(styles.nestedLink, {
|
||||
[styles.nestedLinkActive]:
|
||||
pathname.startsWith('/clickhouse'),
|
||||
})}
|
||||
data-testid="nav-link-clickhouse-dashboard"
|
||||
>
|
||||
ClickHouse
|
||||
</Link>
|
||||
<Link
|
||||
href={`/services`}
|
||||
tabIndex={0}
|
||||
className={cx(styles.nestedLink, {
|
||||
[styles.nestedLinkActive]:
|
||||
pathname.startsWith('/services'),
|
||||
})}
|
||||
data-testid="nav-link-services-dashboard"
|
||||
>
|
||||
Services
|
||||
</Link>
|
||||
{IS_K8S_DASHBOARD_ENABLED && (
|
||||
<Link
|
||||
href={`/kubernetes`}
|
||||
tabIndex={0}
|
||||
className={cx(styles.nestedLink, {
|
||||
[styles.nestedLinkActive]:
|
||||
pathname.startsWith('/kubernetes'),
|
||||
})}
|
||||
data-testid="nav-link-k8s-dashboard"
|
||||
>
|
||||
Kubernetes
|
||||
</Link>
|
||||
)}
|
||||
</Collapse>
|
||||
</div>
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
{!IS_LOCAL_MODE && (
|
||||
<Box mt="sm">
|
||||
<AppNavLink
|
||||
label="Team Settings"
|
||||
href="/team"
|
||||
icon={<IconSettings size={16} />}
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
<Collapse in={!isDashboardsPresetsCollapsed}>
|
||||
<Link
|
||||
href={`/clickhouse`}
|
||||
tabIndex={0}
|
||||
className={cx(styles.subMenuItem, {
|
||||
[styles.subMenuItemActive]:
|
||||
pathname.startsWith('/clickhouse'),
|
||||
})}
|
||||
data-testid="nav-link-clickhouse-dashboard"
|
||||
>
|
||||
ClickHouse
|
||||
</Link>
|
||||
<Link
|
||||
href={`/services`}
|
||||
tabIndex={0}
|
||||
className={cx(styles.subMenuItem, {
|
||||
[styles.subMenuItemActive]:
|
||||
pathname.startsWith('/services'),
|
||||
})}
|
||||
data-testid="nav-link-services-dashboard"
|
||||
>
|
||||
Services
|
||||
</Link>
|
||||
{IS_K8S_DASHBOARD_ENABLED && (
|
||||
<Link
|
||||
href={`/kubernetes`}
|
||||
tabIndex={0}
|
||||
className={cx(styles.subMenuItem, {
|
||||
[styles.subMenuItemActive]:
|
||||
pathname.startsWith('/kubernetes'),
|
||||
})}
|
||||
data-testid="nav-link-k8s-dashboard"
|
||||
>
|
||||
Kubernetes
|
||||
</Link>
|
||||
)}
|
||||
</Collapse>
|
||||
</div>
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
{/* Team Settings (Cloud only) */}
|
||||
{!IS_LOCAL_MODE && (
|
||||
<AppNavLink
|
||||
label="Team Settings"
|
||||
href="/team"
|
||||
icon={<IconSettings size={16} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<div
|
||||
style={{ width: navWidth, paddingBottom: 80 }}
|
||||
className="px-3 mb-2 mt-4"
|
||||
>
|
||||
<OnboardingChecklist onAddDataClick={openInstallInstructions} />
|
||||
<AppNavCloudBanner />
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
style={{ width: navWidth }}
|
||||
className={styles.onboardingSection}
|
||||
>
|
||||
<OnboardingChecklist onAddDataClick={openInstallInstructions} />
|
||||
<AppNavCloudBanner />
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<div
|
||||
className={styles.bottomSection}
|
||||
style={{
|
||||
width: navWidth,
|
||||
}}
|
||||
>
|
||||
<div className={styles.footer} style={{ width: navWidth }}>
|
||||
<AppNavHelpMenu
|
||||
version={APP_VERSION}
|
||||
onAddDataClick={openInstallInstructions}
|
||||
8
packages/app/src/components/AppNav/index.ts
Normal file
8
packages/app/src/components/AppNav/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { default } from './AppNav';
|
||||
export {
|
||||
AppNavCloudBanner,
|
||||
AppNavContext,
|
||||
AppNavHelpMenu,
|
||||
AppNavLink,
|
||||
AppNavUserMenu,
|
||||
} from './AppNav.components';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import AppNav from './AppNav';
|
||||
import AppNav from '@/components/AppNav';
|
||||
|
||||
import { HDXSpotlightProvider } from './Spotlights';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
.wrapper {
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-between;
|
||||
border-right: 1px solid var(--color-border);
|
||||
background: var(--color-bg-sidebar);
|
||||
}
|
||||
|
||||
.list {
|
||||
border-top: 1px solid var(--color-border);
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
padding: 4px 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
.thumb {
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.listGroupDragEnter {
|
||||
opacity: 0.7;
|
||||
background: #ffffff1a;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.listGroupName {
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 6px;
|
||||
padding-left: 16px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.listLink {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
gap: 10px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-success);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
margin-right: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-success);
|
||||
}
|
||||
}
|
||||
|
||||
.listEmptyMsg {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
margin: 8px 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nestedLink {
|
||||
display: block;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
padding-left: 16px;
|
||||
padding-block: 2px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-success);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
|
||||
i {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.nestedLinkActive {
|
||||
color: var(--color-text-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.listLinkActive {
|
||||
color: var(--color-text-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kbd {
|
||||
white-space: nowrap;
|
||||
letter-spacing: -1px;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg-surface);
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
font-size: 10px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
input {
|
||||
min-height: 28px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
// User menu trigger styling
|
||||
.userMenuTrigger {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
margin: 8px 0.875rem 0.875rem;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
transform 0.1s ease;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(0, 1px);
|
||||
}
|
||||
|
||||
&Collapsed {
|
||||
padding: 2px;
|
||||
margin: 8px auto 0.875rem;
|
||||
background: transparent;
|
||||
border: none !important;
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(0, 1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Help menu trigger styling
|
||||
.helpMenuTrigger {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
margin: 0 0.875rem 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-border);
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
transform 0.1s ease,
|
||||
border-color 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-muted);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(0, 1px);
|
||||
}
|
||||
|
||||
&Collapsed {
|
||||
margin: 0 auto 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(0, 1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom section with gradient overlay
|
||||
.bottomSection {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--color-bg-sidebar) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-bg-sidebar);
|
||||
background-color: var(--color-bg-body);
|
||||
word-break: break-all;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
--color-bg-inverted: var(--mantine-color-dark-1);
|
||||
--color-bg-muted: var(--mantine-color-dark-7);
|
||||
--color-bg-highlighted: rgb(55 58 64 / 70%);
|
||||
--color-bg-sidebar: var(--mantine-color-dark-9);
|
||||
--color-bg-sidenav: var(--mantine-color-dark-9);
|
||||
--color-bg-sidenav-link: var(--mantine-color-dark-6);
|
||||
--color-bg-sidenav-link-hover: var(--mantine-color-dark-7);
|
||||
--color-bg-header: var(--mantine-color-dark-9);
|
||||
--color-bg-hover: var(--mantine-color-dark-6);
|
||||
--color-bg-active: var(--mantine-color-dark-5);
|
||||
|
|
@ -33,6 +35,8 @@
|
|||
--color-text-success: var(--mantine-color-green-4);
|
||||
--color-text-success-hover: var(--mantine-color-green-3);
|
||||
--color-text-danger: var(--mantine-color-red-3);
|
||||
--color-text-sidenav-link: var(--mantine-color-dark-0);
|
||||
--color-text-sidenav-link-active: var(--color-text-primary);
|
||||
|
||||
/* Icons */
|
||||
--color-icon-primary: var(--mantine-color-dark-1);
|
||||
|
|
@ -76,7 +80,9 @@
|
|||
--color-bg-inverted: var(--mantine-color-dark-3);
|
||||
--color-bg-muted: #f6f6fa;
|
||||
--color-bg-highlighted: rgb(230 232 237 / 70%);
|
||||
--color-bg-sidebar: var(--mantine-color-white);
|
||||
--color-bg-sidenav: var(--mantine-color-white);
|
||||
--color-bg-sidenav-link: #f6f6fa;
|
||||
--color-bg-sidenav-link-hover: var(--mantine-color-gray-2);
|
||||
--color-bg-header: var(--mantine-color-gray-1);
|
||||
--color-bg-modal: var(--mantine-color-white);
|
||||
--color-bg-hover: var(--mantine-color-gray-3);
|
||||
|
|
@ -104,6 +110,8 @@
|
|||
--color-text-success: var(--mantine-color-green-8);
|
||||
--color-text-success-hover: var(--mantine-color-green-9);
|
||||
--color-text-danger: var(--mantine-color-red-8);
|
||||
--color-text-sidenav-link: var(--mantine-color-dark-6);
|
||||
--color-text-sidenav-link-active: var(--mantine-color-green-9);
|
||||
|
||||
/* Icons - inverted */
|
||||
--color-icon-primary: var(--mantine-color-dark-8);
|
||||
|
|
|
|||
Loading…
Reference in a new issue