@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
766 lines (765 loc) • 44.3 kB
JavaScript
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" }) }) }) })] }));
});