UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

766 lines (765 loc) 44.3 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /* * Copyright 2025 The Kubernetes Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Icon } from '@iconify/react'; import { alpha, Box, Button, ClickAwayListener, IconButton, ListItemIcon, ListItemText, MenuItem, MenuList, Paper, Popper, Tooltip, Typography, useMediaQuery, useTheme, } from '@mui/material'; import { clamp, throttle } from 'lodash'; import React, { createContext, Suspense, useCallback, useContext, useEffect, useRef, useState, } from 'react'; import { createPortal } from 'react-dom'; import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import { useTypedSelector } from '../../redux/hooks'; import store from '../../redux/stores/store'; import { activitySlice } from './activitySlice'; const areWindowsEnabled = false; export const Activity = { /** Launches new Activity */ launch(activity) { store.dispatch(activitySlice.actions.launchActivity(activity)); }, /** Closes activity */ close(id) { store.dispatch(activitySlice.actions.close(id)); }, /** Update existing activity with a partial changes */ update(id, diff) { store.dispatch(activitySlice.actions.update({ ...diff, id })); }, reset() { store.dispatch(activitySlice.actions.reset()); }, }; /** Context for the currently viewed activity */ const ActivityContext = createContext({}); /** Control activity from within, requires to be used within an existing Activity */ export const useActivity = () => { const activity = useContext(ActivityContext); const update = useCallback((changes) => Activity.update(activity.id, changes), [activity.id]); return [activity, update]; }; /** Renders a single activity */ export function SingleActivityRenderer({ activity, zIndex, index, isOverview, onClick, }) { const { id, minimized, location, content, title, hideTitleInHeader, icon, cluster } = activity; const { t } = useTranslation(); const activityElementRef = useRef(null); const containerElementRef = useRef(document.getElementById('main')); const theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')); const [snapMenuAnchor, setSnapMenuAnchor] = useState(null); const [snapMenuOpenReason, setSnapMenuOpenReason] = useState(null); const openTimerRef = useRef(null); const closeTimerRef = useRef(null); const menuListRef = useRef(null); const isSnapMenuOpen = Boolean(snapMenuAnchor); useEffect(() => { containerElementRef.current = document.getElementById('main'); }, []); // Cleanup timers on unmount useEffect(() => { return () => { if (openTimerRef.current) { clearTimeout(openTimerRef.current); } if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); } }; }, []); // Styles of different activity locations const locationStyles = { full: { borderColor: 'transparent', boxShadow: 'none', borderRadius: 0, position: 'absolute', left: 0, width: '100%', height: '100%', }, 'split-right': { position: 'absolute', transform: 'translateX(100%)', width: '50%', height: '100%', gridColumn: '2 / 4', }, 'split-left': { position: 'absolute', width: '50%', height: '100%', gridColumn: '2 / 4', }, 'split-top': { position: 'absolute', top: 0, left: 0, width: '100%', height: '50%', borderBottom: '1px solid', }, 'split-bottom': { position: 'absolute', bottom: 0, left: 0, width: '100%', height: '50%', }, window: { position: 'absolute', width: '50%', height: '70%', gridColumn: '2 / 4', border: '1px solid', borderTop: '1px solid', borderBottom: '1px solid', borderRadius: '10px', }, }[location ?? 'full']; // Reset styles when switching to window location useEffect(() => { const container = activityElementRef.current; if (!container) return; if (location !== 'window') { container.style.transform = ''; container.style.width = ''; container.style.height = ''; } }, [location]); // Toggle overview styles useEffect(() => { const activity = activityElementRef.current; const container = containerElementRef.current; if (!activity || !container) return; let oldTranslation; let oldHeight; let oldWidth; if (isOverview) { const cols = 3; const rows = 5; const gapPx = 20; const box = container.getBoundingClientRect(); const x = (box.width / cols) * (index % 3) + gapPx; const y = (box.height / rows) * Math.floor(index / 3) + gapPx; const width = box.width / cols - gapPx * (cols - 2); const height = box.height / rows - gapPx * (rows - 2); oldTranslation = activity.style.transform ?? ''; oldHeight = activity.style.height; oldWidth = activity.style.width; activity.style.width = width + 'px'; activity.style.height = height + 'px'; activity.style.transform = `translate(${x}px, ${y}px)`; } return () => { if (oldTranslation !== undefined) { activity.style.transform = oldTranslation; activity.style.width = oldWidth; activity.style.height = oldHeight; } }; }, [isOverview, index]); // Move focus inside the Activity useEffect(() => { if (!minimized && activityElementRef.current) { // Find first focusable element const focusableElements = activityElementRef.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); const firstElement = focusableElements[0]; if (firstElement && 'focus' in firstElement && typeof firstElement.focus === 'function') { firstElement.focus(); } } }, [minimized]); // Save last used location const lastNonFullscreenLocation = useRef(); useEffect(() => { return () => { if (location !== 'full') { lastNonFullscreenLocation.current = location; } }; }, [location]); // Handle click outside to close temporary activities const selectedResource = useTypedSelector(state => state.drawerMode.selectedResource); const isDetailDrawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled); const isDetailsDrawerSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isDetailsDrawerOpen = !!selectedResource && isDetailDrawerEnabled && !isDetailsDrawerSmallScreen; useEffect(() => { function handleClickOutside(event) { const target = event.target; const isClickInside = activityElementRef.current?.contains(target); if (activity.temporary && !minimized && !isOverview && activityElementRef.current && !isClickInside && !isDetailsDrawerOpen) { Activity.close(id); } } if (activity.temporary && !minimized && !isOverview) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [activity.temporary, minimized, isOverview, id, location, isDetailsDrawerOpen]); return (_jsx(ActivityContext.Provider, { value: activity, children: _jsx(Box, { role: "complementary", "aria-label": typeof title === 'string' ? title : undefined, sx: { display: minimized && !isOverview ? 'none' : undefined, gridColumn: '2 / 3', gridRow: '1 / 2', }, children: _jsxs(Box, { ref: activityElementRef, onPointerDownCapture: e => { if (isOverview) { e.stopPropagation(); e.preventDefault(); } Activity.update(id, { temporary: false }); onClick(e); }, sx: theme => ({ display: 'flex', opacity: minimized && !isOverview ? 0 : 1, flexDirection: 'column', background: theme.palette.background.default, border: '1px solid', borderTop: 'none', borderBottom: 'none', willChange: 'top, left, width, height', zIndex: zIndex ?? 3, boxShadow: theme.palette.mode === 'light' ? '0px 0px 15px rgba(0,0,0,0.15)' : '0px 0px 15px rgba(0,0,0,0.7)', gridColumn: '2 / 3', gridRow: '1 / 2', ...locationStyles, ...(isOverview ? { borderRadius: '20px', cursor: 'pointer', top: 0, left: 0, right: 'auto', bottom: 'auto', ':hover': { boxShadow: theme.palette.mode === 'light' ? '0px 0px 15px rgba(0,0,0,0.25)' : '0px 0px 15px rgba(0,0,0,0.17)', }, } : {}), borderColor: theme.palette.divider, transitionDuration: '0.25s', transitionProperty: 'width,height,left,top,transform', }), children: [isOverview && (_jsxs(Box, { sx: { fontSize: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 1, gap: 1, height: '100%', }, children: [_jsx(Box, { sx: { width: '48px', height: '48px', flexShrink: 0 }, children: icon }), " ", title] })), _jsxs(_Fragment, { children: [!minimized && !isOverview && !isSmallScreen && areWindowsEnabled && (_jsx(ActivityDragger, { activityElementRef: activityElementRef, containerElementRef: containerElementRef, zIndex: zIndex, location: location, onLocationChange: location => { Activity.update(id, { location }); } })), _jsxs(Box, { sx: { display: isOverview ? 'none' : 'flex', gap: 1, alignItems: 'center', height: '40px', padding: '0 16px', flexShrink: 0, }, children: [!hideTitleInHeader && (_jsxs(_Fragment, { children: [_jsx(Box, { sx: { width: '18px', height: '18px' }, children: icon }), _jsx(Typography, { color: "textSecondary", fontSize: 14, sx: { maxWidth: 'calc(45% - 60px)', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden', }, title: typeof title === 'string' ? title : undefined, children: title })] })), _jsx(Box, { sx: { marginRight: 'auto' } }), cluster && (_jsxs(Box, { sx: theme => ({ display: 'flex', alignItems: 'center', fontSize: '0.875rem', gap: 0.25, paddingX: 0.5, color: theme.palette.text.secondary, }), children: [_jsx(Icon, { icon: "mdi:hexagon-multiple-outline" }), cluster] })), !isOverview && (_jsxs(_Fragment, { children: [_jsx(IconButton, { size: "small", title: t('Window'), "aria-haspopup": "menu", "aria-expanded": isSnapMenuOpen, "aria-controls": isSnapMenuOpen ? 'snap-menu' : undefined, onMouseEnter: event => { // Clear any existing close timer if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } // Set 350ms timer to open on hover if (openTimerRef.current) { clearTimeout(openTimerRef.current); } const target = event.currentTarget; openTimerRef.current = setTimeout(() => { setSnapMenuAnchor(target); setSnapMenuOpenReason('hover'); openTimerRef.current = null; }, 350); }, onMouseLeave: () => { // Cancel open timer if pointer leaves before 350ms if (openTimerRef.current) { clearTimeout(openTimerRef.current); openTimerRef.current = null; } // If menu is open due to hover, start close timer if (snapMenuOpenReason === 'hover' && isSnapMenuOpen) { if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); } closeTimerRef.current = setTimeout(() => { setSnapMenuAnchor(null); setSnapMenuOpenReason(null); closeTimerRef.current = null; }, 150); } }, onClick: event => { // Clear any timers if (openTimerRef.current) { clearTimeout(openTimerRef.current); openTimerRef.current = null; } if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } // Toggle menu on click if (isSnapMenuOpen && snapMenuOpenReason === 'click') { setSnapMenuAnchor(null); setSnapMenuOpenReason(null); } else { setSnapMenuAnchor(event.currentTarget); setSnapMenuOpenReason('click'); } }, onKeyDown: event => { if (event.key === 'Enter' || event.key === 'ArrowDown') { event.preventDefault(); if (openTimerRef.current) { clearTimeout(openTimerRef.current); openTimerRef.current = null; } if (!isSnapMenuOpen) { setSnapMenuAnchor(event.currentTarget); setSnapMenuOpenReason('click'); } } }, children: _jsx(Icon, { icon: "mdi:dock-window" }) }), _jsx(Popper, { id: "snap-menu", open: isSnapMenuOpen, anchorEl: snapMenuAnchor, placement: "bottom-end", sx: { zIndex: theme.zIndex.modal }, children: _jsx(ClickAwayListener, { onClickAway: () => { if (snapMenuOpenReason === 'click') { setSnapMenuAnchor(null); setSnapMenuOpenReason(null); } }, children: _jsx(Paper, { elevation: 8, onMouseEnter: () => { // Cancel close timer when entering menu (for hover-open case) if (closeTimerRef.current && snapMenuOpenReason === 'hover') { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } }, onMouseLeave: () => { // Start close timer when leaving menu (for hover-open case) if (snapMenuOpenReason === 'hover') { if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); } closeTimerRef.current = setTimeout(() => { setSnapMenuAnchor(null); setSnapMenuOpenReason(null); closeTimerRef.current = null; }, 150); } }, onKeyDown: event => { if (event.key === 'Escape') { setSnapMenuAnchor(null); setSnapMenuOpenReason(null); snapMenuAnchor?.focus(); } }, children: _jsxs(MenuList, { ref: menuListRef, "aria-label": t('Window'), autoFocusItem: isSnapMenuOpen, children: [_jsxs(MenuItem, { selected: location === 'full', "aria-label": t('Fullscreen'), title: t('Fullscreen'), onClick: () => { Activity.update(id, { location: 'full' }); setSnapMenuAnchor(null); setSnapMenuOpenReason(null); }, children: [_jsx(ListItemIcon, { children: _jsx(Icon, { icon: "mdi:fullscreen" }) }), _jsx(ListItemText, { children: t('Fullscreen') })] }), _jsxs(MenuItem, { selected: location === 'split-left', "aria-label": t('Snap Left'), title: t('Snap Left'), onClick: () => { Activity.update(id, { location: 'split-left' }); setSnapMenuAnchor(null); setSnapMenuOpenReason(null); }, children: [_jsx(ListItemIcon, { children: _jsx(Icon, { icon: "mdi:dock-left" }) }), _jsx(ListItemText, { children: t('Snap Left') })] }), _jsxs(MenuItem, { selected: location === 'split-right', "aria-label": t('Snap Right'), title: t('Snap Right'), onClick: () => { Activity.update(id, { location: 'split-right' }); setSnapMenuAnchor(null); setSnapMenuOpenReason(null); }, children: [_jsx(ListItemIcon, { children: _jsx(Icon, { icon: "mdi:dock-right" }) }), _jsx(ListItemText, { children: t('Snap Right') })] }), _jsxs(MenuItem, { selected: location === 'split-top', "aria-label": t('Snap Top'), title: t('Snap Top'), onClick: () => { Activity.update(id, { location: 'split-top' }); setSnapMenuAnchor(null); setSnapMenuOpenReason(null); }, children: [_jsx(ListItemIcon, { children: _jsx(Icon, { icon: "mdi:dock-top" }) }), _jsx(ListItemText, { children: t('Snap Top') })] }), _jsxs(MenuItem, { selected: location === 'split-bottom', "aria-label": t('Snap Bottom'), title: t('Snap Bottom'), onClick: () => { Activity.update(id, { location: 'split-bottom' }); setSnapMenuAnchor(null); setSnapMenuOpenReason(null); }, children: [_jsx(ListItemIcon, { children: _jsx(Icon, { icon: "mdi:dock-bottom" }) }), _jsx(ListItemText, { children: t('Snap Bottom') })] })] }) }) }) }), _jsx(IconButton, { onClick: () => { Activity.update(id, { minimized: true }); }, size: "small", title: t('Minimize'), children: _jsx(Icon, { icon: "mdi:minimize" }) }), _jsx(IconButton, { onClick: () => Activity.close(id), size: "small", title: t('Close'), children: _jsx(Icon, { icon: "mdi:close" }) })] }))] }), _jsx(Suspense, { fallback: null, children: _jsx(Box, { sx: { display: isOverview ? 'none' : 'flex', overflowY: 'auto', scrollbarGutter: 'stable', scrollbarWidth: 'thin', flexGrow: 1, flexDirection: 'column', }, children: content }) }), location === 'window' && _jsx(ActivityResizer, { activityElementRef: activityElementRef })] })] }) }) })); } const minHeight = 200; const minWidth = 400; /** Corner resize component */ function ActivityResizer({ activityElementRef }) { return (_jsx(Box, { sx: { width: '44px', height: '44px', position: 'absolute', zIndex: 1, bottom: '-10px', right: '-10px', padding: '0 12px 12px 0', cursor: 'nwse-resize', touchAction: 'none', }, onPointerDown: e => { e.preventDefault(); const activityElement = activityElementRef.current; if (!activityElement) return; const onMoveCallback = throttle(() => { window?.dispatchEvent?.(new Event('resize')); }, 100); const pointerX = e.clientX; const pointerY = e.clientY; const startWidth = activityElement.getBoundingClientRect().width; const startHeight = activityElement.getBoundingClientRect().height; // Disable transition while resizing const oldTransitionDuration = activityElement.style.transitionDuration; activityElement.style.transitionDuration = '0s'; const handleMove = (e) => { const dx = e.clientX - pointerX; const dy = e.clientY - pointerY; const width = Math.max(minWidth, startWidth + dx); const height = Math.max(minHeight, startHeight + dy); activityElement.style.width = width + 'px'; activityElement.style.height = height + 'px'; onMoveCallback(); }; document.addEventListener('pointermove', handleMove); document.addEventListener('pointerup', () => { document.removeEventListener('pointermove', handleMove); activityElement.style.transitionDuration = oldTransitionDuration; }, { once: true }); }, children: _jsx(Icon, { icon: "mdi:resize-bottom-right", width: "100%", height: "100%", style: { pointerEvents: 'none', opacity: 0.6 } }) })); } /** Component for dragging activity */ function ActivityDragger({ activityElementRef, containerElementRef, location, onLocationChange, zIndex, }) { const previewRef = useRef(null); return (_jsxs(_Fragment, { children: [createPortal(_jsx(Box, { ref: previewRef, sx: theme => ({ zIndex: zIndex - 1, pointerEvents: 'none', position: 'fixed', top: 0, left: 0, background: 'rgba(0,0,0,0.7)', border: '1px solid', borderColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)', borderRadius: '10px', willChange: 'width, height, transform, opacity', opacity: 0, transition: 'all 0.25s cubic-bezier(0.25, 0.1, 0.25, 1)', }) }), document.body), _jsx(Box, { sx: theme => ({ top: '3px', cursor: 'grab', left: '50%', position: 'absolute', transform: 'translateX(-50%)', zIndex: 1, touchAction: 'none', background: theme.palette.background.default, }), // Toggle fullscreen on double-click onDoubleClick: () => onLocationChange(location === 'full' ? 'window' : 'full'), // Start dragging onPointerDown: e => { e.preventDefault(); const container = containerElementRef.current; if (!container) return; const containerBox = container.getBoundingClientRect(); const activityElement = activityElementRef.current; if (!activityElement) return; // Remember start position const startX = activityElement.getBoundingClientRect().left - containerBox.left; const startY = activityElement.getBoundingClientRect().top - containerBox.top; const pointerX = e.clientX; const pointerY = e.clientY; // Disable transitions during dragging const oldTransitionDuration = activityElement.style.transitionDuration; activityElement.style.transitionDuration = '0s'; let newLocation; // Update position of the backdrop preview element const updatePreview = throttle((newPreview) => { const preview = previewRef.current; if (!preview) return; preview.style.transform = `translate(${newPreview.left}px, ${newPreview.top}px)`; preview.style.width = newPreview.width + 'px'; preview.style.height = newPreview.height + 'px'; preview.style.opacity = String(newPreview.opacity); }, 50); // Drag the activity on pointer move const handleMove = (e) => { // Calculate difference from start const dx = e.clientX - pointerX; const dy = e.clientY - pointerY; // New window position const x = startX + dx; const y = startY + dy; if (e.clientX < containerBox.left + containerBox.width * 0.05) { // Snap to left newLocation = 'split-left'; updatePreview({ location: newLocation, top: containerBox.top, left: containerBox.left, width: containerBox.width / 2, height: containerBox.height, opacity: 1, }); } else if (e.clientX > containerBox.left + containerBox.width * 0.95) { // Snap to right newLocation = 'split-right'; updatePreview({ location: newLocation, top: containerBox.top, left: containerBox.left + containerBox.width / 2, width: containerBox.width / 2, height: containerBox.height, opacity: 1, }); } else if (e.clientY < containerBox.top + 10) { // Snap fullscreen newLocation = 'full'; updatePreview({ location: newLocation, top: containerBox.top, left: containerBox.left, width: containerBox.width, height: containerBox.height, opacity: 1, }); } else { // In every other case turn activity into a window newLocation = 'window'; const box = activityElement.getBoundingClientRect(); updatePreview({ location: newLocation, top: box.top, left: box.left, width: box.width, height: box.height, opacity: 0, }); } // Apply transform to the Activity window activityElement.style.transform = `translate(${x}px, ${clamp(y, 0, containerBox.height - 30)}px)`; activityElement.style.boxShadow = '0px 0px 15px rgba(0,0,0,0.15)'; activityElement.style.borderRadius = '10px'; }; document.addEventListener('pointermove', handleMove); document.addEventListener('pointerup', () => { if (newLocation) { // Update location after dragging is finished onLocationChange(newLocation); } // Reset all styles if (newLocation !== undefined && newLocation !== 'window') { activityElement.style.transform = ``; } activityElement.style.boxShadow = ''; activityElement.style.borderRadius = ''; activityElement.style.transitionDuration = oldTransitionDuration; updatePreview({ opacity: 0 }); // Remove event listener for dragging document.removeEventListener('pointermove', handleMove); }, { once: true }); }, children: _jsx(Box, { sx: theme => ({ pointerEvents: 'none', width: '80px', height: '10px', margin: '10px', backgroundImage: `radial-gradient(${alpha(theme.palette.text.primary, 0.35)} 1px, transparent 0)`, backgroundSize: '6px 6px', }) }) })] })); } /** Renders all activities and the taskbar */ export const ActivitiesRenderer = React.memo(function ActivitiesRenderer() { const activities = Object.values(useTypedSelector(state => state.activity.activities)); const history = useTypedSelector(state => state.activity.history); const lastElement = history.at(-1); const [isOverview, setIsOverview] = useState(false); const location = useLocation(); const locationRef = useRef(location.pathname); // Minimize activities that block the main content on route change. // Activities in 'split-right' location are kept visible since they // allow the user to see most of the main content. useEffect(() => { activities.forEach(activity => { // If we were triggered just because of the activities changing but the // location hasn't changed, we have nothing to do. if (locationRef.current === location.pathname) { return; } if (activity.location !== 'split-right' && !activity.minimized) { Activity.update(activity.id, { minimized: true }); } }); locationRef.current = location.pathname; }, [location.pathname, activities]); useEffect(() => { if (activities.length === 0 && isOverview) { // eslint-disable-next-line react-hooks/set-state-in-effect setIsOverview(false); } }, [activities, isOverview]); useHotkeys('Ctrl+ArrowDown', () => { setIsOverview(isOverview => !isOverview); }); useHotkeys('Ctrl+ArrowLeft', () => { if (lastElement) { Activity.update(lastElement, { location: 'split-left' }); } }); useHotkeys('Ctrl+ArrowRight', () => { if (lastElement) { Activity.update(lastElement, { location: 'split-right' }); } }); useHotkeys('Ctrl+Shift+ArrowUp', () => { if (lastElement) { Activity.update(lastElement, { location: 'split-top' }); } }); useHotkeys('Ctrl+Shift+ArrowDown', () => { if (lastElement) { Activity.update(lastElement, { location: 'split-bottom' }); } }); useHotkeys('Ctrl+ArrowUp', () => { if (lastElement) { Activity.update(lastElement, { location: 'full' }); } }); return (_jsxs(_Fragment, { children: [_jsx(Box, { sx: { background: 'rgba(0,0,0,0.1)', backdropFilter: 'blur(5px) saturate(1.2)', gridColumn: '2/3', gridRow: '1/2', display: isOverview ? 'block' : 'none', zIndex: 1, } }), activities.map((it, i) => (_jsx(SingleActivityRenderer, { activity: it, zIndex: 4 + history.indexOf(it.id), index: i, isOverview: isOverview, onClick: () => { if (isOverview) { setIsOverview(false); Activity.update(it.id, { minimized: false }); } } }, it.id))), isOverview && (_jsx(Box, { sx: { zIndex: 1, gridColumn: '2/3', gridRow: '1/2', display: 'flex', alignItems: 'flex-end', justifyContent: 'center', }, children: _jsx(Button, { size: "large", variant: "contained", startIcon: _jsx(Icon, { icon: "mdi:close-box-multiple-outline" }), onClick: () => { Activity.reset(); setIsOverview(false); }, sx: { margin: 5, lineHeight: 1, }, children: _jsx(Trans, { children: "Close All" }) }) })), _jsx(ActivityBar, { setIsOverview: setIsOverview })] })); }); /** Taskbar with all current activities */ export const ActivityBar = React.memo(function ({ setIsOverview, }) { const { t } = useTranslation(); const activities = Object.values(useTypedSelector(state => state.activity.activities)); const history = useTypedSelector(state => state.activity.history); const lastElement = history.at(-1); if (activities.length === 0) return null; return (_jsxs(Box, { sx: theme => ({ background: theme.palette.background.muted, borderTop: '1px solid', borderColor: theme.palette.divider, gridRow: '2 / 3', gridColumn: '2 / 3', paddingLeft: 1, zIndex: 10, position: 'relative', alignItems: 'center', display: 'flex', minHeight: '56px', overflowX: 'auto', scrollbarWidth: 'thin', }), children: [[...activities.reverse()].map(it => (_jsxs(Box, { sx: theme => ({ display: 'flex', alignItems: 'center', padding: '3px', height: '100%', position: 'relative', border: '1px solid', borderTop: 0, borderColor: lastElement === it.id ? theme.palette.divider : 'transparent', background: lastElement === it.id ? theme.palette.background.default : 'transparent', }), children: [_jsxs(Button, { sx: theme => ({ height: '100%', padding: '0px 5px 0 10px', lineHeight: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', justifyContent: 'start', color: theme.palette.text.primary, }), onClick: () => { // Minimize or show Activity, unless it's not active then bring it to front Activity.update(it.id, { minimized: it.id !== lastElement ? false : !it.minimized }); }, onMouseDown: e => { if (e.button === 1) { Activity.close(it.id); } }, children: [_jsx(Box, { sx: { width: '22px', height: '22px', flexShrink: 0, marginRight: 1 }, children: it.icon }), _jsxs(Box, { sx: { marginRight: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 0.5, overflow: 'hidden', }, children: [it.cluster && (_jsx(Box, { sx: theme => ({ color: theme.palette.text.secondary }), children: it.cluster })), _jsx(Box, { sx: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontStyle: it.temporary ? 'italic' : undefined, }, children: it.title ?? 'Something' })] })] }), _jsx(IconButton, { size: "small", onClick: e => { e.preventDefault(); e.stopPropagation(); Activity.close(it.id); }, sx: { width: '42px', height: '100%', borderRadius: 1, flexShrink: 0 }, "aria-label": "Close", children: _jsx(Icon, { icon: "mdi:close" }) })] }, it.id))), _jsx(Box, { sx: theme => ({ marginLeft: 'auto', flexShrink: 0, position: 'sticky', right: 0, background: theme.palette.background.muted, }), children: _jsx(Tooltip, { title: t('Overview'), children: _jsx(IconButton, { onClick: () => setIsOverview(it => !it), "aria-label": t('Overview'), children: _jsx(Icon, { icon: "mdi:grid-large" }) }) }) })] })); });