diff --git a/ui/packages/shared/components/src/ParcaContext/index.tsx b/ui/packages/shared/components/src/ParcaContext/index.tsx index a42c2fba636..eb236b02089 100644 --- a/ui/packages/shared/components/src/ParcaContext/index.tsx +++ b/ui/packages/shared/components/src/ParcaContext/index.tsx @@ -72,6 +72,7 @@ interface Props { timezone?: string; preferencesModal?: boolean; checkDebuginfoStatusHandler?: (buildId: string) => void; + iciclechartHelpText?: ReactNode; } export const defaultValue: Props = { diff --git a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx index ec21e43527d..db07e8dbc48 100644 --- a/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx +++ b/ui/packages/shared/profile/src/ProfileIcicleGraph/index.tsx @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, {LegacyRef, useEffect, useMemo, useState} from 'react'; +import React, {LegacyRef, ReactNode, useEffect, useMemo, useState} from 'react'; import {AnimatePresence, motion} from 'framer-motion'; import {useMeasure} from 'react-use'; @@ -55,8 +55,12 @@ interface ProfileIcicleGraphProps { isIcicleChart?: boolean; } -const ErrorContent = ({errorMessage}: {errorMessage: string}): JSX.Element => { - return
{errorMessage}
; +const ErrorContent = ({errorMessage}: {errorMessage: string | ReactNode}): JSX.Element => { + return ( +
+ {errorMessage} +
+ ); }; export const validateIcicleChartQuery = ( @@ -85,7 +89,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ isIcicleChart = false, profileSource, }: ProfileIcicleGraphProps): JSX.Element { - const {onError, authenticationErrorMessage, isDarkMode} = useParcaContext(); + const {onError, authenticationErrorMessage, isDarkMode, iciclechartHelpText} = useParcaContext(); const {compareMode} = useProfileViewContext(); const [isLoading, setIsLoading] = useState(true); const [icicleChartRef, {height: icicleChartHeight}] = useMeasure(); @@ -176,13 +180,41 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ // Do necessary checks to ensure that icicle chart can be rendered for this query. if (isInvalidIcicleChartQuery) { if (isNonDelta) { - return ; + return ( + + To use the Icicle chart, please switch to a Delta profile. + {iciclechartHelpText ?? null} + + } + /> + ); } else if (isDurationTooLong) { return ( - + + + Icicle chart is unavailable for queries longer than one minute. Please select a + point in the metrics graph to continue. + + {iciclechartHelpText ?? null} + + } + /> ); } else { - return ; + return ( + + The Icicle chart is not available for this query. + {iciclechartHelpText ?? null} + + } + /> + ); } } @@ -261,6 +293,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ profileSource, icicleChartHeight, icicleChartRef, + iciclechartHelpText, ]); if (error != null) { diff --git a/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx b/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx index 1d3d166a0cc..cbd8c9bfe9f 100644 --- a/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx +++ b/ui/packages/shared/profile/src/ProfileView/components/Toolbars/index.tsx @@ -174,7 +174,7 @@ export const VisualisationToolbar: FC = ({ profileViewExternalSubActions={profileViewExternalSubActions} /> - {showVisualizationSelector ? : null} + {showVisualizationSelector ? : null} {isGraphViz && !isTableViz && ( diff --git a/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/Dropdown.tsx b/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/Dropdown.tsx index 720e4531b1e..da154407b7a 100644 --- a/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/Dropdown.tsx +++ b/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/Dropdown.tsx @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Fragment} from 'react'; +import {Fragment, useState} from 'react'; import {Listbox, Transition} from '@headlessui/react'; import {Icon} from '@iconify/react'; @@ -26,7 +26,9 @@ export interface DropdownElement { export interface DropdownItem { key: string; + id?: string; disabled?: boolean; + disabledText?: string; element: DropdownElement; innerAction?: InnerAction; } @@ -115,61 +117,7 @@ const Dropdown = ({
{loader}
) : ( items.length > 0 && - items.map(option => ( - - cx( - active && 'bg-indigo-600 text-white', - 'relative flex cursor-default select-none py-2 px-3' - ) - } - value={option.key} - > - {({selected, active, disabled}) => ( -
-
- - {option.element.expanded} - -
- {option.innerAction !== undefined && ( - - )} - {selected ? ( - - - ) : null} -
- )} -
- )) + items.map(option => ) )} @@ -179,4 +127,74 @@ const Dropdown = ({ ); }; +const DropdownOption = ({option}: {option: DropdownItem}): JSX.Element => { + const [isMouseOver, setIsMouseOver] = useState(false); + + return ( + + cx( + active && 'bg-indigo-600 text-white', + 'relative flex cursor-default select-none py-2 px-3' + ) + } + value={option.key} + > + {({selected, active, disabled}) => ( +
setIsMouseOver(true)} + onMouseLeave={() => setIsMouseOver(false)} + > + {isMouseOver && disabled && option.disabledText != null ? ( +
setIsMouseOver(false)} + > + {option.disabledText} +
+
+ ) : null} +
+ + {option.element.expanded} + +
+ {option.innerAction !== undefined && ( + + )} + {selected ? ( + + + ) : null} +
+ )} +
+ ); +}; + export default Dropdown; diff --git a/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/index.tsx b/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/index.tsx index 67cbca62ba7..521054a39d3 100644 --- a/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/index.tsx +++ b/ui/packages/shared/profile/src/ProfileView/components/ViewSelector/index.tsx @@ -16,9 +16,14 @@ import {ReactNode} from 'react'; import {useParcaContext, useURLState} from '@parca/components'; import {USER_PREFERENCES, useUserPreference} from '@parca/hooks'; +import {ProfileSource} from '../../../ProfileSource'; import Dropdown, {DropdownElement, InnerAction} from './Dropdown'; -const ViewSelector = (): JSX.Element => { +interface Props { + profileSource?: ProfileSource; +} + +const ViewSelector = ({profileSource}: Props): JSX.Element => { const [dashboardItems = ['icicle'], setDashboardItems] = useURLState( 'dashboard_items', { @@ -34,6 +39,7 @@ const ViewSelector = (): JSX.Element => { label?: string | ReactNode; canBeSelected: boolean; supportingText?: string; + disabledText?: string; }> = [ {key: 'table', label: 'Table', canBeSelected: !dashboardItems.includes('table')}, {key: 'icicle', label: 'icicle', canBeSelected: !dashboardItems.includes('icicle')}, @@ -43,16 +49,21 @@ const ViewSelector = (): JSX.Element => { key: 'iciclechart', label: ( - IcicleChart + Iciclechart  alpha ), - canBeSelected: !dashboardItems.includes('iciclechart'), + canBeSelected: + !dashboardItems.includes('iciclechart') && profileSource?.ProfileType().delta === true, + disabledText: + !dashboardItems.includes('iciclechart') && profileSource?.ProfileType().delta !== true + ? 'Iciclechart is not available for non-delta profiles' + : undefined, }); } if (enableSourcesView === true) { - allItems.push({key: 'source', canBeSelected: false}); + allItems.push({key: 'source', label: 'Source', canBeSelected: false}); } const getOption = ({ @@ -105,6 +116,7 @@ const ViewSelector = (): JSX.Element => { const items = allItems.map(item => ({ key: item.key, disabled: !item.canBeSelected, + disabledText: item.disabledText, element: getOption(item), innerAction: getInnerActionForItem(item), })); diff --git a/ui/packages/shared/profile/src/ProfileViewWithData.tsx b/ui/packages/shared/profile/src/ProfileViewWithData.tsx index 8ff4f8896ae..4c98636ece6 100644 --- a/ui/packages/shared/profile/src/ProfileViewWithData.tsx +++ b/ui/packages/shared/profile/src/ProfileViewWithData.tsx @@ -37,7 +37,7 @@ export const ProfileViewWithData = ({ showVisualizationSelector, }: ProfileViewWithDataProps): JSX.Element => { const metadata = useGrpcMetadata(); - const [dashboardItems] = useURLState('dashboard_items', { + const [dashboardItems, setDashboardItems] = useURLState('dashboard_items', { alwaysReturnArray: true, }); const [sourceBuildID] = useURLState('source_buildid'); @@ -58,6 +58,23 @@ export const ProfileViewWithData = ({ const [pprofDownloading, setPprofDownloading] = useState(false); + useEffect(() => { + // If profile type is not delta, remove iciclechart from the dashboard items + // and set it to icicle if no other items are selected. + if (profileSource == null) { + return; + } + const profileType = profileSource.ProfileType(); + let newDashboardItems = dashboardItems; + if (dashboardItems.includes('iciclechart') && !profileType.delta) { + newDashboardItems = dashboardItems.filter(item => item !== 'iciclechart'); + } + if (newDashboardItems.length === 0) { + newDashboardItems = ['icicle']; + } + setDashboardItems(newDashboardItems); + }, [profileSource, dashboardItems, setDashboardItems]); + const nodeTrimThreshold = useMemo(() => { let width = // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions