diff --git a/src/components/Assets/AssetList/AssetEntry/AssetEntrySkeleton/AssetEntrySkeleton.stories.tsx b/src/components/Assets/AssetList/AssetEntry/AssetEntrySkeleton/AssetEntrySkeleton.stories.tsx new file mode 100644 index 000000000..de0aa4625 --- /dev/null +++ b/src/components/Assets/AssetList/AssetEntry/AssetEntrySkeleton/AssetEntrySkeleton.stories.tsx @@ -0,0 +1,19 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { AssetEntrySkeleton } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Assets/AssetEntry/AssetEntrySkeleton", + component: AssetEntrySkeleton, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/Assets/AssetList/AssetEntry/AssetEntrySkeleton/index.tsx b/src/components/Assets/AssetList/AssetEntry/AssetEntrySkeleton/index.tsx new file mode 100644 index 000000000..961718094 --- /dev/null +++ b/src/components/Assets/AssetList/AssetEntry/AssetEntrySkeleton/index.tsx @@ -0,0 +1,35 @@ +import { Skeleton } from "../../../../common/Skeleton"; +import * as s from "../styles"; +export const AssetEntrySkeleton = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/src/components/Assets/AssetList/AssetEntry/index.tsx b/src/components/Assets/AssetList/AssetEntry/index.tsx index 7ff42d5b4..7be5fdefd 100644 --- a/src/components/Assets/AssetList/AssetEntry/index.tsx +++ b/src/components/Assets/AssetList/AssetEntry/index.tsx @@ -1,4 +1,4 @@ -import { DefaultTheme, useTheme } from "styled-components"; +import { useTheme } from "styled-components"; import { isString } from "../../../../typeGuards/isString"; import { InsightType } from "../../../../types"; import { formatTimeDistance } from "../../../../utils/formatTimeDistance"; @@ -9,7 +9,7 @@ import { InsightImportance } from "../../../Insights/types"; import { ImpactScore } from "../../../common/ImpactScore"; import { Tag } from "../../../common/Tag"; import { Tooltip } from "../../../common/Tooltip"; -import { GlobeIcon } from "../../../common/icons/GlobeIcon"; +import { GlobeIcon } from "../../../common/icons/16px/GlobeIcon"; import { getAssetTypeInfo } from "../../utils"; import { SORTING_CRITERION } from "../types"; import * as s from "./styles"; @@ -17,19 +17,8 @@ import { AssetEntryProps } from "./types"; const IS_NEW_TIME_LIMIT = 1000 * 60 * 10; // in milliseconds -const getServiceIconColor = (theme: DefaultTheme) => { - switch (theme.mode) { - case "light": - return "#4d668a"; - case "dark": - case "dark-jetbrains": - return "#dadada"; - } -}; - export const AssetEntry = (props: AssetEntryProps) => { const theme = useTheme(); - const serviceIconColor = getServiceIconColor(theme); const handleLinkClick = () => { props.onAssetLinkClick(props.entry); @@ -79,7 +68,7 @@ export const AssetEntry = (props: AssetEntryProps) => { {assetTypeInfo?.icon && ( - + )} @@ -104,7 +93,7 @@ export const AssetEntry = (props: AssetEntryProps) => { @@ -121,12 +110,12 @@ export const AssetEntry = (props: AssetEntryProps) => { - - Services + + Services - + {props.entry.services[0]} {otherServices.length > 0 && ( @@ -134,45 +123,45 @@ export const AssetEntry = (props: AssetEntryProps) => { )} - - - Last + + + Last - + {timeDistanceString} ago - + - + - - Performance - + + Performance + {performanceDuration ? performanceDuration.value : "N/A"} {performanceDuration && ( {performanceDuration.unit} )} - - - - Slowest 5% - + + + + Slowest 5% + {slowestFivePercentDuration ? slowestFivePercentDuration.value : "N/A"} {slowestFivePercentDuration && ( {slowestFivePercentDuration.unit} )} - - + + {!props.isImpactHidden && props.entry.impactScores && ( - - Performance impact + + Performance impact - + { SORTING_CRITERION.PERFORMANCE_IMPACT } /> - + - + )} diff --git a/src/components/Assets/AssetList/AssetEntry/styles.ts b/src/components/Assets/AssetList/AssetEntry/styles.ts index c20de31d3..016ba77a5 100644 --- a/src/components/Assets/AssetList/AssetEntry/styles.ts +++ b/src/components/Assets/AssetList/AssetEntry/styles.ts @@ -1,33 +1,22 @@ import styled from "styled-components"; -import { caption2RegularTypography } from "../../../common/App/typographies"; -import { grayScale } from "../../../common/App/v2colors"; +import { + footnoteRegularTypography, + subscriptRegularTypography +} from "../../../common/App/typographies"; import { CopyButton } from "../../../common/v3/CopyButton"; import { ImpactScoreIndicatorProps } from "./types"; export const Container = styled.div` + ${footnoteRegularTypography} + display: flex; flex-direction: column; - gap: 12px; + gap: 8px; padding: 8px; border-radius: 4px; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#828797"; - case "dark": - case "dark-jetbrains": - return "#9b9b9b"; - } - }}; - background: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#f1f5fa"; - case "dark": - case "dark-jetbrains": - return "#383838"; - } - }}; + color: ${({ theme }) => theme.colors.v3.text.tertiary}; + border: 1px solid ${({ theme }) => theme.colors.v3.stroke.tertiary}; + background: ${({ theme }) => theme.colors.v3.surface.secondary}; `; export const Header = styled.div` @@ -43,8 +32,7 @@ export const StyledCopyButton = styled(CopyButton)` export const TitleRow = styled.div` display: flex; - gap: 2px; - height: 20px; + gap: 4px; align-items: center; &:hover { @@ -57,16 +45,17 @@ export const TitleRow = styled.div` export const AssetTypeIconContainer = styled.div` display: flex; justify-content: center; - width: 20px; - height: 20px; + width: 24px; + height: 24px; align-items: center; + color: ${({ theme }) => theme.colors.v3.text.link}; `; export const Link = styled.a` - color: #7891d0; + ${subscriptRegularTypography} + + color: ${({ theme }) => theme.colors.v3.text.link}; text-decoration: none; - font-weight: 500; - font-size: 14px; cursor: pointer; text-overflow: ellipsis; white-space: nowrap; @@ -82,28 +71,20 @@ export const IndicatorsContainer = styled.div` export const InsightIconContainer = styled(AssetTypeIconContainer)` border-radius: 4px; - background: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#e9eef4"; - case "dark": - case "dark-jetbrains": - return "#2e2e2e"; - } - }}; + background: ${({ theme }) => theme.colors.v3.surface.primaryLight}; `; export const StatsContainer = styled.div` display: flex; - gap: 12px 16px; + gap: 8px 16px; flex-wrap: wrap; font-size: 14px; `; -export const Stats = styled.div` +export const Stat = styled.div` display: flex; flex-direction: column; - gap: 8px; + gap: 4px; width: 140px; `; @@ -122,52 +103,31 @@ export const ServicesContainer = styled.div` export const IconContainer = styled.div` display: flex; align-items: center; + color: ${({ theme }) => theme.colors.v3.icon.tertiary}; `; export const ServiceName = styled.div` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#4d668a"; - case "dark": - case "dark-jetbrains": - return "#dadada"; - } - }}; + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const StatLabel = styled.span` + color: ${({ theme }) => theme.colors.v3.text.secondary}; `; -export const ValueContainer = styled.div` +export const StatValue = styled.div` + ${subscriptRegularTypography} + display: flex; align-items: flex-end; - gap: 2px; - font-size: 14px; - font-weight: 500; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#4d668a"; - case "dark": - case "dark-jetbrains": - return "#c6c6c6"; - } - }}; + gap: 4px; + color: ${({ theme }) => theme.colors.v3.text.primary}; `; export const Suffix = styled.span` - font-weight: 400; - font-size: 14px; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#828797"; - case "dark": - case "dark-jetbrains": - return "#9b9b9b"; - } - }}; + color: ${({ theme }) => theme.colors.v3.text.tertiary}; `; export const ImpactScoreIndicatorContainer = styled.div` @@ -185,12 +145,9 @@ export const ImpactScoreIndicator = styled.div` `; export const ScopeName = styled.div` - ${caption2RegularTypography} - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: ${grayScale[500]}; - opacity: 0.87; + color: ${({ theme }) => theme.colors.v3.text.tertiary}; max-width: fit-content; `; diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index b713a9c7f..d8cfd471a 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -13,9 +13,7 @@ import { isEnvironment } from "../../../typeGuards/isEnvironment"; import { isNumber } from "../../../typeGuards/isNumber"; import { isString } from "../../../typeGuards/isString"; import { ConfigContext } from "../../common/App/ConfigContext"; -import { EmptyState } from "../../common/EmptyState"; import { Menu } from "../../common/Menu"; -import { NewCircleLoader } from "../../common/NewCircleLoader"; import { Pagination } from "../../common/Pagination"; import { Popover } from "../../common/Popover"; import { PopoverContent } from "../../common/Popover/PopoverContent"; @@ -28,6 +26,7 @@ import { AssetFilterQuery } from "../AssetsFilter/types"; import { actions } from "../actions"; import { checkIfAnyFiltersApplied, getAssetTypeInfo } from "../utils"; import { AssetEntry as AssetEntryComponent } from "./AssetEntry"; +import { AssetEntrySkeleton } from "./AssetEntry/AssetEntrySkeleton"; import * as s from "./styles"; import { AssetEntry, @@ -159,7 +158,7 @@ const getData = ( export const AssetList = (props: AssetListProps) => { const [data, setData] = useState(); const previousData = usePrevious(data); - const [isInitialLoading, setIsInitialLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); const [sorting, setSorting] = useState({ @@ -229,10 +228,11 @@ export const AssetList = (props: AssetListProps) => { useEffect(() => { refreshData(); - setIsInitialLoading(true); + setIsLoading(true); const handleAssetsData = (data: unknown, timeStamp: number) => { setData(data as AssetsData); + setIsLoading(false); setLastSetDataTimeStamp(timeStamp); }; @@ -302,12 +302,6 @@ export const AssetList = (props: AssetListProps) => { } }, [props.data]); - useEffect(() => { - if (!previousData && data) { - setIsInitialLoading(false); - } - }, [previousData, data]); - useEffect(() => { if ( isImpactHidden && @@ -382,10 +376,6 @@ export const AssetList = (props: AssetListProps) => { }; const renderContent = () => { - if (isInitialLoading) { - return } />; - } - return entries.length > 0 ? ( <> @@ -498,7 +488,16 @@ export const AssetList = (props: AssetListProps) => { })} - {renderContent()} + + + + + + + + + {renderContent()} + ); }; diff --git a/src/components/Assets/AssetList/styles.ts b/src/components/Assets/AssetList/styles.ts index 9d6c81e25..ce39bb49b 100644 --- a/src/components/Assets/AssetList/styles.ts +++ b/src/components/Assets/AssetList/styles.ts @@ -1,5 +1,6 @@ import styled from "styled-components"; import { subscriptMediumTypography } from "../../common/App/typographies"; +import { FadingContentSwitch } from "../../common/FadingContentSwitch"; import { SORTING_ORDER, SortingMenuButtonProps, @@ -11,6 +12,16 @@ export const Container = styled.div` display: flex; flex-direction: column; height: 100%; +`; + +export const StyledFadingContentSwitch = styled(FadingContentSwitch)` + height: 100%; +`; + +export const FadingContentContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; overflow: hidden; `; diff --git a/src/components/Assets/AssetTypeList/AssetTypeListItemSkeleton/index.tsx b/src/components/Assets/AssetTypeList/AssetTypeListItemSkeleton/index.tsx new file mode 100644 index 000000000..4b149f977 --- /dev/null +++ b/src/components/Assets/AssetTypeList/AssetTypeListItemSkeleton/index.tsx @@ -0,0 +1,9 @@ +import { Skeleton } from "../../../common/Skeleton"; +import * as s from "./styles"; + +export const AssetTypeListItemSkeleton = () => ( + + + + +); diff --git a/src/components/Assets/AssetTypeList/AssetTypeListItemSkeleton/styles.ts b/src/components/Assets/AssetTypeList/AssetTypeListItemSkeleton/styles.ts new file mode 100644 index 000000000..f7e1ae061 --- /dev/null +++ b/src/components/Assets/AssetTypeList/AssetTypeListItemSkeleton/styles.ts @@ -0,0 +1,12 @@ +import styled from "styled-components"; + +export const Container = styled.div` + padding: 8px; + display: flex; + align-items: center; + gap: 4px; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.colors.v3.stroke.tertiary}; + background: ${({ theme }) => theme.colors.v3.surface.secondary}; + height: 20px; +`; diff --git a/src/components/Assets/AssetTypeList/index.tsx b/src/components/Assets/AssetTypeList/index.tsx index af204581d..d7944bc73 100644 --- a/src/components/Assets/AssetTypeList/index.tsx +++ b/src/components/Assets/AssetTypeList/index.tsx @@ -11,6 +11,7 @@ import { NoDataMessage } from "../NoDataMessage"; import { actions } from "../actions"; import { checkIfAnyFiltersApplied, getAssetTypeInfo } from "../utils"; import { AssetTypeListItem } from "./AssetTypeListItem"; +import { AssetTypeListItemSkeleton } from "./AssetTypeListItemSkeleton"; import * as s from "./styles"; import { AssetCategoriesData, @@ -59,7 +60,7 @@ export const AssetTypeList = (props: AssetTypeListProps) => { const previousData = usePrevious(data); const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); - const [isInitialLoading, setIsInitialLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const config = useContext(ConfigContext); const previousEnvironment = usePrevious(config.environment); const refreshTimerId = useRef(); @@ -94,10 +95,11 @@ export const AssetTypeList = (props: AssetTypeListProps) => { useEffect(() => { refreshData(); - setIsInitialLoading(true); + setIsLoading(true); const handleCategoriesData = (data: unknown, timeStamp: number) => { setData(data as AssetCategoriesData); + setIsLoading(false); setLastSetDataTimeStamp(timeStamp); }; @@ -159,60 +161,64 @@ export const AssetTypeList = (props: AssetTypeListProps) => { } }, [props.data]); - useEffect(() => { - if (!previousData && data) { - setIsInitialLoading(false); - } - }, [previousData, data]); - const handleAssetTypeClick = (assetTypeId: string) => { props.onAssetTypeSelect(assetTypeId); }; - if (isInitialLoading) { - return ; - } + const renderContent = () => { + if (data?.assetCategories.every((x) => x.count === 0)) { + if (areAnyFiltersApplied) { + return ; + } - if (data?.assetCategories.every((x) => x.count === 0)) { - if (areAnyFiltersApplied) { - return ; - } + if (config.scope !== null) { + return ; + } - if (config.scope !== null) { - return ; + return ; } - return ; - } - - const assetTypeListItems = ASSET_TYPE_IDS.map((assetTypeId) => { - const assetTypeData = data?.assetCategories.find( - (x) => x.name === assetTypeId - ); - const assetTypeInfo = getAssetTypeInfo(assetTypeId); + const assetTypeListItems = ASSET_TYPE_IDS.map((assetTypeId) => { + const assetTypeData = data?.assetCategories.find( + (x) => x.name === assetTypeId + ); + const assetTypeInfo = getAssetTypeInfo(assetTypeId); - if (assetTypeData && assetTypeInfo) { - return { - ...assetTypeData, - ...assetTypeInfo - }; - } + if (assetTypeData && assetTypeInfo) { + return { + ...assetTypeData, + ...assetTypeInfo + }; + } - return null; - }).filter((x) => !isNull(x) && x.count > 0) as AssetCategoryData[]; + return null; + }).filter((x) => !isNull(x) && x.count > 0) as AssetCategoryData[]; + + return ( + + {assetTypeListItems.map((x) => ( + + ))} + + ); + }; return ( - - {assetTypeListItems.map((x) => ( - - ))} - + + + + + + + + {renderContent()} + ); }; diff --git a/src/components/Assets/AssetTypeList/styles.ts b/src/components/Assets/AssetTypeList/styles.ts index 2f2e0ef59..4582167a8 100644 --- a/src/components/Assets/AssetTypeList/styles.ts +++ b/src/components/Assets/AssetTypeList/styles.ts @@ -1,4 +1,9 @@ import styled from "styled-components"; +import { FadingContentSwitch } from "../../common/FadingContentSwitch"; + +export const StyledFadingContentSwitch = styled(FadingContentSwitch)` + height: 100%; +`; export const List = styled.ul` display: flex; diff --git a/src/components/Assets/NoDataMessage/NoDataMessage.stories.tsx b/src/components/Assets/NoDataMessage/NoDataMessage.stories.tsx index 775a93f57..bb4f839b2 100644 --- a/src/components/Assets/NoDataMessage/NoDataMessage.stories.tsx +++ b/src/components/Assets/NoDataMessage/NoDataMessage.stories.tsx @@ -17,12 +17,6 @@ export default meta; type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args -export const Loading: Story = { - args: { - type: "loading" - } -}; - export const NoDataYet: Story = { args: { type: "noDataYet" diff --git a/src/components/Assets/NoDataMessage/index.tsx b/src/components/Assets/NoDataMessage/index.tsx index c286e8c4d..ff5f0fede 100644 --- a/src/components/Assets/NoDataMessage/index.tsx +++ b/src/components/Assets/NoDataMessage/index.tsx @@ -2,7 +2,6 @@ import { actions as globalActions } from "../../../actions"; import { trackingEvents as globalTrackingEvents } from "../../../trackingEvents"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { EmptyState } from "../../common/EmptyState"; -import { NewCircleLoader } from "../../common/NewCircleLoader"; import { CardsIcon } from "../../common/icons/CardsIcon"; import * as s from "./styles"; import { NoDataMessageProps } from "./types"; @@ -24,9 +23,6 @@ export const NoDataMessage = (props: NoDataMessageProps) => { let content: JSX.Element | null = null; switch (props.type) { - case "loading": - content = ; - break; case "noDataYet": content = ( { return ; } - if (!selectedFilters) { - return ; - } - if (!selectedAssetTypeId) { return ( { @@ -28,7 +30,6 @@ export const HeaderItem = styled.div` gap: 8px; align-items: center; flex-shrink: 0; - height: 36px; `; export const SearchInputContainer = styled.div` diff --git a/src/components/Highlights/Impact/index.tsx b/src/components/Highlights/Impact/index.tsx index f56ec9dd7..dfdfa0233 100644 --- a/src/components/Highlights/Impact/index.tsx +++ b/src/components/Highlights/Impact/index.tsx @@ -1,9 +1,10 @@ import { Row, createColumnHelper } from "@tanstack/react-table"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useState } from "react"; import { PERFORMANCE_IMPACT_DOCUMENTATION_URL, ROUTES } from "../../../constants"; +import { usePrevious } from "../../../hooks/usePrevious"; import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { ConfigContext } from "../../common/App/ConfigContext"; @@ -13,7 +14,8 @@ import { RefreshIcon } from "../../common/icons/16px/RefreshIcon"; import { Button } from "../../common/v3/Button"; import { Card } from "../../common/v3/Card"; import { Tag } from "../../common/v3/Tag"; -import { EmptyStateCard } from "../EmptyStateCard"; +import { CardSkeleton } from "../common/CardSkeleton"; +import { EmptyStateCard } from "../common/EmptyStateCard"; import { EnvironmentName } from "../common/EnvironmentName"; import { Section } from "../common/Section"; import { Table } from "../common/Table"; @@ -37,13 +39,21 @@ const getRankTagType = (normalizedRank: number) => { }; export const Impact = () => { + const [isInitialLoading, setIsInitialLoading] = useState(true); const { data, getData } = useImpactData(); + const previousData = usePrevious(data); const config = useContext(ConfigContext); useEffect(() => { getData(); }, []); + useEffect(() => { + if (!previousData && data) { + setIsInitialLoading(false); + } + }, [previousData, data]); + const renderImpactCard = (data: EnvironmentImpactData[]) => { const columnHelper = createColumnHelper(); @@ -132,6 +142,10 @@ export const Impact = () => { ); } + if (isInitialLoading) { + return ; + } + if (!data || data.impactHighlights.length === 0) { return ( { const renderCard = () => { if (isInitialLoading) { - return ( - - ); + return ; } if (!data || data.performance.length === 0) { diff --git a/src/components/Highlights/Scaling/Scaling.stories.tsx b/src/components/Highlights/Scaling/Scaling.stories.tsx index 2090914a9..198c0d67f 100644 --- a/src/components/Highlights/Scaling/Scaling.stories.tsx +++ b/src/components/Highlights/Scaling/Scaling.stories.tsx @@ -47,7 +47,7 @@ export const NoData: Story = { window.postMessage({ type: "digma", action: actions.SET_HIGHLIGHTS_SCALING_DATA, - payload: { ...mockedScalingData, dataState: "noData" } + payload: { ...mockedScalingData, dataState: "NoData" } }); }); } @@ -66,7 +66,7 @@ export const PartialData: Story = { window.postMessage({ type: "digma", action: actions.SET_HIGHLIGHTS_SCALING_DATA, - payload: { ...mockedScalingData, dataState: "partial" } + payload: { ...mockedScalingData, dataState: "Partial" } }); }); } @@ -85,7 +85,7 @@ export const ScalingWell: Story = { window.postMessage({ type: "digma", action: actions.SET_HIGHLIGHTS_SCALING_DATA, - payload: { ...mockedScalingData, dataState: "scalingWell" } + payload: { ...mockedScalingData, dataState: "ScalingWell" } }); }); } diff --git a/src/components/Highlights/Scaling/index.tsx b/src/components/Highlights/Scaling/index.tsx index 286a1250d..cc732a599 100644 --- a/src/components/Highlights/Scaling/index.tsx +++ b/src/components/Highlights/Scaling/index.tsx @@ -1,7 +1,8 @@ import { Row, createColumnHelper } from "@tanstack/react-table"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useState } from "react"; import { actions as globalActions } from "../../../actions"; import { ROUTES, SCALING_ISSUE_DOCUMENTATION_URL } from "../../../constants"; +import { usePrevious } from "../../../hooks/usePrevious"; import { ChangeViewPayload } from "../../../types"; import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; @@ -12,9 +13,10 @@ import { RefreshIcon } from "../../common/icons/16px/RefreshIcon"; import { CheckCircleIcon } from "../../common/icons/20px/CheckCircleIcon"; import { Button } from "../../common/v3/Button"; import { Card } from "../../common/v3/Card"; -import { EmptyStateCard } from "../EmptyStateCard"; import { addEnvironmentColumns } from "../TopIssues/highlightCards/addEnvironmentColumns"; import { EnvironmentData } from "../TopIssues/types"; +import { CardSkeleton } from "../common/CardSkeleton"; +import { EmptyStateCard } from "../common/EmptyStateCard"; import { Section } from "../common/Section"; import { Table } from "../common/Table"; import { TableText } from "../common/TableText"; @@ -25,13 +27,22 @@ import { EnvironmentScalingData } from "./types"; import { useScalingData } from "./useScalingData"; export const Scaling = () => { + const [isInitialLoading, setIsInitialLoading] = useState(true); const { data, getData } = useScalingData(); + const previousData = usePrevious(data); + const config = useContext(ConfigContext); useEffect(() => { getData(); }, []); + useEffect(() => { + if (!previousData && data) { + setIsInitialLoading(false); + } + }, [previousData, data]); + const renderScalingCard = ( data: EnvironmentData[] ) => { @@ -112,6 +123,10 @@ export const Scaling = () => { }); }; + if (isInitialLoading) { + return ; + } + if (!data) { return null; } diff --git a/src/components/Highlights/SpanInfo/SpanInfo.stories.tsx b/src/components/Highlights/SpanInfo/SpanInfo.stories.tsx index b9791167f..fe9adde35 100644 --- a/src/components/Highlights/SpanInfo/SpanInfo.stories.tsx +++ b/src/components/Highlights/SpanInfo/SpanInfo.stories.tsx @@ -32,7 +32,7 @@ export const Default: Story = { } }; -export const LargeText: Story = { +export const WithLongText: Story = { play: () => { window.setTimeout(() => { window.postMessage({ diff --git a/src/components/Highlights/SpanInfo/index.tsx b/src/components/Highlights/SpanInfo/index.tsx index 8cf65d7cc..5edc70f5c 100644 --- a/src/components/Highlights/SpanInfo/index.tsx +++ b/src/components/Highlights/SpanInfo/index.tsx @@ -1,14 +1,13 @@ import { RefObject, useEffect, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { getAssetTypeInfo } from "../../Assets/utils"; +import { Skeleton } from "../../common/Skeleton"; import { ArrowsInsideIcon } from "../../common/icons/12px/ArrowsInsideIcon"; import { ArrowsOutsideIcon } from "../../common/icons/12px/ArrowsOutsideIcon"; import { GlobeIcon } from "../../common/icons/16px/GlobeIcon"; -import { RefreshIcon } from "../../common/icons/16px/RefreshIcon"; import { WrenchIcon } from "../../common/icons/16px/WrenchIcon"; import { Tag } from "../../common/v3/Tag"; import { Tooltip } from "../../common/v3/Tooltip"; -import { EmptyStateCard } from "../EmptyStateCard"; import * as s from "./styles"; import { useSpanInfoData } from "./useSpanInfoData"; @@ -44,7 +43,20 @@ export const SpanInfo = () => { }, []); if (!data) { - return ; + return ( + + + + + + + + + + + + + ); } const assetTypeInfo = getAssetTypeInfo(data.assetTypeId); @@ -142,14 +154,12 @@ export const SpanInfo = () => { Environments - - {data.environments[0].name} diff --git a/src/components/Highlights/SpanInfo/styles.ts b/src/components/Highlights/SpanInfo/styles.ts index e5bd89630..d1f384de9 100644 --- a/src/components/Highlights/SpanInfo/styles.ts +++ b/src/components/Highlights/SpanInfo/styles.ts @@ -198,3 +198,18 @@ export const StyledCodeSnippet = styled(CodeSnippet)` max-height: 140px; overflow: auto; `; + +export const TitleContainerSkeleton = styled(TitleContainer)` + height: 24px; +`; + +export const StatsContainerSkeleton = styled.div` + display: flex; + gap: 5px; + height: 28px; + justify-content: space-between; +`; + +export const StatSkeleton = styled.div` + width: 81px; +`; diff --git a/src/components/Highlights/TopIssues/index.tsx b/src/components/Highlights/TopIssues/index.tsx index 867f5c3c7..d2848a39d 100644 --- a/src/components/Highlights/TopIssues/index.tsx +++ b/src/components/Highlights/TopIssues/index.tsx @@ -1,8 +1,8 @@ import { Fragment, useEffect, useState } from "react"; import { usePrevious } from "../../../hooks/usePrevious"; import { CrossCircleIcon } from "../../common/icons/16px/CrossCircleIcon"; -import { RefreshIcon } from "../../common/icons/16px/RefreshIcon"; -import { EmptyStateCard } from "../EmptyStateCard"; +import { CardSkeleton } from "../common/CardSkeleton"; +import { EmptyStateCard } from "../common/EmptyStateCard"; import { Section } from "../common/Section"; import { EndpointBottleneckHighlightCard } from "./highlightCards/EndpointBottleneckHighlightCard"; import { EndpointChattyApiV2HighlightCard } from "./highlightCards/EndpointChattyApiV2HighlightCard"; @@ -100,14 +100,7 @@ export const TopIssues = () => { const renderContent = () => { if (isInitialLoading) { - return ( - - ); + return ; } if (!data || data.topInsights.length === 0) { diff --git a/src/components/Highlights/common/CardSkeleton/index.tsx b/src/components/Highlights/common/CardSkeleton/index.tsx new file mode 100644 index 000000000..d68a1f269 --- /dev/null +++ b/src/components/Highlights/common/CardSkeleton/index.tsx @@ -0,0 +1,52 @@ +import { Skeleton } from "../../../common/Skeleton"; +import { Card } from "../../../common/v3/Card"; +import * as s from "./styles"; +import { + CardSkeletonProps, + CellSkeletonProps, + ColumnSkeletonProps +} from "./types"; + +const CellSkeleton = ({ withIcon }: CellSkeletonProps) => { + return ( + + {withIcon && ( + + + + )} + + + ); +}; + +const ColumnSkeleton = ({ withIcon }: ColumnSkeletonProps) => ( + + + + + + +); + +export const CardSkeleton = ({ type }: CardSkeletonProps) => ( + } + content={ + + {type === "asset" && ( + + + + + )} + + + + + + + + } + /> +); diff --git a/src/components/Highlights/common/CardSkeleton/styles.ts b/src/components/Highlights/common/CardSkeleton/styles.ts new file mode 100644 index 000000000..02e0a2db7 --- /dev/null +++ b/src/components/Highlights/common/CardSkeleton/styles.ts @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +export const ContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const AssetContainer = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 6px; +`; + +export const TableContainer = styled.div` + display: flex; + gap: 12px; + flex-grow: 1; + + & > * { + flex: 1 1 0; + } +`; + +export const TableColumnContainer = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; + +export const TableCellContainer = styled.div` + display: flex; + align-items: center; + gap: 4px; + padding: 1px 0; +`; + +export const IconSkeletonContainer = styled.div` + flex-shrink: 0; +`; diff --git a/src/components/Highlights/common/CardSkeleton/types.ts b/src/components/Highlights/common/CardSkeleton/types.ts new file mode 100644 index 000000000..af08d6a76 --- /dev/null +++ b/src/components/Highlights/common/CardSkeleton/types.ts @@ -0,0 +1,9 @@ +export interface CardSkeletonProps { + type?: "default" | "asset" | "spanInfo"; +} + +export interface CellSkeletonProps { + withIcon?: boolean; +} + +export type ColumnSkeletonProps = CellSkeletonProps; diff --git a/src/components/Highlights/EmptyStateCard/EmptyStateCard.stories.tsx b/src/components/Highlights/common/EmptyStateCard/EmptyStateCard.stories.tsx similarity index 85% rename from src/components/Highlights/EmptyStateCard/EmptyStateCard.stories.tsx rename to src/components/Highlights/common/EmptyStateCard/EmptyStateCard.stories.tsx index 4b5ae8a2e..25f2222e1 100644 --- a/src/components/Highlights/EmptyStateCard/EmptyStateCard.stories.tsx +++ b/src/components/Highlights/common/EmptyStateCard/EmptyStateCard.stories.tsx @@ -1,8 +1,8 @@ import { Meta, StoryObj } from "@storybook/react"; import { EmptyStateCard } from "."; -import { CrossCircleIcon } from "../../common/icons/16px/CrossCircleIcon"; -import { RefreshIcon } from "../../common/icons/16px/RefreshIcon"; +import { CrossCircleIcon } from "../../../common/icons/16px/CrossCircleIcon"; +import { RefreshIcon } from "../../../common/icons/16px/RefreshIcon"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta: Meta = { diff --git a/src/components/Highlights/EmptyStateCard/index.tsx b/src/components/Highlights/common/EmptyStateCard/index.tsx similarity index 100% rename from src/components/Highlights/EmptyStateCard/index.tsx rename to src/components/Highlights/common/EmptyStateCard/index.tsx diff --git a/src/components/Highlights/EmptyStateCard/styles.ts b/src/components/Highlights/common/EmptyStateCard/styles.ts similarity index 93% rename from src/components/Highlights/EmptyStateCard/styles.ts rename to src/components/Highlights/common/EmptyStateCard/styles.ts index f844f831a..c0fd44a8d 100644 --- a/src/components/Highlights/EmptyStateCard/styles.ts +++ b/src/components/Highlights/common/EmptyStateCard/styles.ts @@ -2,8 +2,8 @@ import styled from "styled-components"; import { subscriptMediumTypography, subscriptRegularTypography -} from "../../common/App/typographies"; -import { Card as CommonCard } from "../../common/v3/Card"; +} from "../../../common/App/typographies"; +import { Card as CommonCard } from "../../../common/v3/Card"; import { IconContainerProps } from "./types"; export const Card = styled(CommonCard)` diff --git a/src/components/Highlights/EmptyStateCard/types.ts b/src/components/Highlights/common/EmptyStateCard/types.ts similarity index 85% rename from src/components/Highlights/EmptyStateCard/types.ts rename to src/components/Highlights/common/EmptyStateCard/types.ts index 6d628b15f..a08d8e3cf 100644 --- a/src/components/Highlights/EmptyStateCard/types.ts +++ b/src/components/Highlights/common/EmptyStateCard/types.ts @@ -1,5 +1,5 @@ import { ComponentType } from "react"; -import { IconProps } from "../../common/icons/types"; +import { IconProps } from "../../../common/icons/types"; type EmptyStateType = "default" | "success" | "lowSeverity"; diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/InsightsPage.stories.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/InsightsPage.stories.tsx index 3b5faffdb..9f572232f 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/InsightsPage.stories.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/InsightsPage.stories.tsx @@ -32,6 +32,7 @@ const scope: Scope = { }; const props: InsightsPageProps = { + isLoading: false, insights: [], isFilteringEnabled: false, onJiraTicketCreate: () => { diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx index 54dc298a5..d44728c54 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/index.tsx @@ -14,6 +14,7 @@ import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserA import { ConfigContext } from "../../../common/App/ConfigContext"; import { EmptyState } from "../../../common/EmptyState"; import { CardsIcon } from "../../../common/icons/CardsIcon"; +import { InsightCardSkeleton } from "../../InsightsCatalogSkeleton/InsightCardSkeleton"; import { actions } from "../../actions"; import { trackingEvents } from "../../tracking"; import { @@ -563,74 +564,85 @@ export const InsightsPage = (props: InsightsPageProps) => { }; return ( - - {props.insights.length > 0 ? ( - props.insights.map((insight, j) => { - return renderInsightCard( - insight, - handleShowJiraTicket, - !isUndefined(isInsightJiraTicketHintShown) && - !isInsightJiraTicketHintShown?.value && - j === insightIndexWithJiraHint, - props.onRefresh, - props.isMarkAsReadButtonEnabled - ); - }) - ) : props.isFilteringEnabled ? ( - - There are no insights for this criteria - - } - /> - ) : config.scope && - isNumber(config.scope.analyticsInsightsCount) && - config.scope.analyticsInsightsCount > 0 ? ( - - Performing more actions that trigger this asset will increase the - chance of identifying insights. You can also check out the{" "} - - analytics - {" "} - tab - - } - /> - ) : config.scope?.span ? ( - - No data received yet for this span, please trigger some actions - using this code to see more insights. - - } - /> - ) : ( - - - Trigger actions that call this application to learn more about - its runtime behavior - - - Not seeing your application data? - - - } - /> - )} + + + + + + + + + {props.insights.length > 0 ? ( + props.insights.map((insight, j) => { + return renderInsightCard( + insight, + handleShowJiraTicket, + !isUndefined(isInsightJiraTicketHintShown) && + !isInsightJiraTicketHintShown?.value && + j === insightIndexWithJiraHint, + props.onRefresh, + props.isMarkAsReadButtonEnabled + ); + }) + ) : props.isFilteringEnabled ? ( + + There are no insights for this criteria + + } + /> + ) : config.scope && + isNumber(config.scope.analyticsInsightsCount) && + config.scope.analyticsInsightsCount > 0 ? ( + + Performing more actions that trigger this asset will increase + the chance of identifying insights. You can also check out the{" "} + + analytics + {" "} + tab + + } + /> + ) : config.scope?.span ? ( + + No data received yet for this span, please trigger some + actions using this code to see more insights. + + } + /> + ) : ( + + + Trigger actions that call this application to learn more + about its runtime behavior + + + Not seeing your application data? + + + } + /> + )} + + ); }; diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/styles.ts b/src/components/Insights/InsightsCatalog/InsightsPage/styles.ts index 48dbee455..b5a30c461 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/styles.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/styles.ts @@ -1,7 +1,16 @@ import styled from "styled-components"; +import { FadingContentSwitch } from "../../../common/FadingContentSwitch"; import { Link } from "../../../common/Link"; export const Container = styled.div` + height: 100%; +`; + +export const StyledFadingContentSwitch = styled(FadingContentSwitch)` + height: 100%; +`; + +export const ContentContainer = styled.div` display: flex; flex-direction: column; gap: 8px; diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/types.ts b/src/components/Insights/InsightsCatalog/InsightsPage/types.ts index 3494bad27..1c004e0e2 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/types.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/types.ts @@ -10,6 +10,7 @@ export interface InsightsPageProps { onRefresh: () => void; page: number; isMarkAsReadButtonEnabled: boolean; + isLoading: boolean; } export interface isInsightJiraTicketHintShownPayload { diff --git a/src/components/Insights/InsightsCatalog/InsightsStats/InsightStatsSkeleton/index.tsx b/src/components/Insights/InsightsCatalog/InsightsStats/InsightStatsSkeleton/index.tsx new file mode 100644 index 000000000..f90462f85 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/InsightsStats/InsightStatsSkeleton/index.tsx @@ -0,0 +1,18 @@ +import { Skeleton } from "../../../../common/Skeleton"; +import { Stats } from "../styles"; +import * as s from "./styles"; + +const StatSkeleton = () => ( + + + + +); + +export const InsightStatsSkeleton = () => ( + + + + + +); diff --git a/src/components/Insights/InsightsCatalog/InsightsStats/InsightStatsSkeleton/styles.ts b/src/components/Insights/InsightsCatalog/InsightsStats/InsightStatsSkeleton/styles.ts new file mode 100644 index 000000000..fbc2e2454 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/InsightsStats/InsightStatsSkeleton/styles.ts @@ -0,0 +1,6 @@ +import styled from "styled-components"; +import { Stat } from "../styles"; + +export const StatSkeleton = styled(Stat)` + background: ${({ theme }) => theme.colors.v3.surface.secondary}; +`; diff --git a/src/components/Insights/InsightsCatalog/InsightsStats/InsightsStats.stories.tsx b/src/components/Insights/InsightsCatalog/InsightsStats/InsightsStats.stories.tsx index c8b15b1bf..bf4bbd20e 100644 --- a/src/components/Insights/InsightsCatalog/InsightsStats/InsightsStats.stories.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsStats/InsightsStats.stories.tsx @@ -19,7 +19,17 @@ export const Default: Story = { args: { allIssuesCount: 100, criticalCount: 101, - unreadCount: 12 + unreadCount: 12, + isLoading: false + } +}; + +export const Loading: Story = { + args: { + allIssuesCount: 100, + criticalCount: 101, + unreadCount: 12, + isLoading: true } }; diff --git a/src/components/Insights/InsightsCatalog/InsightsStats/index.tsx b/src/components/Insights/InsightsCatalog/InsightsStats/index.tsx index f20fe93d8..9df0b6317 100644 --- a/src/components/Insights/InsightsCatalog/InsightsStats/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsStats/index.tsx @@ -4,6 +4,7 @@ import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserA import { ConfigContext } from "../../../common/App/ConfigContext"; import { Tooltip } from "../../../common/v3/Tooltip"; import { InsightFilterType } from "../types"; +import { InsightStatsSkeleton } from "./InsightStatsSkeleton"; import * as s from "./styles"; import { InsightStatsProps } from "./types"; @@ -11,7 +12,8 @@ export const InsightStats = ({ onChange, criticalCount, allIssuesCount, - unreadCount + unreadCount, + isLoading }: InsightStatsProps) => { const [selectedFilters, setSelectedFilters] = useState( [] @@ -40,36 +42,39 @@ export const InsightStats = ({ }; return ( - - handleSelectionChange("criticality")} - > - {isNumber(criticalCount) ? ( - {criticalCount} - ) : ( - - )} - Critical issues - - handleSelectionChange("unread")} - > - {unreadCount} - Unread issues - - - {isNumber(allIssuesCount) ? ( - {allIssuesCount} - ) : ( - - )} - All issues - - + + + + handleSelectionChange("criticality")} + > + {isNumber(criticalCount) ? ( + {criticalCount} + ) : ( + + )} + Critical issues + + handleSelectionChange("unread")} + > + {unreadCount} + Unread issues + + + {isNumber(allIssuesCount) ? ( + {allIssuesCount} + ) : ( + + )} + All issues + + + ); }; diff --git a/src/components/Insights/InsightsCatalog/InsightsStats/styles.ts b/src/components/Insights/InsightsCatalog/InsightsStats/styles.ts index 7c2d4f881..8b2bae5ad 100644 --- a/src/components/Insights/InsightsCatalog/InsightsStats/styles.ts +++ b/src/components/Insights/InsightsCatalog/InsightsStats/styles.ts @@ -3,6 +3,7 @@ import { bodyBoldTypography, caption1MediumTypography } from "../../../common/App/typographies"; +import { FadingContentSwitch } from "../../../common/FadingContentSwitch"; import { StatsProps } from "./types"; export const Stats = styled.div` @@ -11,7 +12,8 @@ export const Stats = styled.div` border: 1px solid ${({ theme }) => theme.colors.v3.stroke.tertiary}; gap: 8px; padding: 8px; - align-items: stretch; + height: 100%; + box-sizing: border-box; `; export const Stat = styled.button` @@ -37,6 +39,10 @@ export const Stat = styled.button` } `; +export const StyledFadingContentSwitch = styled(FadingContentSwitch)` + height: 72px; +`; + export const CriticalStat = styled(Stat)` border: 1px solid ${({ $selected, theme }) => diff --git a/src/components/Insights/InsightsCatalog/InsightsStats/types.ts b/src/components/Insights/InsightsCatalog/InsightsStats/types.ts index e92da8a8d..cbd6ddbf6 100644 --- a/src/components/Insights/InsightsCatalog/InsightsStats/types.ts +++ b/src/components/Insights/InsightsCatalog/InsightsStats/types.ts @@ -5,6 +5,7 @@ export interface InsightStatsProps { allIssuesCount?: number; unreadCount: number; criticalCount?: number; + isLoading: boolean; } export interface StatsProps { diff --git a/src/components/Insights/InsightsCatalog/index.tsx b/src/components/Insights/InsightsCatalog/index.tsx index 456bf9840..9fc9f7367 100644 --- a/src/components/Insights/InsightsCatalog/index.tsx +++ b/src/components/Insights/InsightsCatalog/index.tsx @@ -252,7 +252,6 @@ export const InsightsCatalog = (props: InsightsCatalogProps) => { /> - {mode === ViewMode.All ? ( <> {!searchInputValue && @@ -267,6 +266,7 @@ export const InsightsCatalog = (props: InsightsCatalogProps) => { : props.unreadCount || 0 } onChange={handleFilterSelectionChange} + isLoading={props.isLoading} /> )} {selectedFilters.length === 1 && ( @@ -319,6 +319,7 @@ export const InsightsCatalog = (props: InsightsCatalogProps) => { onJiraTicketCreate={onJiraTicketCreate} onRefresh={props.onRefresh} isMarkAsReadButtonEnabled={isShowUnreadOnly(selectedFilters)} + isLoading={props.isLoading} /> {totalCount > 0 && ( diff --git a/src/components/Insights/InsightsCatalog/types.ts b/src/components/Insights/InsightsCatalog/types.ts index 394e2f737..1de1374d7 100644 --- a/src/components/Insights/InsightsCatalog/types.ts +++ b/src/components/Insights/InsightsCatalog/types.ts @@ -15,6 +15,7 @@ export interface InsightsCatalogProps { unreadCount?: number; isMarkingAsReadEnabled: boolean; hideInsightsStats?: boolean; + isLoading: boolean; } export enum ViewMode { diff --git a/src/components/Insights/InsightsCatalogSkeleton/InsightCardSkeleton/index.tsx b/src/components/Insights/InsightsCatalogSkeleton/InsightCardSkeleton/index.tsx new file mode 100644 index 000000000..0046434b6 --- /dev/null +++ b/src/components/Insights/InsightsCatalogSkeleton/InsightCardSkeleton/index.tsx @@ -0,0 +1,38 @@ +import { Skeleton } from "../../../common/Skeleton"; +import { Card } from "../../../common/v3/Card"; +import * as s from "./styles"; + +const KeyValueSkeleton = () => ( + + + + +); + +const FooterButtonSkeleton = () => ( + +); + +export const InsightCardSkeleton = () => ( + + + + + } + content={ + + + + + + } + footer={ + + + + + } + /> +); diff --git a/src/components/Insights/InsightsCatalogSkeleton/InsightCardSkeleton/styles.ts b/src/components/Insights/InsightsCatalogSkeleton/InsightCardSkeleton/styles.ts new file mode 100644 index 000000000..f04472635 --- /dev/null +++ b/src/components/Insights/InsightsCatalogSkeleton/InsightCardSkeleton/styles.ts @@ -0,0 +1,24 @@ +import styled from "styled-components"; + +export const HeaderContainer = styled.div` + display: flex; + gap: 8px; + align-items: center; +`; + +export const ContentContainer = styled.div` + display: flex; +`; + +export const KeyValueSkeleton = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + flex-grow: 1; +`; + +export const FooterContainer = styled.div` + display: flex; + justify-content: space-between; + gap: 8px; +`; diff --git a/src/components/Insights/InsightsCatalogSkeleton/index.tsx b/src/components/Insights/InsightsCatalogSkeleton/index.tsx new file mode 100644 index 000000000..0c46fb18b --- /dev/null +++ b/src/components/Insights/InsightsCatalogSkeleton/index.tsx @@ -0,0 +1,25 @@ +import { Skeleton } from "../../common/Skeleton"; +import { InsightStatsSkeleton } from "../InsightsCatalog/InsightsStats/InsightStatsSkeleton"; +import { InsightCardSkeleton } from "./InsightCardSkeleton"; +import * as s from "./styles"; +import { InsightsCatalogSkeletonProps } from "./types"; + +export const InsightsCatalogSkeleton = ({ + insightViewType +}: InsightsCatalogSkeletonProps) => ( + + + + + + + + + {insightViewType === "Issues" && } + + + + + + +); diff --git a/src/components/Insights/InsightsCatalogSkeleton/styles.ts b/src/components/Insights/InsightsCatalogSkeleton/styles.ts new file mode 100644 index 000000000..fdba760dd --- /dev/null +++ b/src/components/Insights/InsightsCatalogSkeleton/styles.ts @@ -0,0 +1,49 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; + overflow: auto; +`; + +export const Toolbar = styled.div` + display: flex; + height: 28px; + flex-shrink: 0; + gap: 4px; + + & > * { + &:nth-child(1) { + width: 20%; + flex-grow: 1; + } + + &:nth-child(2) { + width: 40%; + flex-grow: 1; + } + + &:nth-child(3) { + width: 20%; + flex-grow: 1; + } + + &:nth-child(4) { + width: 28px; + flex-shrink: 0; + } + + &:nth-child(5) { + width: 28px; + flex-shrink: 0; + } + } +`; + +export const CardsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; diff --git a/src/components/Insights/InsightsCatalogSkeleton/types.ts b/src/components/Insights/InsightsCatalogSkeleton/types.ts new file mode 100644 index 000000000..dcf21cfed --- /dev/null +++ b/src/components/Insights/InsightsCatalogSkeleton/types.ts @@ -0,0 +1,5 @@ +import { InsightViewType } from "../types"; + +export interface InsightsCatalogSkeletonProps { + insightViewType: InsightViewType; +} diff --git a/src/components/Insights/index.tsx b/src/components/Insights/index.tsx index 2115f3062..0eae5c80b 100644 --- a/src/components/Insights/index.tsx +++ b/src/components/Insights/index.tsx @@ -13,7 +13,6 @@ import { isNumber } from "../../typeGuards/isNumber"; import { openURLInDefaultBrowser } from "../../utils/actions/openURLInDefaultBrowser"; import { sendUserActionTrackingEvent } from "../../utils/actions/sendUserActionTrackingEvent"; import { ConfigContext } from "../common/App/ConfigContext"; -import { CircleLoader } from "../common/CircleLoader"; import { EmptyState } from "../common/EmptyState"; import { RegistrationDialog } from "../common/RegistrationDialog"; import { RegistrationFormValues } from "../common/RegistrationDialog/types"; @@ -26,6 +25,7 @@ import { OpenTelemetryLogoCrossedSmallIcon } from "../common/icons/OpenTelemetry import { SlackLogoIcon } from "../common/icons/SlackLogoIcon"; import { InsightsCatalog } from "./InsightsCatalog"; import { SORTING_CRITERION } from "./InsightsCatalog/types"; +import { InsightsCatalogSkeleton } from "./InsightsCatalogSkeleton"; import { EndpointBottleneckInsightTicket } from "./insightTickets/EndpointBottleneckInsightTicket"; import { EndpointHighNumberOfQueriesInsightTicket } from "./insightTickets/EndpointHighNumberOfQueriesInsightTicket"; import { EndpointQueryOptimizationV2InsightTicket } from "./insightTickets/EndpointQueryOptimizationV2InsightTicket"; @@ -206,7 +206,7 @@ export const Insights = (props: InsightsProps) => { }; // const [isAutofixing, setIsAutofixing] = useState(false); const [query, setQuery] = useState(DEFAULT_QUERY); - const { isInitialLoading, data, refresh } = useInsightsData({ + const { isInitialLoading, data, refresh, isLoading } = useInsightsData({ refreshInterval: REFRESH_INTERVAL, query }); @@ -314,7 +314,10 @@ export const Insights = (props: InsightsProps) => { } }; - const renderDefaultContent = (data: InsightsData): JSX.Element => { + const renderDefaultContent = ( + data: InsightsData, + isLoading: boolean + ): JSX.Element => { return ( { unreadCount={data.unreadCount} isMarkingAsReadEnabled={isMarkingAsReadEnabled} hideInsightsStats={props.insightViewType === "Analytics"} + isLoading={isLoading} /> ); }; const renderContent = ( - data: InsightsData, - isInitialLoading: boolean + data: InsightsData | undefined, + isLoading: boolean ): JSX.Element => { - if (isInitialLoading) { - return } />; - } - - if (!config.environments?.length) { - return ; + if (!data || !config.environments?.length) { + return <>; } switch (data?.insightsStatus) { @@ -417,13 +417,20 @@ export const Insights = (props: InsightsProps) => { ); case InsightsStatus.DEFAULT: default: - return renderDefaultContent(data); + return renderDefaultContent(data, isLoading); } }; return ( - {renderContent(data, isInitialLoading)} + + + + + + {renderContent(data, isInitialLoading ? false : isLoading)} + + {infoToOpenJiraTicket && ( diff --git a/src/components/Insights/styles.ts b/src/components/Insights/styles.ts index 0cf012f74..2eeab56f7 100644 --- a/src/components/Insights/styles.ts +++ b/src/components/Insights/styles.ts @@ -1,11 +1,11 @@ import styled from "styled-components"; import { LAYERS } from "../common/App/styles"; +import { FadingContentSwitch } from "../common/FadingContentSwitch"; import { Link as CommonLink } from "../common/Link"; export const Container = styled.div` display: flex; flex-direction: column; - padding: 8px 0; gap: 8px; height: 100%; box-sizing: border-box; @@ -13,6 +13,17 @@ export const Container = styled.div` position: relative; `; +export const StyledFadingContentSwitch = styled(FadingContentSwitch)` + height: 100%; +`; + +export const FadingContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + height: 100%; +`; + export const StartupText = styled.span` display: flex; flex-direction: column; diff --git a/src/components/Insights/useInsightsData.ts b/src/components/Insights/useInsightsData.ts index e5fe1505a..666451788 100644 --- a/src/components/Insights/useInsightsData.ts +++ b/src/components/Insights/useInsightsData.ts @@ -76,12 +76,7 @@ export const useInsightsData = ({ refreshInterval, query }: UseInsightDataProps) => { - const [data, setData] = useState({ - insightsStatus: InsightsStatus.LOADING, - insights: [], - viewMode: ViewMode.INSIGHTS, - totalCount: 0 - }); + const [data, setData] = useState(); const previousData = usePrevious(data); const [isInitialLoading, setIsInitialLoading] = useState(false); const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); diff --git a/src/components/Tests/TestCardSkeleton/index.tsx b/src/components/Tests/TestCardSkeleton/index.tsx new file mode 100644 index 000000000..518810417 --- /dev/null +++ b/src/components/Tests/TestCardSkeleton/index.tsx @@ -0,0 +1,24 @@ +import { Skeleton } from "../../common/Skeleton"; +import { Card } from "../../common/v3/Card"; +import * as s from "./styles"; + +export const TestCardSkeleton = () => ( + + + + + } + content={ + + + + + + + + + } + /> +); diff --git a/src/components/Tests/TestCardSkeleton/styles.ts b/src/components/Tests/TestCardSkeleton/styles.ts new file mode 100644 index 000000000..2b080d12b --- /dev/null +++ b/src/components/Tests/TestCardSkeleton/styles.ts @@ -0,0 +1,21 @@ +import styled from "styled-components"; + +export const HeaderContainer = styled.div` + display: flex; + gap: 8px; + height: 24px; + align-items: center; +`; + +export const ContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const ButtonsContainer = styled.div` + display: flex; + justify-content: space-between; + height: 28px; + gap: 8px; +`; diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index 0e2d4ad32..be9263701 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -13,12 +13,12 @@ import { isNumber } from "../../typeGuards/isNumber"; import { sendTrackingEvent } from "../../utils/actions/sendTrackingEvent"; import { ConfigContext } from "../common/App/ConfigContext"; import { MenuItem } from "../common/FilterMenu/types"; -import { NewCircleLoader } from "../common/NewCircleLoader"; import { Pagination } from "../common/Pagination"; import { RegistrationDialog } from "../common/RegistrationDialog"; import { RegistrationFormValues } from "../common/RegistrationDialog/types"; import { EnvironmentFilter } from "./EnvironmentFilter"; import { TestCard } from "./TestCard"; +import { TestCardSkeleton } from "./TestCardSkeleton"; import { TestTicket } from "./TestTicket"; import { actions } from "./actions"; import * as s from "./styles"; @@ -274,14 +274,6 @@ export const Tests = (props: TestsProps) => { }; const renderContent = () => { - if (isInitialLoading) { - return ( - - - - ); - } - if (data?.error) { return {data.error.message}; } @@ -328,7 +320,16 @@ export const Tests = (props: TestsProps) => { onMenuItemClick={handleEnvironmentMenuItemClick} /> - {renderContent()} + + + + + + + + + {renderContent()} + {testToOpenTicketPopup && ( diff --git a/src/components/Tests/styles.ts b/src/components/Tests/styles.ts index 19e446980..a84cab003 100644 --- a/src/components/Tests/styles.ts +++ b/src/components/Tests/styles.ts @@ -1,5 +1,6 @@ import styled from "styled-components"; import { LAYERS } from "../common/App/styles"; +import { FadingContentSwitch } from "../common/FadingContentSwitch"; export const Container = styled.div` background: ${({ theme }) => theme.colors.panel.background}; @@ -9,6 +10,10 @@ export const Container = styled.div` overflow: hidden; `; +export const StyledFadingContentSwitch = styled(FadingContentSwitch)` + height: 100%; +`; + export const NoDataContainer = styled.div` flex-grow: 1; display: flex; @@ -19,6 +24,7 @@ export const NoDataContainer = styled.div` text-align: center; color: ${({ theme }) => theme.colors.text.subtext}; font-size: 14px; + height: 100%; `; export const EnvironmentFilterContainer = styled.div` @@ -37,6 +43,7 @@ export const ContentContainer = styled.div` padding: 8px 12px; gap: 12px; overflow: auto; + box-sizing: border-box; `; export const TestsList = styled.div` diff --git a/src/components/common/FadingContentSwitch/FadingContentSwitch.stories.tsx b/src/components/common/FadingContentSwitch/FadingContentSwitch.stories.tsx new file mode 100644 index 000000000..988f7bfcd --- /dev/null +++ b/src/components/common/FadingContentSwitch/FadingContentSwitch.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { FadingContentSwitch } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Common/FadingContentSwitch", + component: FadingContentSwitch, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = { + args: { + switchFlag: true, + children: ["Content A", "Content B"] + } +}; diff --git a/src/components/common/FadingContentSwitch/index.tsx b/src/components/common/FadingContentSwitch/index.tsx new file mode 100644 index 000000000..15e147026 --- /dev/null +++ b/src/components/common/FadingContentSwitch/index.tsx @@ -0,0 +1,70 @@ +import { ForwardedRef, forwardRef, useEffect, useRef, useState } from "react"; +import { CSSTransition } from "react-transition-group"; +import * as s from "./styles"; +import { FadingContentSwitchProps } from "./types"; + +const TRANSITION_CLASS_NAME = "fading"; +const TRANSITION_DURATION = 0; +const TRANSITION_DELAY = 200; + +export const FadingContentSwitchComponent = ( + { switchFlag, children, className }: FadingContentSwitchProps, + ref: ForwardedRef +) => { + const [showTransition, setShowTransition] = useState(false); + const contentAContainerRef = useRef(null); + const contentBContainerRef = useRef(null); + + useEffect(() => { + const timer = window.setTimeout(() => { + setShowTransition(true); + }, TRANSITION_DELAY); + + return () => { + window.clearTimeout(timer); + }; + }, [switchFlag]); + + if (children.length !== 2) { + return null; + } + + return ( + + + + {children[0]} + + + + + {children[1]} + + + + ); +}; + +export const FadingContentSwitch = forwardRef(FadingContentSwitchComponent); diff --git a/src/components/common/FadingContentSwitch/styles.ts b/src/components/common/FadingContentSwitch/styles.ts new file mode 100644 index 000000000..5b8c8b281 --- /dev/null +++ b/src/components/common/FadingContentSwitch/styles.ts @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import { FadingContainerProps } from "./types"; + +export const Container = styled.div` + position: relative; +`; + +export const FadingContentContainer = styled.div` + position: absolute; + width: 100%; + height: 100%; + + ${({ $transitionClassName, $transitionDuration }) => { + return ` + &.${$transitionClassName}-enter { + opacity: 0; + } + + &.${$transitionClassName}-enter-active { + opacity: 1; + transition: opacity ${$transitionDuration}ms ease-out; + } + + &.${$transitionClassName}-exit { + opacity: 1; + } + + &.${$transitionClassName}-exit-active { + opacity: 0; + transition: opacity ${$transitionDuration}ms ease-out; + } + `; + }} +`; diff --git a/src/components/common/FadingContentSwitch/types.ts b/src/components/common/FadingContentSwitch/types.ts new file mode 100644 index 000000000..ec2c3df1a --- /dev/null +++ b/src/components/common/FadingContentSwitch/types.ts @@ -0,0 +1,12 @@ +import { ReactNode } from "react"; + +export interface FadingContentSwitchProps { + switchFlag: boolean; + children: ReactNode[]; + className?: string; +} + +export interface FadingContainerProps { + $transitionDuration: number; + $transitionClassName: string; +} diff --git a/src/components/common/Skeleton/Skeleton.stories.tsx b/src/components/common/Skeleton/Skeleton.stories.tsx new file mode 100644 index 000000000..4f31088ba --- /dev/null +++ b/src/components/common/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { Skeleton } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Common/Skeleton", + component: Skeleton, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Rectangle: Story = { + args: { + type: "rectangle" + } +}; + +export const Circle: Story = { + args: { + type: "circle" + } +}; + +export const Text: Story = { + args: { + type: "text" + } +}; diff --git a/src/components/common/Skeleton/index.tsx b/src/components/common/Skeleton/index.tsx new file mode 100644 index 000000000..a9764d480 --- /dev/null +++ b/src/components/common/Skeleton/index.tsx @@ -0,0 +1,21 @@ +import * as s from "./styles"; +import { SkeletonProps } from "./types"; + +export const Skeleton = ({ type, height, width, gradient }: SkeletonProps) => { + switch (type) { + case "rectangle": + return ( + + ); + case "circle": + return ; + case "text": + return ( + + ); + } +}; diff --git a/src/components/common/Skeleton/styles.ts b/src/components/common/Skeleton/styles.ts new file mode 100644 index 000000000..a4130479f --- /dev/null +++ b/src/components/common/Skeleton/styles.ts @@ -0,0 +1,73 @@ +import styled, { css, keyframes } from "styled-components"; +import { isNumber } from "../../../typeGuards/isNumber"; +import { hexToRgb } from "../../../utils/hexToRgb"; +import { SkeletonElementProps } from "./types"; + +const blinkingAnimation = keyframes` +from { opacity: 0.05; } +to { opacity: 0.1; } +`; + +const Skeleton = styled.div` + opacity: 0.05; + animation-name: ${blinkingAnimation}; + animation-delay: 200ms; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-duration: 400ms; + ${({ theme, $gradient }) => { + if ($gradient) { + const rgb = hexToRgb(theme.colors.v3.icon.primary); + const rgbString = rgb ? `${rgb.r} ${rgb.g} ${rgb.b}` : ""; + + if (rgbString) { + return css` + background-image: linear-gradient( + to right, + rgb(${rgbString} / 100%), + rgb(${rgbString} / 0%) + ); + `; + } + } + + return css` + background: ${theme.colors.v3.icon.primary}; + `; + }} +`; + +export const RectangleSkeleton = styled(Skeleton)` + height: ${({ $height }) => (isNumber($height) ? `${$height}px` : "100%")}; + width: ${({ $width }) => (isNumber($width) ? `${$width}px` : "100%")}; + border-radius: 4px; +`; + +export const CircleSkeleton = styled(Skeleton)` + ${({ $height }) => + isNumber($height) + ? css` + width: ${$height}px; + height: ${$height}px; + ` + : css` + width: 24px; + height: 24px; + `} + border-radius: 50%; +`; + +export const TextSkeleton = styled(Skeleton)` + ${({ $height }) => + isNumber($height) + ? css` + height: ${$height}px; + border-radius: ${$height / 2}px; + ` + : css` + height: 16px; + border-radius: 8px; + `} + width: ${({ $width }) => (isNumber($width) ? `${$width}px` : "100%")}; +`; diff --git a/src/components/common/Skeleton/types.ts b/src/components/common/Skeleton/types.ts new file mode 100644 index 000000000..3d8f21176 --- /dev/null +++ b/src/components/common/Skeleton/types.ts @@ -0,0 +1,12 @@ +export interface SkeletonProps { + width?: number; + height?: number; + gradient?: boolean; + type: "rectangle" | "circle" | "text"; +} + +export interface SkeletonElementProps { + $width?: number; + $height?: number; + $gradient?: boolean; +} diff --git a/src/utils/hexToRgb.ts b/src/utils/hexToRgb.ts new file mode 100644 index 000000000..a947f259e --- /dev/null +++ b/src/utils/hexToRgb.ts @@ -0,0 +1,11 @@ +export function hexToRgb(hex: string) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } + : null; +}