diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index 3e15c86e0..ee7b03871 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -4,6 +4,7 @@ import { DigmaMessageError } from "../../../api/types"; import { dispatcher } from "../../../dispatcher"; import { usePrevious } from "../../../hooks/usePrevious"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; +import { useStore } from "../../../store/useStore"; import { isEnvironment } from "../../../typeGuards/isEnvironment"; import { changeScope } from "../../../utils/actions/changeScope"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; @@ -147,6 +148,7 @@ export const AssetList = ({ const previousEnvironment = usePrevious(environment); const previousViewScope = usePrevious(scopeViewOptions); const isServicesFilterEnabled = !scope?.span?.spanCodeObjectId; + const { setShowAssetsHeaderToolBox } = useStore.getState(); const refreshData = useCallback(() => { getData( @@ -204,6 +206,7 @@ export const AssetList = ({ }; dispatcher.addActionListener(actions.SET_DATA, handleAssetsData); + setShowAssetsHeaderToolBox(true); return () => { dispatcher.removeActionListener(actions.SET_DATA, handleAssetsData); diff --git a/src/components/Assets/AssetTypeList/AssetTypeList.stories.tsx b/src/components/Assets/AssetTypeList/AssetTypeList.stories.tsx index 57266a477..595df1d53 100644 --- a/src/components/Assets/AssetTypeList/AssetTypeList.stories.tsx +++ b/src/components/Assets/AssetTypeList/AssetTypeList.stories.tsx @@ -82,3 +82,37 @@ export const Empty: Story = { }, 0); } }; + +export const EmptyWithParents: Story = { + args: { + searchQuery: "", + setRefresher: () => { + return undefined; + } + }, + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_CATEGORIES_DATA, + payload: { + assetCategories: [], + parents: [ + { + name: "http test", + displayName: "http get one", + instrumentationLibrary: "common", + spanCodeObjectId: "some span" + }, + { + name: "http test 2", + displayName: "http get two", + instrumentationLibrary: "common", + spanCodeObjectId: "some span 2" + } + ] + } + }); + }, 0); + } +}; diff --git a/src/components/Assets/AssetTypeList/index.tsx b/src/components/Assets/AssetTypeList/index.tsx index 902153e21..20e1d8b00 100644 --- a/src/components/Assets/AssetTypeList/index.tsx +++ b/src/components/Assets/AssetTypeList/index.tsx @@ -3,12 +3,18 @@ import { DigmaMessageError } from "../../../api/types"; import { dispatcher } from "../../../dispatcher"; import { usePrevious } from "../../../hooks/usePrevious"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; +import { useStore } from "../../../store/useStore"; import { isEnvironment } from "../../../typeGuards/isEnvironment"; import { isNull } from "../../../typeGuards/isNull"; import { isString } from "../../../typeGuards/isString"; +import { changeScope } from "../../../utils/actions/changeScope"; +import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; +import { SCOPE_CHANGE_EVENTS } from "../../Main/types"; +import { ChildIcon } from "../../common/icons/30px/ChildIcon"; import { AssetFilterQuery } from "../AssetsFilter/types"; import { NoDataMessage } from "../NoDataMessage"; import { actions } from "../actions"; +import { trackingEvents } from "../tracking"; import { checkIfAnyFiltersApplied, getAssetTypeInfo } from "../utils"; import { AssetTypeListItem } from "./AssetTypeListItem"; import * as s from "./styles"; @@ -75,6 +81,8 @@ export const AssetTypeList = ({ const previousSearchQuery = usePrevious(searchQuery); const previousViewScope = usePrevious(scopeViewOptions); const isServicesFilterEnabled = !scope?.span?.spanCodeObjectId; + const { setShowAssetsHeaderToolBox } = useStore.getState(); + const [showNoDataWithParents, setShowNoDataWithParents] = useState(false); const isInitialLoading = !data; @@ -137,8 +145,15 @@ export const AssetTypeList = ({ useEffect(() => { if (data && previousData !== data) { onAssetCountChange(getAssetCount(data)); + const showNoDataWithParents = Boolean( + data?.parents && + data.parents.length > 0 && + data?.assetCategories.every((x) => x.count === 0) + ); + setShowAssetsHeaderToolBox(!showNoDataWithParents); + setShowNoDataWithParents(showNoDataWithParents); } - }, [previousData, data, onAssetCountChange]); + }, [previousData, data, onAssetCountChange, setShowAssetsHeaderToolBox]); useEffect(() => { if ( @@ -172,6 +187,16 @@ export const AssetTypeList = ({ onAssetTypeSelect(assetTypeId); }; + const handleAssetLinkClick = (spanCodeObjectId: string) => { + sendUserActionTrackingEvent(trackingEvents.ALL_ASSETS_LINK_CLICKED); + changeScope({ + span: { spanCodeObjectId }, + context: { + event: SCOPE_CHANGE_EVENTS.ASSETS_EMPTY_CATEGORY_PARENT_LINK_CLICKED + } + }); + }; + if (isInitialLoading) { return ; } @@ -181,11 +206,42 @@ export const AssetTypeList = ({ return ; } - if (scope !== null) { - return ; + if (!scope) { + return ; + } + + if (showNoDataWithParents && data.parents) { + return ( + + + + + There are no child assets under this asset. You can try + + + browsing its parent spans to continue to explore the trace. + + + {data.parents.map((x) => ( + handleAssetLinkClick(x.spanCodeObjectId)} + > + {x.displayName} + + ))} + + } + /> + + ); } - return ; + return ; } const assetTypeListItems = ASSET_TYPE_IDS.map((assetTypeId) => { diff --git a/src/components/Assets/AssetTypeList/styles.ts b/src/components/Assets/AssetTypeList/styles.ts index 2f2e0ef59..5d6cca4a1 100644 --- a/src/components/Assets/AssetTypeList/styles.ts +++ b/src/components/Assets/AssetTypeList/styles.ts @@ -1,4 +1,10 @@ import styled from "styled-components"; +import { + footnoteRegularTypography, + subscriptRegularTypography +} from "../../common/App/typographies"; +import { Link } from "../../common/v3/Link"; +import { NewEmptyState } from "../../common/v3/NewEmptyState"; export const List = styled.ul` display: flex; @@ -7,3 +13,34 @@ export const List = styled.ul` padding: 0 8px 8px; margin: 0; `; + +export const EmptyStateContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + justify-content: center; + height: 100%; +`; + +export const EmptyStateTextContainer = styled.div` + ${footnoteRegularTypography} + + display: flex; + flex-direction: column; + text-align: center; + gap: 4px; + padding-top: 4px; + padding-bottom: 4px; + color: ${({ theme }) => theme.colors.v3.text.tertiary}; +`; + +export const StyledEmptyState = styled(NewEmptyState)` + flex-grow: 1; + align-self: center; +`; + +export const ParentLink = styled(Link)` + text-decoration: underline; + ${subscriptRegularTypography} +`; diff --git a/src/components/Assets/AssetTypeList/types.ts b/src/components/Assets/AssetTypeList/types.ts index cee38169c..8f81139b5 100644 --- a/src/components/Assets/AssetTypeList/types.ts +++ b/src/components/Assets/AssetTypeList/types.ts @@ -1,4 +1,5 @@ import { MemoExoticComponent } from "react"; +import { SpanInfo } from "../../../types"; import { IconProps } from "../../common/icons/types"; import { AssetFilterQuery } from "../AssetsFilter/types"; import { AssetScopeOption } from "../AssetsViewScopeConfiguration/types"; @@ -17,6 +18,7 @@ export interface AssetCategoriesData { name: string; count: number; }[]; + parents?: SpanInfo[]; } export interface AssetCategoryData { diff --git a/src/components/Assets/index.tsx b/src/components/Assets/index.tsx index 21f715ac3..311299aba 100644 --- a/src/components/Assets/index.tsx +++ b/src/components/Assets/index.tsx @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom"; import { getFeatureFlagValue } from "../../featureFlags"; import { useDebounce } from "../../hooks/useDebounce"; import { usePrevious } from "../../hooks/usePrevious"; +import { useAssetsSelector } from "../../store/assetsSlice/useAssetsSelector"; import { useConfigSelector } from "../../store/config/useConfigSelector"; import { FeatureFlag } from "../../types"; import { sendUserActionTrackingEvent } from "../../utils/actions/sendUserActionTrackingEvent"; @@ -45,6 +46,7 @@ export const Assets = () => { backendInfo, FeatureFlag.ARE_EXTENDED_ASSETS_FILTERS_ENABLED ); + const { showAssetsHeaderToolBox } = useAssetsSelector(); useEffect(() => { if (!scope?.span) { @@ -131,7 +133,7 @@ export const Assets = () => { return ; } - if (!selectedFilters) { + if (!selectedFilters && showAssetsHeaderToolBox) { return ; } @@ -173,33 +175,40 @@ export const Assets = () => { /> )} - - - - - - - - {scope?.span && ( - Assets filtered to current scope + {showAssetsHeaderToolBox && ( + <> + + + + + + + + {scope?.span && ( + Assets filtered to current scope + )} + )} + {renderContent()} ); diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx index 53861352c..e375a1409 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx @@ -562,7 +562,7 @@ const renderEmptyState = ( ); } - if (!scope && insightsViewType == "Analytics") { + if (!scope?.span?.spanCodeObjectId && insightsViewType == "Analytics") { return ( { } goTo(`/${TAB_IDS.ISSUES}`, { state }); break; + case SCOPE_CHANGE_EVENTS.ASSETS_EMPTY_CATEGORY_PARENT_LINK_CLICKED as string: + goTo(`/${TAB_IDS.ASSETS}`, { state }); + break; case SCOPE_CHANGE_EVENTS.IDE_CODE_LENS_CLICKED as string: { const url = getURLToNavigateOnCodeLensClick(scope); if (url) { @@ -212,6 +215,7 @@ export const Main = () => { break; } } + // falls through case SCOPE_CHANGE_EVENTS.DASHBOARD_SLOW_QUERIES_WIDGET_ITEM_LINK_CLICKED as string: case SCOPE_CHANGE_EVENTS.DASHBOARD_CLIENT_SPANS_PERFORMANCE_IMPACT_WIDGET_ITEM_LINK_CLICKED as string: diff --git a/src/components/Main/types.ts b/src/components/Main/types.ts index d62946312..41017adaf 100644 --- a/src/components/Main/types.ts +++ b/src/components/Main/types.ts @@ -55,7 +55,8 @@ export enum SCOPE_CHANGE_EVENTS { NOTIFICATIONS_NOTIFICATION_CARD_ASSET_LINK_CLICKED = "NOTIFICATIONS/NOTIFICATION_CARD_ASSET_LINK_CLICKED", RECENT_ACTIVITY_SPAN_LINK_CLICKED = "RECENT_ACTIVITY_SPAN_LINK_CLICKED", IDE_CODE_LENS_CLICKED = "IDE/CODE_LENS_CLICKED", - IDE_NOTIFICATION_LINK_CLICKED = "IDE/NOTIFICATION_LINK_CLICKED" + IDE_NOTIFICATION_LINK_CLICKED = "IDE/NOTIFICATION_LINK_CLICKED", + ASSETS_EMPTY_CATEGORY_PARENT_LINK_CLICKED = "ASSETS/EMPTY_CATEGORY_PARENT_LINK_CLICKED" } export interface ReactRouterLocationState { diff --git a/src/components/common/icons/30px/ChildIcon.tsx b/src/components/common/icons/30px/ChildIcon.tsx new file mode 100644 index 000000000..0f4c0348e --- /dev/null +++ b/src/components/common/icons/30px/ChildIcon.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const ChildIconComponent = (props: IconProps) => { + const { size } = useIconProps(props); + + return ( + + + + + + + + + ); +}; + +export const ChildIcon = React.memo(ChildIconComponent); diff --git a/src/store/assetsSlice/assetsSlice.ts b/src/store/assetsSlice/assetsSlice.ts new file mode 100644 index 000000000..6ad581221 --- /dev/null +++ b/src/store/assetsSlice/assetsSlice.ts @@ -0,0 +1,23 @@ +import { createSlice } from "zustand-slices"; + +interface AssetsState { + showAssetsHeaderToolBox: boolean; +} + +const initialState: AssetsState = { + showAssetsHeaderToolBox: true +}; + +const set = (update: Partial) => (state: AssetsState) => ({ + ...state, + ...update +}); + +export const assetsSlice = createSlice({ + name: "assets", + value: initialState, + actions: { + setShowAssetsHeaderToolBox: (showAssetsHeaderToolBox: boolean) => + set({ showAssetsHeaderToolBox }) + } +}); diff --git a/src/store/assetsSlice/useAssetsSelector.ts b/src/store/assetsSlice/useAssetsSelector.ts new file mode 100644 index 000000000..759e16d26 --- /dev/null +++ b/src/store/assetsSlice/useAssetsSelector.ts @@ -0,0 +1,3 @@ +import { useStore } from "../useStore"; + +export const useAssetsSelector = () => useStore((state) => state.assets); diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 827bee2ba..01c6a2acb 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -1,12 +1,13 @@ import { create } from "zustand"; import { withSlices } from "zustand-slices"; import { Scope } from "../components/common/App/types"; +import { assetsSlice } from "./assetsSlice/assetsSlice"; import { configSlice } from "./config/configSlice"; import { insightsSlice } from "./insights/insightsSlice"; import { withMutableActions } from "./withMutableActions"; export const useStore = create( - withMutableActions(withSlices(configSlice, insightsSlice), { + withMutableActions(withSlices(configSlice, insightsSlice, assetsSlice), { setScope: (scope: Scope) => (_, set) => { set((state) => state.config.scope?.span?.spanCodeObjectId !==