import { Button, makeStyles, Menu, MenuButton, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, shorthands, SplitButton } from "@fluentui/react-components"; import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons"; import { configContext, Platform } from "ConfigContext"; import Explorer from "Explorer/Explorer"; import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel"; import { Tabs } from "Explorer/Tabs/Tabs"; import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; import { useDatabases } from "Explorer/useDatabases"; import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts"; import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils"; import { Allotment, AllotmentHandle } from "allotment"; import { useSidePanel } from "hooks/useSidePanel"; import { useTheme } from "hooks/useTheme"; import { debounce } from "lodash"; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { ResourceTree } from "./Tree/ResourceTree"; const useSidebarStyles = makeStyles({ sidebar: { height: "100%", }, sidebarContainer: { height: "100%", width: "100%", borderRight: `1px solid ${tokens.colorNeutralStroke1}`, transition: "all 0.2s ease-in-out", display: "flex", flexDirection: "column", backgroundColor: tokens.colorNeutralBackground1, position: "relative", }, expandedContent: { display: "grid", height: "100%", width: "100%", gridTemplateRows: `calc(${tokens.layoutRowHeight} * 2) 1fr`, }, floatingControlsContainer: { position: "absolute", top: 0, right: 0, zIndex: 1000, width: "auto", padding: tokens.spacingHorizontalS, }, floatingControls: { display: "flex", flexDirection: "row", gap: tokens.spacingHorizontalXS, }, floatingControlButton: { ...shorthands.border("none"), backgroundColor: "transparent", color: tokens.colorNeutralForeground1, cursor: "pointer", padding: tokens.spacingHorizontalXS, borderRadius: tokens.borderRadiusMedium, display: "flex", alignItems: "center", justifyContent: "center", ":hover": { backgroundColor: tokens.colorNeutralBackground1Hover, color: tokens.colorNeutralForeground1, }, ":active": { backgroundColor: tokens.colorNeutralBackground1Pressed, color: tokens.colorNeutralForeground1, }, ":disabled": { color: tokens.colorNeutralForegroundDisabled, cursor: "not-allowed", }, }, globalCommandsContainer: { display: "grid", alignItems: "center", justifyItems: "center", width: "100%", containerType: "size", padding: tokens.spacingHorizontalS, ...cosmosShorthands.borderBottom(), backgroundColor: tokens.colorNeutralBackground1, }, loadingProgressBar: { position: "absolute", width: "100%", height: "2px", zIndex: 2000, backgroundColor: tokens.colorCompoundBrandBackground, animationIterationCount: "infinite", animationDuration: "3s", animationName: { "0%": { opacity: ".2", }, "50%": { opacity: "1", }, "100%": { opacity: ".2", }, }, }, globalCommandsMenuButton: { display: "inline-flex", "@container (min-width: 250px)": { display: "none", }, }, globalCommandsSplitButton: { display: "none", "@container (min-width: 250px)": { display: "flex", }, }, treeContainer: { flex: 1, overflow: "auto", paddingLeft: tokens.spacingHorizontalM, backgroundColor: tokens.colorNeutralBackground1, color: tokens.colorNeutralForeground1, }, }); interface GlobalCommandsProps { explorer: Explorer; } interface GlobalCommand { id: string; label: string; icon: JSX.Element; onClick: () => void; keyboardAction?: KeyboardAction; ref?: React.RefObject; } const GlobalCommands: React.FC = ({ explorer }) => { const styles = useSidebarStyles(); // Since we have two buttons in the DOM (one for small screens and one for larger screens), we wrap the entire thing in a div. // However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu. // We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render. const [globalCommandButton, setGlobalCommandButton] = useState(null); const primaryFocusableRef = useRef(null); const actions = useMemo(() => { if ( (isFabric() && userContext.fabricContext?.isReadOnly) || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ) { // No Global Commands for these API types. // In fact, no sidebar for Postgres or VCoreMongo at all, but just in case, we check here anyway. return []; } const actions: GlobalCommand[] = [ { id: "new_collection", label: `New ${getCollectionName()}`, icon: , onClick: () => { const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined; explorer.onNewCollectionClicked({ databaseId }); }, keyboardAction: KeyboardAction.NEW_COLLECTION, }, ]; if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") { actions.push({ id: "new_database", label: `New ${getDatabaseName()}`, icon: , onClick: async () => { const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; if (throughputCap && throughputCap !== -1) { await useDatabases.getState().loadAllOffers(); } useSidePanel.getState().openSidePanel("New " + getDatabaseName(), ); }, keyboardAction: KeyboardAction.NEW_DATABASE, }); } return actions; }, [explorer]); const primaryAction = useMemo(() => (actions.length > 0 ? actions[0] : undefined), [actions]); const onPrimaryActionClick = useCallback(() => primaryAction?.onClick(), [primaryAction]); const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.GLOBAL_COMMANDS); useEffect(() => { setKeyboardActions( actions.reduce( (acc, action) => { if (action.keyboardAction) { acc[action.keyboardAction] = action.onClick; } return acc; }, {} as Record, ), ); }, [actions, setKeyboardActions]); useLayoutEffect(() => { if (primaryFocusableRef.current) { const timer = setTimeout(() => { primaryFocusableRef.current.focus(); }, 0); return () => clearTimeout(timer); } return undefined; }, []); if (!primaryAction) { return null; } return (
{actions.length === 1 ? ( ) : ( {(triggerProps: MenuButtonProps) => (
{primaryAction.label} New...
)}
{actions.map((action) => ( {action.label} ))}
)}
); }; interface SidebarProps { explorer: Explorer; } const CollapseThreshold = 140; export const SidebarContainer: React.FC = ({ explorer }) => { const styles = useSidebarStyles(); const [expanded, setExpanded] = React.useState(true); const [loading, setLoading] = React.useState(false); const [expandedSize, setExpandedSize] = React.useState(300); const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo"; const allotment = useRef(null); const { isDarkMode } = useTheme(); const expand = useCallback(() => { if (!expanded) { allotment.current.resize([expandedSize, Infinity]); setExpanded(true); } }, [expanded, expandedSize, setExpanded]); const collapse = useCallback(() => { if (expanded) { allotment.current.resize([24, Infinity]); setExpanded(false); } }, [expanded, setExpanded]); const onChange = debounce((sizes: number[]) => { if (expanded && sizes[0] <= CollapseThreshold) { collapse(); } else if (!expanded && sizes[0] > CollapseThreshold) { expand(); } }, 10); const onDragEnd = useCallback( (sizes: number[]) => { if (expanded) { // Remember the last size we had when expanded setExpandedSize(sizes[0]); } else { allotment.current.resize([24, Infinity]); } }, [expanded, setExpandedSize], ); const onRefreshClick = useCallback(async () => { setLoading(true); await explorer.onRefreshResourcesClick(); setLoading(false); }, [setLoading]); const hasGlobalCommands = !( isFabricMirrored() || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ); return (
{/* Collections Tree - Start */} {hasSidebar && ( // When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
{loading && ( // The Fluent UI progress bar has some issues in reduced-motion environments so we use a simple CSS animation here. // https://github.com/microsoft/fluentui/issues/29076
)} {expanded ? ( <>
{hasGlobalCommands && }
) : ( )}
)}
); };