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