From 8da29105afba4a3b720ebb9404d6ad0e30abe48b Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 23 Jan 2024 10:03:52 +0100 Subject: [PATCH 01/18] Add Select and Asset Filters component --- .../FiltersMenu/FiltersMenu.stories.tsx | 20 +++ src/components/Assets/FiltersMenu/index.tsx | 128 +++++++++++++++++ src/components/Assets/FiltersMenu/styles.ts | 45 ++++++ src/components/Assets/FiltersMenu/types.ts | 1 + src/components/Assets/actions.ts | 4 +- src/components/Assets/index.tsx | 2 + src/components/common/App/getTheme.ts | 10 ++ src/components/common/NewPopover/index.tsx | 3 + src/components/common/NewPopover/types.ts | 1 + .../common/Select/Select.stories.tsx | 84 +++++++++++ src/components/common/Select/index.tsx | 94 +++++++++++++ src/components/common/Select/styles.ts | 130 ++++++++++++++++++ src/components/common/Select/types.ts | 25 ++++ src/styled.d.ts | 2 + 14 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 src/components/Assets/FiltersMenu/FiltersMenu.stories.tsx create mode 100644 src/components/Assets/FiltersMenu/index.tsx create mode 100644 src/components/Assets/FiltersMenu/styles.ts create mode 100644 src/components/Assets/FiltersMenu/types.ts create mode 100644 src/components/common/Select/Select.stories.tsx create mode 100644 src/components/common/Select/index.tsx create mode 100644 src/components/common/Select/styles.ts create mode 100644 src/components/common/Select/types.ts diff --git a/src/components/Assets/FiltersMenu/FiltersMenu.stories.tsx b/src/components/Assets/FiltersMenu/FiltersMenu.stories.tsx new file mode 100644 index 000000000..fa42e50cd --- /dev/null +++ b/src/components/Assets/FiltersMenu/FiltersMenu.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { FiltersMenu } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Assets/FiltersMenu", + component: FiltersMenu, + 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 = {}; diff --git a/src/components/Assets/FiltersMenu/index.tsx b/src/components/Assets/FiltersMenu/index.tsx new file mode 100644 index 000000000..7e841e61c --- /dev/null +++ b/src/components/Assets/FiltersMenu/index.tsx @@ -0,0 +1,128 @@ +import { useEffect, useState } from "react"; +import { InsightType } from "../../../types"; +import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; +import { NewButton } from "../../common/NewButton"; +import { NewPopover } from "../../common/NewPopover"; +import { Select } from "../../common/Select"; +import { ChevronIcon } from "../../common/icons/ChevronIcon"; +import { Direction } from "../../common/icons/types"; +import { actions } from "../actions"; +import * as s from "./styles"; +import { FiltersMenuProps } from "./types"; + +const insights = Object.values(InsightType).map((x) => x); + +export const FiltersMenu = (props: FiltersMenuProps) => { + const [services, setServices] = useState(); + const [selectedServices, setSelectedServices] = useState([]); + const [selectedInsights, setSelectedInsights] = useState([]); + const [isOpen, setIsOpen] = useState(false); + + const serviceItems = (services || []).map((x) => ({ + value: x, + label: x, + selected: selectedServices.includes(x) + })); + + const insightItems = insights + .map((x) => ({ + value: x as string, + label: getInsightTypeInfo(x)?.label || "", + selected: selectedInsights.includes(x) + })) + .filter((x) => x.label.length > 0); + + useEffect(() => { + window.sendMessageToDigma({ + action: actions.GET_ASSET_FILTERS_DATA + }); + }, []); + + const handleServicesMenuItemClick = (service: string) => { + const oldSelectedServices = selectedServices || []; + const serviceIndex = oldSelectedServices.findIndex((x) => x === service); + + if (serviceIndex < 0) { + setSelectedServices([...oldSelectedServices, service]); + } else { + setSelectedServices([ + ...oldSelectedServices.slice(0, serviceIndex), + ...oldSelectedServices.slice(serviceIndex + 1) + ]); + } + }; + + const handleInsightsMenuItemClick = (insightType: string) => { + // TODO: + }; + + const handleClearFiltersButtonClick = () => { + setSelectedServices([]); + }; + const handleApplyButtonClick = () => { + // TODO: send filters message + }; + + const areFiltersSet = selectedServices.length > 0; + + return ( + + Filters + Services + + + Insights + + ); + }, + args: { + counts: { + filtered: 6, + total: 10 + }, + items: [ + { + label: "Item 1", + value: "item_1" + }, + { + label: "Item 2", + value: "item_2" + }, + { + label: "Item 3", + value: "item_3" + }, + { + label: "Item 4", + value: "item_4" + }, + { + label: "Item 5", + value: "item_5" + }, + { + label: "Item 6", + value: "item_6" + } + ] + } +}; diff --git a/src/components/common/Select/index.tsx b/src/components/common/Select/index.tsx new file mode 100644 index 000000000..95ac6d58f --- /dev/null +++ b/src/components/common/Select/index.tsx @@ -0,0 +1,94 @@ +import { useState } from "react"; +import { isString } from "../../../typeGuards/isString"; +import { Checkbox } from "../Checkbox"; +import { NewPopover } from "../NewPopover"; +import { ChevronIcon } from "../icons/ChevronIcon"; +import { MagnifierIcon } from "../icons/MagnifierIcon"; +import { Direction } from "../icons/types"; +import * as s from "./styles"; +import { SelectProps } from "./types"; + +export const Select = (props: SelectProps) => { + const [isOpen, setIsOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); + + const handleButtonClick = () => { + setIsOpen(!isOpen); + }; + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }; + + const selectedValues = props.items + .filter((x) => x.selected) + .map((x) => x.value); + + const filteredItems = props.items.filter((x) => + x.label.toLocaleLowerCase().includes(searchValue) + ); + + return ( + + + + + + + + + {filteredItems.length > 0 ? ( + filteredItems.map((x) => { + const handleClick = () => { + props.onItemClick(x.value); + }; + + return ( + + undefined} + /> + {x.label} + + ); + }) + ) : ( + No results + )} + + + } + onOpenChange={setIsOpen} + isOpen={isOpen} + placement={"bottom-start"} + > + + {isString(props.title) && {props.title}} + {selectedValues.length > 0 && ( + {selectedValues.length} + )} + {props.counts && ( + + {props.counts.filtered}/ + {props.counts.total} + + )} + + + + + + ); +}; diff --git a/src/components/common/Select/styles.ts b/src/components/common/Select/styles.ts new file mode 100644 index 000000000..63f4a18ce --- /dev/null +++ b/src/components/common/Select/styles.ts @@ -0,0 +1,130 @@ +import styled from "styled-components"; +import { grayScale, primaryScale } from "../App/getTheme"; +import { ButtonProps } from "./types"; + +export const Button = styled.button` + border: 1px solid + ${({ theme, $isOpen }) => + $isOpen ? theme.colors.stroke.brand : theme.colors.stroke.primary}; + background: ${({ theme }) => theme.colors.surface.secondary}; + border-radius: 4px; + padding: 4px 6px 4px 4px; + display: flex; + gap: 6px; + align-items: center; + width: 100%; + justify-content: flex-end; + font-size: 14px; + + &:hover { + border: 1px solid ${({ theme }) => theme.colors.stroke.secondary}; + } + + &:focus, + &:active { + border: 1px solid ${({ theme }) => theme.colors.stroke.brand}; + } +`; + +export const ButtonLabel = styled.span` + display: flex; + gap: 4px; + align-items: center; + font-size: 14px; + color: ${({ theme }) => theme.colors.text.base}; + margin-right: auto; +`; + +export const Number = styled.span` + display: flex; + justify-content: center; + align-items: center; + line-height: 100%; + color: ${grayScale[0]}; + width: 16px; + height: 16px; + border-radius: 4px; + background: ${primaryScale[300]}; + margin-left: auto; +`; + +export const Counts = styled.span` + color: ${grayScale[300]}; +`; + +export const FilteredCount = styled.span` + color: ${primaryScale[200]}; +`; + +export const ChevronIconContainer = styled.span` + color: ${({ theme }) => theme.colors.icon.primary}; +`; + +export const MenuContainer = styled.div` + padding: 4px; + border-radius: 4px; + border: 1px solid ${grayScale[1100]}; + background: ${grayScale[800]}; + box-shadow: 0 2px 4px 0 rgba(0 0 0 / 2%); + gap: 4px; + display: flex; + flex-direction: column; + max-height: 162px; + box-sizing: border-box; +`; + +export const SearchInputContainer = styled.div` + border-radius: 4px; + border: 1px solid ${grayScale[700]}; + padding: 5px 8px; + gap: 4px; + display: flex; + align-items: center; +`; + +export const SearchInput = styled.input` + width: 100%; + background: transparent; + border: none; + outline: none; + font-size: 14px; + padding: 0; + color: ${grayScale[0] /* TODO: use theme */}; + + &::placeholder { + color: ${grayScale[500]}; + } +`; + +export const SearchInputIconContainer = styled.div` + pointer-events: none; + color: ${grayScale[400]}; +`; + +export const OptionList = styled.ul` + display: flex; + flex-direction: column; + gap: 4px; + padding: 0; + margin: 0; + overflow-y: auto; + font-size: 14px; +`; + +export const OptionListItem = styled.li` + display: flex; + padding: 4px 8px; + align-items: center; + gap: 6px; + align-self: stretch; + cursor: pointer; +`; + +export const NoResultsContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 8px 0; + font-size: 14px; + color: ${({ theme }) => theme.colors.text.subtext}; +`; diff --git a/src/components/common/Select/types.ts b/src/components/common/Select/types.ts new file mode 100644 index 000000000..4ace9d384 --- /dev/null +++ b/src/components/common/Select/types.ts @@ -0,0 +1,25 @@ +export interface SelectThemeColors { + menu: { + background: string; + }; +} + +export interface SelectProps { + items: { + value: string; + label: string; + selected?: boolean; + }[]; + multiselect?: boolean; + searchable?: boolean; + title?: string; + onItemClick: (value: string) => void; + counts?: { + total: number; + filtered: number; + }; +} + +export interface ButtonProps { + $isOpen: boolean; +} diff --git a/src/styled.d.ts b/src/styled.d.ts index bd527b23e..b03e45c6c 100644 --- a/src/styled.d.ts +++ b/src/styled.d.ts @@ -7,6 +7,7 @@ import { ToggleThemeColors } from "./components/RecentActivity/Toggle/types"; import { RecentActivityThemeColors } from "./components/RecentActivity/types"; import { IconTagThemeColors } from "./components/common/IconTag/types"; import { ButtonThemeColors } from "./components/common/NewButton/types"; +import { SelectThemeColors } from "./components/common/Select/types"; import { TagThemeColors } from "./components/common/Tag/types"; import { TooltipThemeColors } from "./components/common/Tooltip/types"; import { Mode } from "./globals"; @@ -35,6 +36,7 @@ export interface ThemeColors { attachmentTag: AttachmentTagThemeColors; jiraTicket: JiraTicketThemeColors; field: FieldThemeColors; + select: SelectThemeColors; panel: { background: string; }; From 87a79da1d589b542d02341fd0f41666f93680bec Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 29 Jan 2024 08:23:27 +0100 Subject: [PATCH 02/18] Populate filter selects with data --- src/components/Assets/AssetList/index.tsx | 34 ++- src/components/Assets/AssetList/types.ts | 2 + src/components/Assets/AssetTypeList/index.tsx | 22 +- src/components/Assets/AssetTypeList/types.ts | 3 + .../AssetsFilter/AssetsFilter.stories.tsx | 211 ++++++++++++++ src/components/Assets/AssetsFilter/index.tsx | 275 ++++++++++++++++++ .../{FiltersMenu => AssetsFilter}/styles.ts | 16 +- src/components/Assets/AssetsFilter/types.ts | 26 ++ .../FiltersMenu/FiltersMenu.stories.tsx | 20 -- src/components/Assets/FiltersMenu/index.tsx | 128 -------- src/components/Assets/FiltersMenu/types.ts | 1 - .../Assets/ServicesFilter/index.tsx | 170 +++++++++++ .../Assets/ServicesFilter/styles.ts | 67 +++++ src/components/Assets/ServicesFilter/types.ts | 4 + src/components/Assets/index.tsx | 198 ++----------- src/components/Assets/styles.ts | 66 ----- src/components/common/App/getTheme.ts | 6 + .../common/Select/Select.stories.tsx | 20 +- src/components/common/Select/index.tsx | 55 ++-- src/components/common/Select/types.ts | 20 +- src/featureFlags.ts | 3 +- src/types.ts | 3 +- 22 files changed, 901 insertions(+), 449 deletions(-) create mode 100644 src/components/Assets/AssetsFilter/AssetsFilter.stories.tsx create mode 100644 src/components/Assets/AssetsFilter/index.tsx rename src/components/Assets/{FiltersMenu => AssetsFilter}/styles.ts (65%) create mode 100644 src/components/Assets/AssetsFilter/types.ts delete mode 100644 src/components/Assets/FiltersMenu/FiltersMenu.stories.tsx delete mode 100644 src/components/Assets/FiltersMenu/index.tsx delete mode 100644 src/components/Assets/FiltersMenu/types.ts create mode 100644 src/components/Assets/ServicesFilter/index.tsx create mode 100644 src/components/Assets/ServicesFilter/styles.ts create mode 100644 src/components/Assets/ServicesFilter/types.ts diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index f60fd1dec..be19089c2 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -162,6 +162,7 @@ export const AssetList = (props: AssetListProps) => { const previousEnvironment = usePrevious(config.environment); const previousAssetTypeId = usePrevious(props.assetTypeId); const previousServices = usePrevious(props.services); + const previousFilters = usePrevious(props.filters); const entries = data?.data || []; @@ -191,7 +192,7 @@ export const AssetList = (props: AssetListProps) => { ...(debouncedSearchInputValue.length > 0 ? { displayName: debouncedSearchInputValue } : {}), - services: props.services + ...(props.filters || { services: props.services }) } } }); @@ -212,15 +213,17 @@ export const AssetList = (props: AssetListProps) => { useEffect(() => { if ( - (isNumber(previousPage) && previousPage !== page) || - (isString(previousEnvironment) && - previousEnvironment !== config.environment) || - (previousSorting && previousSorting !== sorting) || - (isString(previousDebouncedSearchInputValue) && - previousDebouncedSearchInputValue !== debouncedSearchInputValue) || - (isString(previousAssetTypeId) && - previousAssetTypeId !== props.assetTypeId) || - (Array.isArray(previousServices) && previousServices !== props.services) + ((isNumber(previousPage) && previousPage !== page) || + (isString(previousEnvironment) && + previousEnvironment !== config.environment) || + (previousSorting && previousSorting !== sorting) || + (isString(previousDebouncedSearchInputValue) && + previousDebouncedSearchInputValue !== debouncedSearchInputValue) || + (isString(previousAssetTypeId) && + previousAssetTypeId !== props.assetTypeId) || + (Array.isArray(previousServices) && + previousServices !== props.services), + previousFilters && previousFilters !== props.filters) ) { window.sendMessageToDigma({ action: actions.GET_DATA, @@ -234,7 +237,7 @@ export const AssetList = (props: AssetListProps) => { ...(debouncedSearchInputValue.length > 0 ? { displayName: debouncedSearchInputValue } : {}), - services: props.services + ...(props.filters || { services: props.services }) } } }); @@ -251,7 +254,9 @@ export const AssetList = (props: AssetListProps) => { previousEnvironment, config.environment, props.services, - previousServices + previousServices, + props.filters, + previousFilters ]); useEffect(() => { @@ -270,7 +275,7 @@ export const AssetList = (props: AssetListProps) => { ...(debouncedSearchInputValue.length > 0 ? { displayName: debouncedSearchInputValue } : {}), - services: props.services + ...(props.filters || { services: props.services }) } } }); @@ -284,7 +289,8 @@ export const AssetList = (props: AssetListProps) => { sorting, debouncedSearchInputValue, config.environment, - props.services + props.services, + props.filters ]); useEffect(() => { diff --git a/src/components/Assets/AssetList/types.ts b/src/components/Assets/AssetList/types.ts index 7c1678a73..5f38e5e6a 100644 --- a/src/components/Assets/AssetList/types.ts +++ b/src/components/Assets/AssetList/types.ts @@ -1,10 +1,12 @@ import { Duration } from "../../../globals"; +import { AssetFilterQuery } from "../AssetsFilter/types"; export interface AssetListProps { data?: AssetsData; onBackButtonClick: () => void; assetTypeId: string; services: string[]; + filters?: AssetFilterQuery; } export enum SORTING_CRITERION { diff --git a/src/components/Assets/AssetTypeList/index.tsx b/src/components/Assets/AssetTypeList/index.tsx index ba5e18b29..2c8445688 100644 --- a/src/components/Assets/AssetTypeList/index.tsx +++ b/src/components/Assets/AssetTypeList/index.tsx @@ -39,12 +39,13 @@ export const AssetTypeList = (props: AssetTypeListProps) => { const previousEnvironment = usePrevious(config.environment); const refreshTimerId = useRef(); const previousServices = usePrevious(props.services); + const previousFilters = usePrevious(props.filters); useEffect(() => { window.sendMessageToDigma({ action: actions.GET_CATEGORIES_DATA, payload: { - services: props.services + ...(props.filters || { services: props.services }) } }); setIsInitialLoading(true); @@ -72,12 +73,14 @@ export const AssetTypeList = (props: AssetTypeListProps) => { if ( (isString(previousEnvironment) && previousEnvironment !== config.environment) || - (Array.isArray(previousServices) && previousServices !== props.services) + (Array.isArray(previousServices) && + previousServices !== props.services) || + (previousFilters && previousFilters !== props.filters) ) { window.sendMessageToDigma({ action: actions.GET_CATEGORIES_DATA, payload: { - services: props.services + ...(props.filters || { services: props.services }) } }); } @@ -85,7 +88,9 @@ export const AssetTypeList = (props: AssetTypeListProps) => { previousEnvironment, config.environment, previousServices, - props.services + props.services, + previousFilters, + props.filters ]); useEffect(() => { @@ -95,12 +100,17 @@ export const AssetTypeList = (props: AssetTypeListProps) => { window.sendMessageToDigma({ action: actions.GET_CATEGORIES_DATA, payload: { - services: props.services + ...(props.filters || { services: props.services }) } }); }, REFRESH_INTERVAL); } - }, [props.services, previousLastSetDataTimeStamp, lastSetDataTimeStamp]); + }, [ + props.services, + previousLastSetDataTimeStamp, + lastSetDataTimeStamp, + props.filters + ]); useEffect(() => { if (props.data) { diff --git a/src/components/Assets/AssetTypeList/types.ts b/src/components/Assets/AssetTypeList/types.ts index 5ad35604f..ba18d534e 100644 --- a/src/components/Assets/AssetTypeList/types.ts +++ b/src/components/Assets/AssetTypeList/types.ts @@ -1,7 +1,10 @@ +import { AssetFilterQuery } from "../AssetsFilter/types"; + export interface AssetTypeListProps { data?: AssetCategoriesData; onAssetTypeSelect: (assetTypeId: string) => void; services: string[]; + filters?: AssetFilterQuery; } export interface AssetCategoriesData { diff --git a/src/components/Assets/AssetsFilter/AssetsFilter.stories.tsx b/src/components/Assets/AssetsFilter/AssetsFilter.stories.tsx new file mode 100644 index 000000000..923e32202 --- /dev/null +++ b/src/components/Assets/AssetsFilter/AssetsFilter.stories.tsx @@ -0,0 +1,211 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { AssetsFilter } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Assets/AssetsFilter", + component: AssetsFilter, + 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: { + data: { + categories: [ + { + categoryName: "Services", + entries: [ + { + enabled: true, + selected: true, + name: "ClientTesterOfPetClinic" + }, + { + enabled: true, + selected: false, + name: "spring-petclinic" + } + ] + }, + { + categoryName: "Operations", + categories: [ + { + categoryName: "Endpoints", + entries: [ + { + enabled: true, + selected: true, + name: "HTTP GET /" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /**" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /oups" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /owners" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /owners/{ownerId}" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /owners/find" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /owners/new" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/ErrorHotspot" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/ErrorRecordedOnCurrentSpan" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/ErrorRecordedOnDeeplyNestedSpan" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/ErrorRecordedOnLocalRootSpan" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/GenAsyncSpanVar01" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/HighUsage" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/NPlusOneWithInternalSpan" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/NPlusOneWithoutInternalSpan" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/req-map-get" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/SlowEndpoint" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /SampleInsights/SpanBottleneck" + }, + { + enabled: true, + selected: false, + name: "HTTP GET /vets.html" + } + ] + }, + { + categoryName: "Consumers", + entries: [] + }, + { + categoryName: "Internal", + entries: [ + { + enabled: true, + selected: false, + name: "ClientTester.generateInsightData" + } + ] + } + ] + }, + { + categoryName: "Insights", + entries: [ + { + enabled: true, + selected: false, + name: "EndpointBreakdown" + }, + { + enabled: true, + selected: false, + name: "EndpointChattyApi" + }, + { + enabled: true, + selected: false, + name: "EndpointHighNumberOfQueries" + }, + { + enabled: true, + selected: false, + name: "EndpointSpaNPlusOne" + }, + { + enabled: true, + selected: false, + name: "HighUsage" + }, + { + enabled: true, + selected: false, + name: "LowUsage" + }, + { + enabled: true, + selected: false, + name: "SlowEndpoint" + }, + { + enabled: true, + selected: false, + name: "SlowestSpans" + }, + { + enabled: true, + selected: false, + name: "SpanScalingInsufficientData" + } + ] + } + ] + } + } +}; diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx new file mode 100644 index 000000000..e8d86ce41 --- /dev/null +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -0,0 +1,275 @@ +import { useEffect, useState } from "react"; +import { dispatcher } from "../../../dispatcher"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { isBoolean } from "../../../typeGuards/isBoolean"; +import { InsightType } from "../../../types"; +import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; +import { NewButton } from "../../common/NewButton"; +import { NewPopover } from "../../common/NewPopover"; +import { Select } from "../../common/Select"; +import { ChevronIcon } from "../../common/icons/ChevronIcon"; +import { Direction } from "../../common/icons/types"; +import { actions } from "../actions"; +import * as s from "./styles"; +import { + AssetFilterCategory, + AssetsFilterProps, + AssetsFiltersData +} from "./types"; + +const renderFilterCategory = ( + category: AssetFilterCategory, + placeholder: string, + selectedValues: string[], + onChange: (value: string | string[], categoryName?: string) => void, + transformLabel?: (value: string) => string +): JSX.Element => { + const items = + category.entries?.map((entry) => ({ + value: entry.name, + label: transformLabel ? transformLabel(entry.name) : entry.name, + enabled: entry.enabled, + selected: selectedValues.includes(entry.name) + })) || []; + + return ( + - Operations - - - - - - - - } - onOpenChange={setIsOpen} - isOpen={isOpen} - placement={"bottom-end"} - > - - Filters - - - - - - ); -}; diff --git a/src/components/Assets/FiltersMenu/types.ts b/src/components/Assets/FiltersMenu/types.ts deleted file mode 100644 index 15bcc6efa..000000000 --- a/src/components/Assets/FiltersMenu/types.ts +++ /dev/null @@ -1 +0,0 @@ -export interface FiltersMenuProps {} diff --git a/src/components/Assets/ServicesFilter/index.tsx b/src/components/Assets/ServicesFilter/index.tsx new file mode 100644 index 000000000..9ff2af492 --- /dev/null +++ b/src/components/Assets/ServicesFilter/index.tsx @@ -0,0 +1,170 @@ +import { useContext, useEffect, useState } from "react"; +import { dispatcher } from "../../../dispatcher"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { isNumber } from "../../../typeGuards/isNumber"; +import { isString } from "../../../typeGuards/isString"; +import { ConfigContext } from "../../common/App/ConfigContext"; +import { NewPopover } from "../../common/NewPopover"; +import { ChevronIcon } from "../../common/icons/ChevronIcon"; +import { FilterIcon } from "../../common/icons/FilterIcon"; +import { Direction } from "../../common/icons/types"; +import { FilterMenu } from "../FilterMenu"; +import { actions } from "../actions"; +import { ServiceData } from "../types"; +import * as s from "./styles"; +import { ServicesFilterProps } from "./types"; + +const REFRESH_INTERVAL = isNumber(window.assetsRefreshInterval) + ? window.assetsRefreshInterval + : 10 * 1000; // in milliseconds + +const preselectedServices = + Array.isArray(window.assetsSelectedServices) && + window.assetsSelectedServices.every(isString) + ? window.assetsSelectedServices + : []; + +export const ServicesFilter = (props: ServicesFilterProps) => { + const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false); + const [areServicesLoading, setAreServicesLoading] = useState(false); + const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const [services, setServices] = useState(); + const previousSelectedServices = usePrevious(props.selectedServices); + const config = useContext(ConfigContext); + const previousEnvironment = usePrevious(config.environment); + + useEffect(() => { + window.sendMessageToDigma({ + action: actions.GET_SERVICES + }); + setAreServicesLoading(true); + + const handleServicesData = (data: unknown, timeStamp: number) => { + const serviceData = data as ServiceData; + setLastSetDataTimeStamp(timeStamp); + if (services === undefined) { + const oldSelectedServices = Array.isArray(props.selectedServices) + ? props.selectedServices + : preselectedServices; + + const newSelectedServices = serviceData.services.filter((x) => + oldSelectedServices.includes(x) + ); + + props.onChange(newSelectedServices); + } + setServices(serviceData.services); + setAreServicesLoading(false); + }; + + dispatcher.addActionListener(actions.SET_SERVICES, handleServicesData); + + return () => { + dispatcher.removeActionListener(actions.SET_SERVICES, handleServicesData); + }; + }, []); + + useEffect(() => { + if ( + isString(previousEnvironment) && + previousEnvironment !== config.environment + ) { + setServices(undefined); + window.sendMessageToDigma({ + action: actions.GET_SERVICES + }); + setAreServicesLoading(true); + } + }, [previousEnvironment, config.environment, services]); + + useEffect(() => { + const timerId = window.setTimeout(() => { + window.sendMessageToDigma({ + action: actions.GET_SERVICES + }); + }, REFRESH_INTERVAL); + + return () => { + window.clearTimeout(timerId); + }; + }, [lastSetDataTimeStamp]); + + useEffect(() => { + if ( + previousSelectedServices && + previousSelectedServices !== props.selectedServices + ) { + window.sendMessageToDigma({ + action: actions.SET_SELECTED_SERVICES, + payload: { + services: props.selectedServices + } + }); + } + }, [previousSelectedServices, props.selectedServices]); + + const handleServiceMenuClose = () => { + setIsServiceMenuOpen(false); + }; + + const handleServiceMenuItemClick = (service: string) => { + const oldSelectedServices = props.selectedServices || []; + const serviceIndex = oldSelectedServices.findIndex((x) => x === service); + + if (serviceIndex < 0) { + props.onChange([...oldSelectedServices, service]); + } else { + props.onChange([ + ...oldSelectedServices.slice(0, serviceIndex), + ...oldSelectedServices.slice(serviceIndex + 1) + ]); + } + }; + + const filterMenuItems = (services || []).map((x) => ({ + label: x, + value: x, + selected: (props.selectedServices || []).includes(x) + })); + + return ( + + } + width={"max-content"} + onOpenChange={setIsServiceMenuOpen} + isOpen={isServiceMenuOpen} + placement={"bottom-end"} + > + + + + Services + {props.selectedServices && + props.selectedServices.length > 0 && + !areServicesLoading ? ( + {props.selectedServices.length} + ) : ( + + All + + )} + + + + + + + ); +}; diff --git a/src/components/Assets/ServicesFilter/styles.ts b/src/components/Assets/ServicesFilter/styles.ts new file mode 100644 index 000000000..03db0e560 --- /dev/null +++ b/src/components/Assets/ServicesFilter/styles.ts @@ -0,0 +1,67 @@ +import styled from "styled-components"; + +export const ServiceMenuButton = styled.button` + border: 1px solid #4e5157; + background: transparent; + border-radius: 4px; + padding: 8px; + display: flex; + gap: 10px; + align-items: center; +`; + +export const ServiceMenuButtonLabel = styled.span` + display: flex; + gap: 4px; + align-items: center; + font-size: 14px; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#818594"; + case "dark": + case "dark-jetbrains": + return "#b4b8bf"; + } + }}; +`; + +export const SelectedServiceNumberPlaceholder = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; + user-select: none; +`; + +export const Number = styled.span` + min-width: 18px; + height: 18px; + flex-shrink: 0; + font-size: 14px; + line-height: 100%; + font-weight: 500; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + background: #5053d4; +`; + +export const ServiceMenuChevronIconContainer = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; diff --git a/src/components/Assets/ServicesFilter/types.ts b/src/components/Assets/ServicesFilter/types.ts new file mode 100644 index 000000000..4e5641d78 --- /dev/null +++ b/src/components/Assets/ServicesFilter/types.ts @@ -0,0 +1,4 @@ +export interface ServicesFilterProps { + selectedServices?: string[]; + onChange: (services: string[]) => void; +} diff --git a/src/components/Assets/index.tsx b/src/components/Assets/index.tsx index 4fb9bd78d..2ea71bfd8 100644 --- a/src/components/Assets/index.tsx +++ b/src/components/Assets/index.tsx @@ -1,155 +1,41 @@ -import { - useContext, - useEffect, - useLayoutEffect, - useMemo, - useState -} from "react"; -import { dispatcher } from "../../dispatcher"; +import { useContext, useLayoutEffect, useMemo, useState } from "react"; import { getFeatureFlagValue } from "../../featureFlags"; -import { usePrevious } from "../../hooks/usePrevious"; -import { isNumber } from "../../typeGuards/isNumber"; -import { isString } from "../../typeGuards/isString"; import { FeatureFlag } from "../../types"; import { ConfigContext } from "../common/App/ConfigContext"; -import { NewPopover } from "../common/NewPopover"; -import { ChevronIcon } from "../common/icons/ChevronIcon"; -import { FilterIcon } from "../common/icons/FilterIcon"; -import { Direction } from "../common/icons/types"; import { AssetList } from "./AssetList"; import { AssetTypeList } from "./AssetTypeList"; -import { FilterMenu } from "./FilterMenu"; -import { FiltersMenu } from "./FiltersMenu"; +import { AssetsFilter } from "./AssetsFilter"; +import { AssetFilterQuery } from "./AssetsFilter/types"; +import { ServicesFilter } from "./ServicesFilter"; import { actions } from "./actions"; import * as s from "./styles"; -import { ServiceData } from "./types"; - -const REFRESH_INTERVAL = isNumber(window.assetsRefreshInterval) - ? window.assetsRefreshInterval - : 10 * 1000; // in milliseconds - -const preselectedServices = - Array.isArray(window.assetsSelectedServices) && - window.assetsSelectedServices.every(isString) - ? window.assetsSelectedServices - : []; export const Assets = () => { const [selectedAssetTypeId, setSelectedAssetTypeId] = useState( null ); - const [services, setServices] = useState(); - const [areServicesLoading, setAreServicesLoading] = useState(false); - const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); const [selectedServices, setSelectedServices] = useState(); - const previousSelectedServices = usePrevious(selectedServices); - const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false); + const [selectedFilters, setSelectedFilters] = useState(); const config = useContext(ConfigContext); - const previousEnvironment = usePrevious(config.environment); const isServiceFilterVisible = getFeatureFlagValue( config, FeatureFlag.IS_ASSETS_SERVICE_FILTER_VISIBLE ); + // const isComplexFilterVisible = getFeatureFlagValue( + // config, + // FeatureFlag.IS_ASSETS_COMPLEX_FILTER_ENABLED + // ); + + const isComplexFilterVisible = true; + useLayoutEffect(() => { window.sendMessageToDigma({ action: actions.INITIALIZE }); - window.sendMessageToDigma({ - action: actions.GET_SERVICES - }); - setAreServicesLoading(true); - - const handleServicesData = (data: unknown, timeStamp: number) => { - const serviceData = data as ServiceData; - setLastSetDataTimeStamp(timeStamp); - if (services === undefined) { - setSelectedServices((selectedServices) => { - const oldSelectedServices = Array.isArray(selectedServices) - ? selectedServices - : preselectedServices; - - const newSelectedServices = serviceData.services.filter((x) => - oldSelectedServices.includes(x) - ); - return newSelectedServices; - }); - } - setServices(serviceData.services); - setAreServicesLoading(false); - }; - - dispatcher.addActionListener(actions.SET_SERVICES, handleServicesData); - - return () => { - dispatcher.removeActionListener(actions.SET_SERVICES, handleServicesData); - }; }, []); - useEffect(() => { - if ( - isString(previousEnvironment) && - previousEnvironment !== config.environment - ) { - setServices(undefined); - window.sendMessageToDigma({ - action: actions.GET_SERVICES - }); - setAreServicesLoading(true); - } - }, [previousEnvironment, config.environment, services]); - - useEffect(() => { - const timerId = window.setTimeout(() => { - window.sendMessageToDigma({ - action: actions.GET_SERVICES - }); - }, REFRESH_INTERVAL); - - return () => { - window.clearTimeout(timerId); - }; - }, [lastSetDataTimeStamp]); - - useEffect(() => { - if ( - previousSelectedServices && - previousSelectedServices !== selectedServices - ) { - window.sendMessageToDigma({ - action: actions.SET_SELECTED_SERVICES, - payload: { - services: selectedServices - } - }); - } - }, [previousSelectedServices, selectedServices]); - - const handleServiceMenuClose = () => { - setIsServiceMenuOpen(false); - }; - - const handleServiceMenuItemClick = (service: string) => { - const oldSelectedServices = selectedServices || []; - const serviceIndex = oldSelectedServices.findIndex((x) => x === service); - - if (serviceIndex < 0) { - setSelectedServices([...oldSelectedServices, service]); - } else { - setSelectedServices([ - ...oldSelectedServices.slice(0, serviceIndex), - ...oldSelectedServices.slice(serviceIndex + 1) - ]); - } - }; - - const filterMenuItems = (services || []).map((x) => ({ - label: x, - value: x, - selected: (selectedServices || []).includes(x) - })); - const handleBackButtonClick = () => { setSelectedAssetTypeId(null); }; @@ -158,12 +44,21 @@ export const Assets = () => { setSelectedAssetTypeId(assetTypeId); }; + const handleServicesChange = (services: string[]) => { + setSelectedServices(services); + }; + + const handleApplyFilters = (filters: AssetFilterQuery) => { + setSelectedFilters(filters); + }; + const renderContent = useMemo((): JSX.Element => { if (!selectedAssetTypeId) { return ( ); } @@ -173,53 +68,24 @@ export const Assets = () => { onBackButtonClick={handleBackButtonClick} assetTypeId={selectedAssetTypeId} services={selectedServices || []} + filters={selectedFilters} /> ); - }, [selectedAssetTypeId, selectedServices]); + }, [selectedFilters, selectedAssetTypeId, selectedServices]); return ( Assets - - {isServiceFilterVisible && ( - - } - onOpenChange={setIsServiceMenuOpen} - isOpen={isServiceMenuOpen} - placement={"bottom-end"} - > - - - - Services - {selectedServices && - selectedServices.length > 0 && - !areServicesLoading ? ( - {selectedServices.length} - ) : ( - - All - - )} - - - - - - + {isComplexFilterVisible ? ( + + ) : ( + isServiceFilterVisible && ( + + ) )} {renderContent} diff --git a/src/components/Assets/styles.ts b/src/components/Assets/styles.ts index b41025276..36db73455 100644 --- a/src/components/Assets/styles.ts +++ b/src/components/Assets/styles.ts @@ -34,69 +34,3 @@ export const Header = styled.div` } }}; `; - -export const ServiceMenuButton = styled.button` - border: 1px solid #4e5157; - background: transparent; - border-radius: 4px; - padding: 8px; - display: flex; - gap: 10px; - align-items: center; -`; - -export const ServiceMenuButtonLabel = styled.span` - display: flex; - gap: 4px; - align-items: center; - font-size: 14px; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#818594"; - case "dark": - case "dark-jetbrains": - return "#b4b8bf"; - } - }}; -`; - -export const SelectedServiceNumberPlaceholder = styled.span` - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#494b57"; - case "dark": - case "dark-jetbrains": - return "#dfe1e5"; - } - }}; - user-select: none; -`; - -export const Number = styled.span` - min-width: 18px; - height: 18px; - flex-shrink: 0; - font-size: 14px; - line-height: 100%; - font-weight: 500; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: #fff; - background: #5053d4; -`; - -export const ServiceMenuChevronIconContainer = styled.span` - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return "#494b57"; - case "dark": - case "dark-jetbrains": - return "#dfe1e5"; - } - }}; -`; diff --git a/src/components/common/App/getTheme.ts b/src/components/common/App/getTheme.ts index 52107db89..d0aa66f8f 100644 --- a/src/components/common/App/getTheme.ts +++ b/src/components/common/App/getTheme.ts @@ -245,6 +245,9 @@ const darkThemeColors: ThemeColors = { }, select: { menu: { + text: { + primary: grayScale[0] + }, background: grayScale[800] } }, @@ -454,6 +457,9 @@ const lightThemeColors: ThemeColors = { panel: { background: grayScale[150] }, select: { menu: { + text: { + primary: grayScale[900] + }, background: grayScale[100] } }, diff --git a/src/components/common/Select/Select.stories.tsx b/src/components/common/Select/Select.stories.tsx index 235238202..0595388af 100644 --- a/src/components/common/Select/Select.stories.tsx +++ b/src/components/common/Select/Select.stories.tsx @@ -17,22 +17,13 @@ export default meta; type Story = StoryObj; -export const Default: Story = { +export const Multiselect: Story = { render: (args) => { // eslint-disable-next-line react-hooks/rules-of-hooks const [selectedItems, setSelectedItems] = useState([]); - const handleItemClick = (value: string) => { - const itemIndex = selectedItems.findIndex((x) => x === value); - - if (itemIndex < 0) { - setSelectedItems([...selectedItems, value]); - } else { - setSelectedItems([ - ...selectedItems.slice(0, itemIndex), - ...selectedItems.slice(itemIndex + 1) - ]); - } + const handleChange = (value: string | string[]) => { + setSelectedItems(Array.isArray(value) ? value : [value]); }; const items = args.items.map((x) => ({ @@ -44,8 +35,9 @@ export const Default: Story = { onChange(value, category.categoryName)} diff --git a/src/components/common/Select/Select.stories.tsx b/src/components/common/Select/Select.stories.tsx index b1d1dba02..0999486da 100644 --- a/src/components/common/Select/Select.stories.tsx +++ b/src/components/common/Select/Select.stories.tsx @@ -17,6 +17,52 @@ export default meta; type Story = StoryObj; +const mockedData = { + counts: { + filtered: 6, + total: 10 + }, + items: [ + { + label: "Very long long long long long long long long long long long name", + value: "item_1" + }, + { + label: "Item 2", + value: "item_2" + }, + { + label: "Item 3", + value: "item_3" + }, + { + label: "Item 4", + value: "item_4" + }, + { + label: "Item 5", + value: "item_5" + }, + { + label: "Item 6", + value: "item_6" + } + ] +}; + +export const Default: Story = { + args: { + items: mockedData.items.map((x, i) => ({ ...x, enabled: i !== 0 })) + } +}; + +export const Searchable: Story = { + args: { + searchable: true, + items: mockedData.items.map((x, i) => ({ ...x, enabled: i !== 0 })) + } +}; + export const Multiselect: Story = { render: (args) => { // eslint-disable-next-line react-hooks/rules-of-hooks @@ -43,36 +89,6 @@ export const Multiselect: Story = { ); }, args: { - counts: { - filtered: 6, - total: 10 - }, - items: [ - { - label: - "Very long long long long long long long long long long long name", - value: "item_1" - }, - { - label: "Item 2", - value: "item_2" - }, - { - label: "Item 3", - value: "item_3" - }, - { - label: "Item 4", - value: "item_4" - }, - { - label: "Item 5", - value: "item_5" - }, - { - label: "Item 6", - value: "item_6" - } - ] + ...mockedData } }; diff --git a/src/components/common/Select/index.tsx b/src/components/common/Select/index.tsx index 4b7170bc3..1a01bc140 100644 --- a/src/components/common/Select/index.tsx +++ b/src/components/common/Select/index.tsx @@ -26,6 +26,11 @@ export const Select = (props: SelectProps) => { return; } + if (!props.multiselect) { + props.onChange(item.value); + return; + } + const otherSelectedItems = props.items .filter((x) => x.selected && x.value !== item.value) .map((x) => x.value); @@ -34,7 +39,7 @@ export const Select = (props: SelectProps) => { ? otherSelectedItems : [...otherSelectedItems, item.value]; - props.onChange(props.multiselect ? newValue : item.value); + props.onChange(newValue); }; const selectedValues = props.items @@ -50,16 +55,18 @@ export const Select = (props: SelectProps) => { sameWidth={true} content={ - - - - - - + {props.searchable && ( + + + + + + + )} {filteredItems.length > 0 ? ( filteredItems.map((x) => ( @@ -69,12 +76,14 @@ export const Select = (props: SelectProps) => { $enabled={x.enabled} $selected={x.selected} > - undefined} - disabled={!x.enabled} - /> + {props.multiselect && ( + undefined} + disabled={!x.enabled} + /> + )} {x.label} diff --git a/src/featureFlags.ts b/src/featureFlags.ts index 105e8b1a3..071fb2cc8 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -8,7 +8,7 @@ const featureFlagMinBackendVersions: Record = { [FeatureFlag.IS_ASSETS_SERVICE_FILTER_VISIBLE]: "v0.2.174", [FeatureFlag.IS_ASSETS_OVERALL_IMPACT_HIDDEN]: "v0.2.181-alpha.1", [FeatureFlag.IS_INSIGHT_TICKET_LINKAGE_ENABLED]: "v0.2.200", - [FeatureFlag.IS_ASSETS_COMPLEX_FILTER_ENABLED]: "v0.2.212" + [FeatureFlag.IS_ASSETS_COMPLEX_FILTER_ENABLED]: "v0.2.213" }; export const getFeatureFlagValue = ( From e3928b9cb588dd0851b2829c80518721f595f097 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 30 Jan 2024 16:45:03 +0100 Subject: [PATCH 08/18] Add filter refresh, handle empty response --- .../AssetEntry/AssetEntry.stories.tsx | 2 +- .../Assets/AssetList/AssetEntry/index.tsx | 10 +++-- .../Assets/AssetList/AssetEntry/styles.ts | 7 ++- src/components/Assets/AssetsFilter/index.tsx | 44 ++++++++++++++++--- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/components/Assets/AssetList/AssetEntry/AssetEntry.stories.tsx b/src/components/Assets/AssetList/AssetEntry/AssetEntry.stories.tsx index 7c17934b6..eaf5adb9e 100644 --- a/src/components/Assets/AssetList/AssetEntry/AssetEntry.stories.tsx +++ b/src/components/Assets/AssetList/AssetEntry/AssetEntry.stories.tsx @@ -55,7 +55,7 @@ export const Default: Story = { } ], latestSpanTimestamp: "2023-02-20T14:36:03.480951Z", - instrumentationLibrary: "Global" + instrumentationLibrary: "Very long long long long long long name" } } }; diff --git a/src/components/Assets/AssetList/AssetEntry/index.tsx b/src/components/Assets/AssetList/AssetEntry/index.tsx index 7cb8fee08..d4bd6956b 100644 --- a/src/components/Assets/AssetList/AssetEntry/index.tsx +++ b/src/components/Assets/AssetList/AssetEntry/index.tsx @@ -199,9 +199,13 @@ export const AssetEntry = (props: AssetEntryProps) => { Scope - - {props.entry.instrumentationLibrary} - + + + + {props.entry.instrumentationLibrary} + + + )} diff --git a/src/components/Assets/AssetList/AssetEntry/styles.ts b/src/components/Assets/AssetList/AssetEntry/styles.ts index e52acbff1..7aaa73f18 100644 --- a/src/components/Assets/AssetList/AssetEntry/styles.ts +++ b/src/components/Assets/AssetList/AssetEntry/styles.ts @@ -134,7 +134,6 @@ export const ValueContainer = styled.div` return "#c6c6c6"; } }}; - width: fit-content; `; export const Suffix = styled.span` @@ -164,3 +163,9 @@ export const ImpactScoreIndicator = styled.div` height: 10px; background: hsl(14deg 66% ${({ $score }) => 100 - 50 * $score}%); `; + +export const ScopeName = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx index bb7910872..4948299f6 100644 --- a/src/components/Assets/AssetsFilter/index.tsx +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -1,5 +1,7 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { dispatcher } from "../../../dispatcher"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { isNumber } from "../../../typeGuards/isNumber"; import { InsightType } from "../../../types"; import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; import { NewButton } from "../../common/NewButton"; @@ -14,6 +16,10 @@ import { AssetsFiltersData } from "./types"; +const REFRESH_INTERVAL = isNumber(window.assetsRefreshInterval) + ? window.assetsRefreshInterval + : 10 * 1000; // in milliseconds + const renderFilterCategory = ( category: AssetFilterCategory, placeholder: string, @@ -46,13 +52,16 @@ const renderFilterCategory = ( }; export const AssetsFilter = (props: AssetsFilterProps) => { - const [data, setData] = useState(); + const [data, setData] = useState(); const [isOpen, setIsOpen] = useState(false); const [selectedServices, setSelectedServices] = useState([]); const [selectedEndpoints, setSelectedEndpoints] = useState([]); const [selectedConsumers, setSelectedConsumers] = useState([]); const [selectedInternals, setSelectedInternals] = useState([]); const [selectedInsights, setSelectedInsights] = useState([]); + const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); + const refreshTimerId = useRef(); const getData = ( services: string[], @@ -78,12 +87,13 @@ export const AssetsFilter = (props: AssetsFilterProps) => { selectedInsights ); - const handleData = (data: unknown) => { - const filtersData = data as AssetsFiltersData; + const handleData = (data: unknown, timeStamp: number) => { + const filtersData = data as AssetsFiltersData | null; setData(filtersData); + setLastSetDataTimeStamp(timeStamp); setSelectedServices( - filtersData.categories + filtersData?.categories .find((x) => x.categoryName === "Services") ?.entries?.filter((x) => x.selected) .map((x) => x.name) || [] @@ -114,7 +124,7 @@ export const AssetsFilter = (props: AssetsFilterProps) => { .map((x) => x.name) || []; setSelectedInternals(selectedInternals); - const selectedInsights = (filtersData.categories + const selectedInsights = (filtersData?.categories .find((x) => x.categoryName === "Insights") ?.entries?.filter((x) => x.selected) .map((x) => x.name) || []) as InsightType[]; @@ -140,6 +150,7 @@ export const AssetsFilter = (props: AssetsFilterProps) => { actions.SET_ASSET_FILTERS_DATA, handleData ); + window.clearTimeout(refreshTimerId.current); }; }, []); @@ -149,6 +160,27 @@ export const AssetsFilter = (props: AssetsFilterProps) => { } }, [props.data]); + useEffect(() => { + if (previousLastSetDataTimeStamp !== lastSetDataTimeStamp) { + window.clearTimeout(refreshTimerId.current); + refreshTimerId.current = window.setTimeout(() => { + getData( + selectedServices, + [...selectedEndpoints, ...selectedConsumers, ...selectedInternals], + selectedInsights + ); + }, REFRESH_INTERVAL); + } + }, [ + lastSetDataTimeStamp, + previousLastSetDataTimeStamp, + selectedServices, + selectedEndpoints, + selectedConsumers, + selectedInternals, + selectedInsights + ]); + const handleClearFiltersButtonClick = () => { getData([], [], []); }; From 27ff64c09a5d93ca6a429c0a2362a9a30dff7e77 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 31 Jan 2024 01:43:46 +0100 Subject: [PATCH 09/18] Add API to persist data --- src/actions.ts | 5 +- src/components/Assets/AssetsFilter/index.tsx | 80 +++++++++++++++----- src/components/Assets/index.tsx | 5 +- src/hooks/usePersistence.ts | 65 ++++++++++++++++ src/typeGuards/isObject.ts | 4 +- src/utils/saveToPersistence.ts | 16 ++++ 6 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 src/hooks/usePersistence.ts create mode 100644 src/utils/saveToPersistence.ts diff --git a/src/actions.ts b/src/actions.ts index 306850000..20335d41f 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -28,5 +28,8 @@ export const actions = addPrefix(ACTION_PREFIX, { REGISTER: "REGISTER", SET_ENVIRONMENTS: "SET_ENVIRONMENTS", SET_SELECTED_CODE_SCOPE: "SET_SELECTED_CODE_SCOPE", - SET_IS_MICROMETER_PROJECT: "SET_IS_MICROMETER_PROJECT" + SET_IS_MICROMETER_PROJECT: "SET_IS_MICROMETER_PROJECT", + SAVE_TO_PERSISTENCE: "SAVE_TO_PERSISTENCE", + GET_FROM_PERSISTENCE: "GET_FROM_PERSISTENCE", + SET_FROM_PERSISTENCE: "SET_FROM_PERSISTENCE" }); diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx index 4948299f6..69e5c7b05 100644 --- a/src/components/Assets/AssetsFilter/index.tsx +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -1,7 +1,9 @@ import { useEffect, useRef, useState } from "react"; import { dispatcher } from "../../../dispatcher"; +import { usePersistence } from "../../../hooks/usePersistence"; import { usePrevious } from "../../../hooks/usePrevious"; import { isNumber } from "../../../typeGuards/isNumber"; +import { isUndefined } from "../../../typeGuards/isUndefined"; import { InsightType } from "../../../types"; import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; import { NewButton } from "../../common/NewButton"; @@ -12,6 +14,7 @@ import { actions } from "../actions"; import * as s from "./styles"; import { AssetFilterCategory, + AssetFilterQuery, AssetsFilterProps, AssetsFiltersData } from "./types"; @@ -54,6 +57,9 @@ const renderFilterCategory = ( export const AssetsFilter = (props: AssetsFilterProps) => { const [data, setData] = useState(); const [isOpen, setIsOpen] = useState(false); + const [persistedFilters, setPersistedFilters] = + usePersistence("assetsFilters", "project"); + const previousPersistedFilters = usePrevious(persistedFilters); const [selectedServices, setSelectedServices] = useState([]); const [selectedEndpoints, setSelectedEndpoints] = useState([]); const [selectedConsumers, setSelectedConsumers] = useState([]); @@ -81,12 +87,31 @@ export const AssetsFilter = (props: AssetsFilterProps) => { }; useEffect(() => { - getData( - selectedServices, - [...selectedEndpoints, ...selectedConsumers, ...selectedInternals], - selectedInsights - ); + if ( + isUndefined(previousPersistedFilters) && + previousPersistedFilters !== persistedFilters + ) { + getData( + persistedFilters?.services || selectedServices, + persistedFilters?.operations || [ + ...selectedEndpoints, + ...selectedConsumers, + ...selectedInternals + ], + (persistedFilters?.insights as InsightType[]) || selectedInsights + ); + } + }, [ + previousPersistedFilters, + persistedFilters, + selectedServices, + selectedEndpoints, + selectedConsumers, + selectedInternals, + selectedInsights + ]); + useEffect(() => { const handleData = (data: unknown, timeStamp: number) => { const filtersData = data as AssetsFiltersData | null; setData(filtersData); @@ -129,19 +154,22 @@ export const AssetsFilter = (props: AssetsFilterProps) => { ?.entries?.filter((x) => x.selected) .map((x) => x.name) || []) as InsightType[]; setSelectedInsights(selectedInsights); - }; - if (!props.filters) { - props.onApply({ - services: selectedServices, - operations: [ - ...selectedEndpoints, - ...selectedConsumers, - ...selectedInternals - ], - insights: selectedInsights - }); - } + if (!props.filters) { + const filtersQuery = { + services: selectedServices, + operations: [ + ...selectedEndpoints, + ...selectedConsumers, + ...selectedInternals + ], + insights: selectedInsights + }; + + setPersistedFilters(filtersQuery); + props.onApply(filtersQuery); + } + }; dispatcher.addActionListener(actions.SET_ASSET_FILTERS_DATA, handleData); @@ -152,7 +180,16 @@ export const AssetsFilter = (props: AssetsFilterProps) => { ); window.clearTimeout(refreshTimerId.current); }; - }, []); + }, [ + props.filters, + props.onApply, + selectedConsumers, + selectedEndpoints, + selectedInsights, + selectedInternals, + selectedServices, + setPersistedFilters + ]); useEffect(() => { if (props.data) { @@ -186,7 +223,7 @@ export const AssetsFilter = (props: AssetsFilterProps) => { }; const handleApplyButtonClick = () => { - props.onApply({ + const filtersQuery = { services: selectedServices, operations: [ ...selectedEndpoints, @@ -194,7 +231,10 @@ export const AssetsFilter = (props: AssetsFilterProps) => { ...selectedInternals ], insights: selectedInsights - }); + }; + + props.onApply(filtersQuery); + setPersistedFilters(filtersQuery); setIsOpen(false); }; diff --git a/src/components/Assets/index.tsx b/src/components/Assets/index.tsx index 6dc60c534..28d2edcc9 100644 --- a/src/components/Assets/index.tsx +++ b/src/components/Assets/index.tsx @@ -103,7 +103,10 @@ export const Assets = () => { )} {isComplexFilterVisible ? ( - + ) : ( isServiceFilterVisible && ( { + key: string; + value: T | null; + scope: PersistenceScope; + error: { message: string } | null; +} + +export const usePersistence = ( + key: string, + scope: PersistenceScope +): [T | null | undefined, (value: T | null) => void] => { + const [value, setValue] = useState(); + + const saveToPersistence = useCallback( + (value: T | null) => { + window.sendMessageToDigma({ + action: actions.SAVE_TO_PERSISTENCE, + payload: { + key, + value, + scope + } + }); + }, + [key, scope] + ); + + useEffect(() => { + window.sendMessageToDigma({ + action: actions.GET_FROM_PERSISTENCE, + payload: { + key, + scope + } + }); + + const handlePersistedData = (data: unknown) => { + if (isObject(data) && data.key === key) { + const persistenceData = data as unknown as SetFromPersistencePayload; + setValue(persistenceData.value); + } + }; + + dispatcher.addActionListener( + actions.SET_FROM_PERSISTENCE, + handlePersistedData + ); + + return () => { + dispatcher.removeActionListener( + actions.SET_FROM_PERSISTENCE, + handlePersistedData + ); + }; + }, []); + + return [value, saveToPersistence]; +}; diff --git a/src/typeGuards/isObject.ts b/src/typeGuards/isObject.ts index acdab276c..c8d899b47 100644 --- a/src/typeGuards/isObject.ts +++ b/src/typeGuards/isObject.ts @@ -1,4 +1,6 @@ import { isNull } from "./isNull"; -export const isObject = (x: unknown): x is Record => +export const isObject = ( + x: unknown +): x is Record => typeof x === "object" && !isNull(x); diff --git a/src/utils/saveToPersistence.ts b/src/utils/saveToPersistence.ts new file mode 100644 index 000000000..afd1902cc --- /dev/null +++ b/src/utils/saveToPersistence.ts @@ -0,0 +1,16 @@ +import { actions } from "../actions"; + +export const saveToPersistence = >( + key: string, + value: T, + scope: "application" | "project" +) => { + window.sendMessageToDigma({ + action: actions.SAVE_TO_PERSISTENCE, + payload: { + key, + value, + scope + } + }); +}; From 9f7274afb97f35f499ca471bba820d12e6322ef6 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 31 Jan 2024 01:45:11 +0100 Subject: [PATCH 10/18] Remove redundant util --- src/utils/saveToPersistence.ts | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/utils/saveToPersistence.ts diff --git a/src/utils/saveToPersistence.ts b/src/utils/saveToPersistence.ts deleted file mode 100644 index afd1902cc..000000000 --- a/src/utils/saveToPersistence.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { actions } from "../actions"; - -export const saveToPersistence = >( - key: string, - value: T, - scope: "application" | "project" -) => { - window.sendMessageToDigma({ - action: actions.SAVE_TO_PERSISTENCE, - payload: { - key, - value, - scope - } - }); -}; From 49702d2b4b6ec780659b1391a7f2a407bf3e8418 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 31 Jan 2024 13:20:35 +0100 Subject: [PATCH 11/18] Improve filters and Select logic --- src/components/Assets/AssetsFilter/index.tsx | 61 ++++++++++++------- src/components/Dashboard/index.tsx | 2 +- .../RunDigma/runDigmaWithGradleTasks.tsx | 2 +- .../Insights/BottleneckInsight/index.tsx | 4 +- .../EndpointNPlusOneInsight/index.tsx | 2 +- src/components/Insights/JiraTicket/index.tsx | 2 +- .../Insights/NPlusOneInsight/index.tsx | 2 +- .../QueryOptimizationInsight/index.tsx | 2 +- .../Insights/SpanBottleneckInsight/index.tsx | 2 +- .../Insights/common/JiraButton/index.tsx | 4 +- src/components/common/NewPopover/types.ts | 2 +- .../common/Select/Select.stories.tsx | 7 +++ src/components/common/Select/index.tsx | 52 +++++++++++++--- src/components/common/Select/types.ts | 1 + 14 files changed, 102 insertions(+), 43 deletions(-) diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx index 4948299f6..d425dbe2f 100644 --- a/src/components/Assets/AssetsFilter/index.tsx +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -54,6 +54,7 @@ const renderFilterCategory = ( export const AssetsFilter = (props: AssetsFilterProps) => { const [data, setData] = useState(); const [isOpen, setIsOpen] = useState(false); + const previousIsOpen = usePrevious(isOpen); const [selectedServices, setSelectedServices] = useState([]); const [selectedEndpoints, setSelectedEndpoints] = useState([]); const [selectedConsumers, setSelectedConsumers] = useState([]); @@ -181,23 +182,33 @@ export const AssetsFilter = (props: AssetsFilterProps) => { selectedInsights ]); + useEffect(() => { + if (previousIsOpen && !isOpen) { + props.onApply({ + services: selectedServices, + operations: [ + ...selectedEndpoints, + ...selectedConsumers, + ...selectedInternals + ], + insights: selectedInsights + }); + } + }, [ + previousIsOpen, + isOpen, + props.onApply, + selectedConsumers, + selectedEndpoints, + selectedInsights, + selectedInternals, + selectedServices + ]); + const handleClearFiltersButtonClick = () => { getData([], [], []); }; - const handleApplyButtonClick = () => { - props.onApply({ - services: selectedServices, - operations: [ - ...selectedEndpoints, - ...selectedConsumers, - ...selectedInternals - ], - insights: selectedInsights - }); - setIsOpen(false); - }; - const handleSelectedItemsChange = ( value: string | string[], category?: string @@ -267,13 +278,13 @@ export const AssetsFilter = (props: AssetsFilterProps) => { entries: [] }; - const areFiltersDefault = [ - selectedServices, - selectedEndpoints, - selectedConsumers, - selectedInternals, - selectedInsights - ].every((x) => x.length === 0); + const selectedFilters = [ + ...selectedServices, + ...selectedEndpoints, + ...selectedConsumers, + ...selectedInternals, + ...selectedInsights + ]; return ( { - } @@ -332,7 +342,12 @@ export const AssetsFilter = (props: AssetsFilterProps) => { placement={"bottom-end"} >
- +
); diff --git a/src/components/Dashboard/index.tsx b/src/components/Dashboard/index.tsx index 750b18097..75ddc6206 100644 --- a/src/components/Dashboard/index.tsx +++ b/src/components/Dashboard/index.tsx @@ -133,7 +133,7 @@ export const Dashboard = () => { Open in browser - + )} diff --git a/src/components/Documentation/pages/RunDigma/runDigmaWithGradleTasks.tsx b/src/components/Documentation/pages/RunDigma/runDigmaWithGradleTasks.tsx index 783eb6291..563289d00 100644 --- a/src/components/Documentation/pages/RunDigma/runDigmaWithGradleTasks.tsx +++ b/src/components/Documentation/pages/RunDigma/runDigmaWithGradleTasks.tsx @@ -17,7 +17,7 @@ export const runDigmaWithGradleTasks: PageContent = { To do so, simply add the environment variable{" "} DIGMA_OBSERVABILITY=true - + That's it! Digma will now collect information about your task. diff --git a/src/components/Insights/BottleneckInsight/index.tsx b/src/components/Insights/BottleneckInsight/index.tsx index 912ba9c33..a94f015f3 100644 --- a/src/components/Insights/BottleneckInsight/index.tsx +++ b/src/components/Insights/BottleneckInsight/index.tsx @@ -69,11 +69,11 @@ export const BottleneckInsight = (props: BottleneckInsightProps) => { diff --git a/src/components/Insights/EndpointNPlusOneInsight/index.tsx b/src/components/Insights/EndpointNPlusOneInsight/index.tsx index 814321158..8e74712c9 100644 --- a/src/components/Insights/EndpointNPlusOneInsight/index.tsx +++ b/src/components/Insights/EndpointNPlusOneInsight/index.tsx @@ -104,7 +104,7 @@ export const EndpointNPlusOneInsight = ( onTicketInfoButtonClick={handleTicketInfoButtonClick} spanCodeObjectId={spanInfo.spanCodeObjectId} ticketLink={span.ticketLink} - buttonType="small" + buttonType={"small"} /> {config.isJaegerEnabled && ( diff --git a/src/components/Insights/JiraTicket/index.tsx b/src/components/Insights/JiraTicket/index.tsx index 66051dcb7..b16fee4b1 100644 --- a/src/components/Insights/JiraTicket/index.tsx +++ b/src/components/Insights/JiraTicket/index.tsx @@ -253,7 +253,7 @@ export const JiraTicket = (props: JiraTicketProps) => { )} {isLinkUnlinkInputVisible && ( { onTicketInfoButtonClick={handleCreateJiraTicketButtonClick} spanCodeObjectId={props.insight.spanInfo?.spanCodeObjectId} ticketLink={props.insight.ticketLink} - buttonType="large" + buttonType={"large"} /> ]} /> diff --git a/src/components/Insights/QueryOptimizationInsight/index.tsx b/src/components/Insights/QueryOptimizationInsight/index.tsx index 0f57428bd..ecef00493 100644 --- a/src/components/Insights/QueryOptimizationInsight/index.tsx +++ b/src/components/Insights/QueryOptimizationInsight/index.tsx @@ -107,7 +107,7 @@ export const QueryOptimizationInsight = ( onTicketInfoButtonClick={handleCreateJiraTicketButtonClick} spanCodeObjectId={props.insight.spanInfo?.spanCodeObjectId} ticketLink={props.insight.ticketLink} - buttonType="large" + buttonType={"large"} /> ]} /> diff --git a/src/components/Insights/SpanBottleneckInsight/index.tsx b/src/components/Insights/SpanBottleneckInsight/index.tsx index 516a3fc0c..fceadc3f0 100644 --- a/src/components/Insights/SpanBottleneckInsight/index.tsx +++ b/src/components/Insights/SpanBottleneckInsight/index.tsx @@ -65,7 +65,7 @@ export const SpanBottleneckInsight = (props: SpanBottleneckInsightProps) => { onTicketInfoButtonClick={handleTicketInfoButtonClick} spanCodeObjectId={spanCodeObjectId} ticketLink={span.ticketLink} - buttonType="small" + buttonType={"small"} /> diff --git a/src/components/Insights/common/JiraButton/index.tsx b/src/components/Insights/common/JiraButton/index.tsx index 013fe80ff..a655d72a1 100644 --- a/src/components/Insights/common/JiraButton/index.tsx +++ b/src/components/Insights/common/JiraButton/index.tsx @@ -25,7 +25,7 @@ export const JiraButton = (props: JiraButtonProps) => { return ( <> {!props.ticketLink && ( - + From e8b825de4fc95df37418f3226b7d7f5b7924cdcf Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 6 Feb 2024 02:36:05 +0100 Subject: [PATCH 17/18] Fix styles, add persistence --- src/components/Assets/AssetsFilter/index.tsx | 4 +- .../Insights/BottleneckInsight/index.tsx | 1 + .../EndpointNPlusOneInsight/index.tsx | 2 +- .../HighNumberOfQueriesInsight/index.tsx | 1 + src/components/Insights/InsightList/index.tsx | 30 ++++- src/components/Insights/InsightList/types.ts | 2 +- .../Insights/NPlusOneInsight/index.tsx | 2 +- .../QueryOptimizationInsight/index.tsx | 2 +- .../Insights/SpanBottleneckInsight/index.tsx | 2 +- .../Insights/common/JiraButton/index.tsx | 126 ++++++++++-------- .../Insights/common/JiraButton/styles.ts | 18 ++- .../Insights/common/JiraButton/types.ts | 2 +- .../EnvironmentTypePanel.stories.tsx | 16 ++- src/components/Tests/TestTicket/index.tsx | 4 +- src/components/common/IconTag/styles.ts | 2 +- src/components/common/Tooltip/index.tsx | 19 ++- src/components/common/Tooltip/types.ts | 3 +- src/hooks/usePersistence.ts | 1 + 18 files changed, 156 insertions(+), 81 deletions(-) diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx index 49eee102d..c8f419d44 100644 --- a/src/components/Assets/AssetsFilter/index.tsx +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -28,6 +28,8 @@ import { AssetsFiltersData } from "./types"; +const PERSISTENCE_KEY = "assetsFilters"; + const REFRESH_INTERVAL = isNumber(window.assetsRefreshInterval) ? window.assetsRefreshInterval : 10 * 1000; // in milliseconds @@ -68,7 +70,7 @@ export const AssetsFilter = (props: AssetsFilterProps) => { const [isOpen, setIsOpen] = useState(false); const previousIsOpen = usePrevious(isOpen); const [persistedFilters, setPersistedFilters] = - usePersistence("assetsFilters", "project"); + usePersistence(PERSISTENCE_KEY, "project"); const previousPersistedFilters = usePrevious(persistedFilters); const [selectedServices, setSelectedServices] = useState([]); const [selectedEndpoints, setSelectedEndpoints] = useState([]); diff --git a/src/components/Insights/BottleneckInsight/index.tsx b/src/components/Insights/BottleneckInsight/index.tsx index aa845cbcc..521b3f523 100644 --- a/src/components/Insights/BottleneckInsight/index.tsx +++ b/src/components/Insights/BottleneckInsight/index.tsx @@ -73,6 +73,7 @@ export const BottleneckInsight = (props: BottleneckInsightProps) => { spanCodeObjectId={props.insight.spanInfo?.spanCodeObjectId} ticketLink={props.insight.ticketLink} buttonType={"large"} + isHintEnabled={props.isJiraHintEnabled} /> diff --git a/src/components/Insights/EndpointNPlusOneInsight/index.tsx b/src/components/Insights/EndpointNPlusOneInsight/index.tsx index 4ab81d8f6..a17276c44 100644 --- a/src/components/Insights/EndpointNPlusOneInsight/index.tsx +++ b/src/components/Insights/EndpointNPlusOneInsight/index.tsx @@ -105,7 +105,7 @@ export const EndpointNPlusOneInsight = ( spanCodeObjectId={spanInfo.spanCodeObjectId} ticketLink={span.ticketLink} buttonType={"small"} - showHint={props.isJiraHintEnabled} + isHintEnabled={props.isJiraHintEnabled} /> {config.isJaegerEnabled && ( diff --git a/src/components/Insights/HighNumberOfQueriesInsight/index.tsx b/src/components/Insights/HighNumberOfQueriesInsight/index.tsx index bd7813e19..e9f4d6631 100644 --- a/src/components/Insights/HighNumberOfQueriesInsight/index.tsx +++ b/src/components/Insights/HighNumberOfQueriesInsight/index.tsx @@ -77,6 +77,7 @@ export const HighNumberOfQueriesInsight = ( spanCodeObjectId={insight.spanInfo?.spanCodeObjectId} ticketLink={insight.ticketLink} buttonType={"small"} + isHintEnabled={props.isJiraHintEnabled} /> {traceId && ( diff --git a/src/components/Insights/InsightList/index.tsx b/src/components/Insights/InsightList/index.tsx index 70c923285..7480e1629 100644 --- a/src/components/Insights/InsightList/index.tsx +++ b/src/components/Insights/InsightList/index.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { DefaultTheme, useTheme } from "styled-components"; -// import { usePersistence } from "../../../hooks/usePersistence"; +import { usePersistence } from "../../../hooks/usePersistence"; import { trackingEvents as globalTrackingEvents } from "../../../trackingEvents"; import { InsightType } from "../../../types"; import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; @@ -69,7 +69,7 @@ import { Trace } from "../types"; import * as s from "./styles"; -import { InsightListProps } from "./types"; +import { InsightListProps, isInsightJiraTicketHintShownPayload } from "./types"; export const getInsightTypeOrderPriority = (type: string): number => { const insightOrderPriorityMap: Record = { @@ -112,7 +112,8 @@ const getInsightToShowJiraHint = ( InsightType.SpanNPlusOne, InsightType.SpanEndpointBottleneck, InsightType.SlowestSpans, - InsightType.SpanQueryOptimization + InsightType.SpanQueryOptimization, + InsightType.EndpointHighNumberOfQueries ]; let insightIndex = -1; @@ -603,6 +604,7 @@ const renderInsightCard = ( onRecalculate={handleRecalculate} onRefresh={handleRefresh} onJiraTicketCreate={onJiraTicketCreate} + isJiraHintEnabled={isJiraHintEnabled} /> ); } @@ -634,13 +636,19 @@ const renderInsightCard = ( } }; +const IS_INSIGHT_JIRA_TICKET_HINT_SHOWN_PERSISTENCE_KEY = + "isInsightJiraTicketHintShown"; + export const InsightList = (props: InsightListProps) => { const [insightGroups, setInsightGroups] = useState([]); const [isAutofixing, setIsAutofixing] = useState(false); const theme = useTheme(); const insightGroupIconColor = getInsightGroupIconColor(theme); - // const [isJiraHintShown, setIsJiraHintShown] = - // usePersistence("isJiraHintShown", "application"); + const [isInsightJiraTicketHintShown, setIsInsightJiraTicketHintShown] = + usePersistence( + IS_INSIGHT_JIRA_TICKET_HINT_SHOWN_PERSISTENCE_KEY, + "application" + ); const insightWithJiraHint = getInsightToShowJiraHint(insightGroups); @@ -683,6 +691,14 @@ export const InsightList = (props: InsightListProps) => { setIsAutofixing(true); }; + const handleShowJiraTicket = ( + insight: GenericCodeObjectInsight, + spanCodeObjectId?: string + ) => { + props.onJiraTicketCreate(insight, spanCodeObjectId); + setIsInsightJiraTicketHintShown({ value: true }); + }; + return ( {insightGroups.map((x, i) => ( @@ -700,11 +716,13 @@ export const InsightList = (props: InsightListProps) => { {x.insights.length > 0 ? ( x.insights.map((insight, j) => { const isJiraHintEnabled = + !isInsightJiraTicketHintShown?.value && i === insightWithJiraHint?.groupIndex && j === insightWithJiraHint?.insightIndex; + return renderInsightCard( insight, - props.onJiraTicketCreate, + handleShowJiraTicket, isJiraHintEnabled ); }) diff --git a/src/components/Insights/InsightList/types.ts b/src/components/Insights/InsightList/types.ts index 96f90057b..5c53dd29e 100644 --- a/src/components/Insights/InsightList/types.ts +++ b/src/components/Insights/InsightList/types.ts @@ -15,6 +15,6 @@ export interface InsightListProps { ) => void; } -export interface IsJiraShownPayload { +export interface isInsightJiraTicketHintShownPayload { value: boolean; } diff --git a/src/components/Insights/NPlusOneInsight/index.tsx b/src/components/Insights/NPlusOneInsight/index.tsx index 0ae4d69b7..ae5b1ce28 100644 --- a/src/components/Insights/NPlusOneInsight/index.tsx +++ b/src/components/Insights/NPlusOneInsight/index.tsx @@ -138,7 +138,7 @@ export const NPlusOneInsight = (props: NPlusOneInsightProps) => { spanCodeObjectId={props.insight.spanInfo?.spanCodeObjectId} ticketLink={props.insight.ticketLink} buttonType={"large"} - showHint={props.isJiraHintEnabled} + isHintEnabled={props.isJiraHintEnabled} /> ]} /> diff --git a/src/components/Insights/QueryOptimizationInsight/index.tsx b/src/components/Insights/QueryOptimizationInsight/index.tsx index 5bb68cfd6..f8542b2f0 100644 --- a/src/components/Insights/QueryOptimizationInsight/index.tsx +++ b/src/components/Insights/QueryOptimizationInsight/index.tsx @@ -108,7 +108,7 @@ export const QueryOptimizationInsight = ( spanCodeObjectId={props.insight.spanInfo?.spanCodeObjectId} ticketLink={props.insight.ticketLink} buttonType={"large"} - showHint={props.isJiraHintEnabled} + isHintEnabled={props.isJiraHintEnabled} /> ]} /> diff --git a/src/components/Insights/SpanBottleneckInsight/index.tsx b/src/components/Insights/SpanBottleneckInsight/index.tsx index a3b771887..0719f7967 100644 --- a/src/components/Insights/SpanBottleneckInsight/index.tsx +++ b/src/components/Insights/SpanBottleneckInsight/index.tsx @@ -66,7 +66,7 @@ export const SpanBottleneckInsight = (props: SpanBottleneckInsightProps) => { spanCodeObjectId={spanCodeObjectId} ticketLink={span.ticketLink} buttonType={"small"} - showHint={props.isJiraHintEnabled} + isHintEnabled={props.isJiraHintEnabled} /> diff --git a/src/components/Insights/common/JiraButton/index.tsx b/src/components/Insights/common/JiraButton/index.tsx index 10f77988e..c97bb6845 100644 --- a/src/components/Insights/common/JiraButton/index.tsx +++ b/src/components/Insights/common/JiraButton/index.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; +import { useTheme } from "styled-components"; import { openURLInDefaultBrowser } from "../../../../utils/openURLInDefaultBrowser"; import { Button } from "../../../common/Button"; import { Menu } from "../../../common/Menu"; -import { NewButton } from "../../../common/NewButton"; import { NewPopover } from "../../../common/NewPopover"; import { Tooltip } from "../../../common/Tooltip"; import { PencilIcon } from "../../../common/icons/12px/PencilIcon"; @@ -15,29 +15,26 @@ import { JiraButtonProps } from "./types"; export const JiraButton = (props: JiraButtonProps) => { const [isJiraPopoverOpen, setIsJiraPopoverOpen] = useState(false); + const theme = useTheme(); const handleJiraButtonClick = () => { setIsJiraPopoverOpen(!isJiraPopoverOpen); }; + const handleViewButtonClick = () => { + props.ticketLink && openURLInDefaultBrowser(props.ticketLink); + }; + + const openTicketInfo = () => { + props.onTicketInfoButtonClick(props.spanCodeObjectId); + }; + const menuWidth = props.buttonType == "large" ? "119px" : "70px"; const buttonText = props.buttonType == "large" ? "Ticket Info" : ""; - return ( - <> - {!props.ticketLink && ( - - - - )} - {props.ticketLink && ( + const renderButton = () => ( +
+ {props.ticketLink ? ( { icon: { component: OpenLinkIcon }, label: "View", value: props.ticketLink, - onClick: () => - props.ticketLink && - openURLInDefaultBrowser(props.ticketLink) + onClick: handleViewButtonClick }, { icon: { component: PencilIcon }, label: "Edit", value: props.spanCodeObjectId ?? "", - onClick: () => - props.onTicketInfoButtonClick(props.spanCodeObjectId) + onClick: openTicketInfo } ]} onSelect={handleJiraButtonClick} @@ -66,42 +60,62 @@ export const JiraButton = (props: JiraButtonProps) => { onOpenChange={handleJiraButtonClick} placement={"bottom-start"} > -
- - - - Get Ticket Info - - You can now easily create a ticket using information from - Digma - - - - - } - permanent={props.showHint} - > - - } - > - {buttonText} - - -
+ + } + > + {buttonText} +
+ ) : ( + + + )} - +
+ ); + + return ( + + + + + + Get Ticket Info + + + You can now easily create a ticket using information from Digma + + + + } + isOpen={props.isHintEnabled} + > + {renderButton()} + ); }; diff --git a/src/components/Insights/common/JiraButton/styles.ts b/src/components/Insights/common/JiraButton/styles.ts index 42759db39..607d142ea 100644 --- a/src/components/Insights/common/JiraButton/styles.ts +++ b/src/components/Insights/common/JiraButton/styles.ts @@ -1,5 +1,6 @@ import styled from "styled-components"; import { Button } from "../../../common/Button"; +import { NewButton } from "../../../common/NewButton"; export const StyledButton = styled(Button)` display: flex; @@ -11,8 +12,10 @@ export const HintContainer = styled.div` display: flex; flex-direction: column; gap: 4px; - align-items: flex-end; - padding: 8px; + padding: 4px; + width: 235px; + color: ${({ theme }) => theme.colors.text.subtext}; + word-break: keep-all; `; export const HintHeader = styled.div` @@ -21,5 +24,14 @@ export const HintHeader = styled.div` align-items: center; font-weight: 500; color: ${({ theme }) => theme.colors.text.base}; - margin-bottom: 8px; +`; + +export const HintIconContainer = styled.div` + display: flex; + color: ${({ theme }) => theme.colors.icon.primary}; +`; + +export const TryNowButton = styled(NewButton)` + margin-top: 8px; + align-self: flex-end; `; diff --git a/src/components/Insights/common/JiraButton/types.ts b/src/components/Insights/common/JiraButton/types.ts index 2be5553ff..0131366ac 100644 --- a/src/components/Insights/common/JiraButton/types.ts +++ b/src/components/Insights/common/JiraButton/types.ts @@ -3,5 +3,5 @@ export interface JiraButtonProps { ticketLink?: string | null; spanCodeObjectId?: string; buttonType: "small" | "large"; - showHint?: boolean; + isHintEnabled?: boolean; } diff --git a/src/components/RecentActivity/EnvironmentTypePanel/EnvironmentTypePanel.stories.tsx b/src/components/RecentActivity/EnvironmentTypePanel/EnvironmentTypePanel.stories.tsx index f114b8ebb..2034b74a3 100644 --- a/src/components/RecentActivity/EnvironmentTypePanel/EnvironmentTypePanel.stories.tsx +++ b/src/components/RecentActivity/EnvironmentTypePanel/EnvironmentTypePanel.stories.tsx @@ -16,4 +16,18 @@ 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 = {}; +export const Default: Story = { + args: { + environment: { + originalName: "environmentName", + hasRecentActivity: false, + name: "environmentName", + isPending: true, + additionToConfigResult: null, + type: null, + token: null, + serverApiUrl: null, + isOrgDigmaSetupFinished: false + } + } +}; diff --git a/src/components/Tests/TestTicket/index.tsx b/src/components/Tests/TestTicket/index.tsx index a088a025f..bc96156e1 100644 --- a/src/components/Tests/TestTicket/index.tsx +++ b/src/components/Tests/TestTicket/index.tsx @@ -35,8 +35,8 @@ export const TestTicket = (props: TestTicketProps) => { : "" }`} , -
Last run at: ${new Date(runAt).toString()}
, -
Duration: ${getDurationString(duration)}
, +
Last run at: {new Date(runAt).toString()}
, +
Duration: {getDurationString(duration)}
, <> {relatedSpans.length > 0 && (
{`Related spans:\n${relatedSpans}`}
diff --git a/src/components/common/IconTag/styles.ts b/src/components/common/IconTag/styles.ts index bd0f98675..3128268ca 100644 --- a/src/components/common/IconTag/styles.ts +++ b/src/components/common/IconTag/styles.ts @@ -1,7 +1,7 @@ import styled from "styled-components"; import { ContainerProps, IconTagSize } from "./types"; -const getDimensions = (size: IconTagSize) => (size === "large" ? 28 : 20); //in pixels +const getDimensions = (size: IconTagSize) => (size === "large" ? 28 : 20); // in pixels export const Container = styled.div` display: flex; diff --git a/src/components/common/Tooltip/index.tsx b/src/components/common/Tooltip/index.tsx index e40b446d7..6672c63ca 100644 --- a/src/components/common/Tooltip/index.tsx +++ b/src/components/common/Tooltip/index.tsx @@ -3,6 +3,7 @@ import { FloatingPortal, Placement, arrow, + autoUpdate, flip, offset, shift, @@ -12,6 +13,8 @@ import { } from "@floating-ui/react"; import { Children, cloneElement, useRef, useState } from "react"; import { useTheme } from "styled-components"; +import { isBoolean } from "../../../typeGuards/isBoolean"; +import { isString } from "../../../typeGuards/isString"; import * as s from "./styles"; import { TooltipProps } from "./types"; @@ -51,7 +54,9 @@ const getArrowStyles = (placement: Placement) => { }; export const Tooltip = (props: TooltipProps) => { - const [isOpen, setIsOpen] = useState(props.permanent || false); + const [isOpen, setIsOpen] = useState( + isBoolean(props.isOpen) ? props.isOpen : false + ); const arrowRef = useRef(null); const theme = useTheme(); @@ -59,6 +64,7 @@ export const Tooltip = (props: TooltipProps) => { const placement = props.placement || "top"; const { refs, floatingStyles, context } = useFloating({ + whileElementsMounted: autoUpdate, placement, open: isOpen, onOpenChange: setIsOpen, @@ -74,7 +80,7 @@ export const Tooltip = (props: TooltipProps) => { const hover = useHover(context, { delay: { open: 1000, close: 0 }, - enabled: !props.permanent + enabled: !isBoolean(props.isOpen) }); const { getReferenceProps, getFloatingProps } = useInteractions([hover]); @@ -92,14 +98,19 @@ export const Tooltip = (props: TooltipProps) => { ( scope } }); + setValue(value); // TODO: handle error }, [key, scope] ); From 039b6c5a2caedefb5c3c08ac82974519ec2cf056 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 6 Feb 2024 11:05:27 +0100 Subject: [PATCH 18/18] Fix tooltip state --- src/components/Insights/InsightList/index.tsx | 2 ++ src/components/common/Tooltip/index.tsx | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Insights/InsightList/index.tsx b/src/components/Insights/InsightList/index.tsx index 7480e1629..e599f64b5 100644 --- a/src/components/Insights/InsightList/index.tsx +++ b/src/components/Insights/InsightList/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { DefaultTheme, useTheme } from "styled-components"; import { usePersistence } from "../../../hooks/usePersistence"; import { trackingEvents as globalTrackingEvents } from "../../../trackingEvents"; +import { isUndefined } from "../../../typeGuards/isUndefined"; import { InsightType } from "../../../types"; import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; @@ -716,6 +717,7 @@ export const InsightList = (props: InsightListProps) => { {x.insights.length > 0 ? ( x.insights.map((insight, j) => { const isJiraHintEnabled = + !isUndefined(isInsightJiraTicketHintShown) && !isInsightJiraTicketHintShown?.value && i === insightWithJiraHint?.groupIndex && j === insightWithJiraHint?.insightIndex; diff --git a/src/components/common/Tooltip/index.tsx b/src/components/common/Tooltip/index.tsx index 6672c63ca..b13052c88 100644 --- a/src/components/common/Tooltip/index.tsx +++ b/src/components/common/Tooltip/index.tsx @@ -54,9 +54,7 @@ const getArrowStyles = (placement: Placement) => { }; export const Tooltip = (props: TooltipProps) => { - const [isOpen, setIsOpen] = useState( - isBoolean(props.isOpen) ? props.isOpen : false - ); + const [isOpen, setIsOpen] = useState(false); const arrowRef = useRef(null); const theme = useTheme(); @@ -66,8 +64,8 @@ export const Tooltip = (props: TooltipProps) => { const { refs, floatingStyles, context } = useFloating({ whileElementsMounted: autoUpdate, placement, - open: isOpen, - onOpenChange: setIsOpen, + open: isBoolean(props.isOpen) ? props.isOpen : isOpen, + onOpenChange: isBoolean(props.isOpen) ? undefined : setIsOpen, middleware: [ offset(ARROW_HEIGHT + GAP), flip(), @@ -93,7 +91,7 @@ export const Tooltip = (props: TooltipProps) => { ...getReferenceProps() }) )} - {isOpen && ( + {(isBoolean(props.isOpen) ? props.isOpen : isOpen) && (