diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx index d8bca8b8f..b059da61c 100644 --- a/src/components/Assets/AssetsFilter/index.tsx +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -9,6 +9,7 @@ import { InsightType } from "../../../types"; import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; import { ConfigContext } from "../../common/App/ConfigContext"; +import { FilterButton } from "../../common/FilterButton"; import { NewButton } from "../../common/NewButton"; import { NewPopover } from "../../common/NewPopover"; import { Select } from "../../common/Select"; @@ -18,7 +19,6 @@ import { SparkleIcon } from "../../common/icons/SparkleIcon"; import { IconProps } from "../../common/icons/types"; import { actions } from "../actions"; import { trackingEvents } from "../tracking"; -import { FilterButton } from "./FilterButton"; import * as s from "./styles"; import { AssetFilterCategory, diff --git a/src/components/Assets/ServicesFilter/index.tsx b/src/components/Assets/ServicesFilter/index.tsx index 5cdf64c5a..707a09f0b 100644 --- a/src/components/Assets/ServicesFilter/index.tsx +++ b/src/components/Assets/ServicesFilter/index.tsx @@ -5,8 +5,8 @@ import { isEnvironment } from "../../../typeGuards/isEnvironment"; import { isNumber } from "../../../typeGuards/isNumber"; import { isString } from "../../../typeGuards/isString"; import { ConfigContext } from "../../common/App/ConfigContext"; +import { FilterMenu } from "../../common/FilterMenu"; import { NewPopover } from "../../common/NewPopover"; -import { FilterMenu } from "../FilterMenu"; import { actions } from "../actions"; import { ServiceData } from "../types"; import { FilterButton } from "./FilterButton"; diff --git a/src/components/Insights/Insights.stories.tsx b/src/components/Insights/Insights.stories.tsx index d0f5b9a3c..d8583fd95 100644 --- a/src/components/Insights/Insights.stories.tsx +++ b/src/components/Insights/Insights.stories.tsx @@ -31,25 +31,26 @@ type Story = StoryObj; export const Default: Story = { args: { data: { - spans: [ - { - spanCodeObjectId: "empty_span1_id", - spanDisplayName: "empty_span1" - }, - { - spanCodeObjectId: "empty_span2_id", - spanDisplayName: "empty_span2" - } - ], - assetId: "string", - serviceName: "string", - environment: "string", + totalCount: 100, + // spans: [ + // { + // spanCodeObjectId: "empty_span1_id", + // spanDisplayName: "empty_span1" + // }, + // { + // spanCodeObjectId: "empty_span2_id", + // spanDisplayName: "empty_span2" + // } + // ], + // assetId: "string", + // serviceName: "string", + // environment: "string", viewMode: ViewMode.INSIGHTS, - hasMissingDependency: false, + // hasMissingDependency: false, insightsStatus: InsightsStatus.DEFAULT, - methods: [], - canInstrumentMethod: false, - needsObservabilityFix: false, + // methods: [], + // canInstrumentMethod: false, + // needsObservabilityFix: false, insights: [ { firstDetected: "2023-12-05T17:25:47.010Z", @@ -393,57 +394,57 @@ export const Default: Story = { category: InsightCategory.Performance, specifity: 3, importance: 2, - spans: [ - { - spanInfo: { - name: "Validating account funds", - displayName: "Validating account funds", - instrumentationLibrary: "MoneyTransferDomainService", - spanCodeObjectId: - "span:MoneyTransferDomainService$_$Validating account funds", - methodCodeObjectId: null, - kind: "Internal", - codeObjectId: null - }, - probabilityOfBeingBottleneck: 0.2564102564102564, - avgDurationWhenBeingBottleneck: { - value: 2, - unit: "sec", - raw: 2003535300 - }, - criticality: 0, - p50: { - fraction: 0, - maxDuration: { - value: 0, - unit: "ns", - raw: 0 - } - }, - p95: { - fraction: 0, - maxDuration: { - value: 0, - unit: "ns", - raw: 0 - } - }, - p99: { - fraction: 0, - maxDuration: { - value: 0, - unit: "ns", - raw: 0 - } - } - } - ], + // spans: [ + // { + // spanInfo: { + // name: "Validating account funds", + // displayName: "Validating account funds", + // instrumentationLibrary: "MoneyTransferDomainService", + // spanCodeObjectId: + // "span:MoneyTransferDomainService$_$Validating account funds", + // methodCodeObjectId: null, + // kind: "Internal", + // codeObjectId: null + // }, + // probabilityOfBeingBottleneck: 0.2564102564102564, + // avgDurationWhenBeingBottleneck: { + // value: 2, + // unit: "sec", + // raw: 2003535300 + // }, + // criticality: 0, + // p50: { + // fraction: 0, + // maxDuration: { + // value: 0, + // unit: "ns", + // raw: 0 + // } + // }, + // p95: { + // fraction: 0, + // maxDuration: { + // value: 0, + // unit: "ns", + // raw: 0 + // } + // }, + // p99: { + // fraction: 0, + // maxDuration: { + // value: 0, + // unit: "ns", + // raw: 0 + // } + // } + // } + // ], scope: InsightScope.EntrySpan, - endpointSpan: "HTTP POST Transfer/TransferFunds", - spanCodeObjectId: - "span:OpenTelemetry.Instrumentation.AspNetCore$_$HTTP POST Transfer/TransferFunds", - route: "epHTTP:HTTP POST Transfer/TransferFunds", - serviceName: "Sample.MoneyTransfer.API", + // endpointSpan: "HTTP POST Transfer/TransferFunds", + // spanCodeObjectId: + // "span:OpenTelemetry.Instrumentation.AspNetCore$_$HTTP POST Transfer/TransferFunds", + // route: "epHTTP:HTTP POST Transfer/TransferFunds", + // serviceName: "Sample.MoneyTransfer.API", spanInfo: { name: "HTTP POST Transfer/TransferFunds", displayName: "HTTP POST Transfer/TransferFunds", @@ -718,17 +719,19 @@ export const Default: Story = { export const NoInsights: Story = { args: { data: { - spans: [], - assetId: "string", - serviceName: "string", - environment: "string", + // spans: [], + // assetId: "string", + // serviceName: "string", + // environment: "string", + // canInstrumentMethod: false, + // needsObservabilityFix: false, + // hasMissingDependency: false, + // methods: [], + viewMode: ViewMode.INSIGHTS, - hasMissingDependency: false, insightsStatus: InsightsStatus.NO_INSIGHTS, - methods: [], insights: [], - canInstrumentMethod: false, - needsObservabilityFix: false + totalCount: 0 } } }; @@ -736,17 +739,19 @@ export const NoInsights: Story = { export const NoDataYet: Story = { args: { data: { - spans: [], - assetId: "string", - serviceName: "string", - environment: "string", + // spans: [], + // assetId: "string", + // serviceName: "string", + // environment: "string", + // canInstrumentMethod: false, + // needsObservabilityFix: false, + // hasMissingDependency: false, + // methods: [], + viewMode: ViewMode.INSIGHTS, - hasMissingDependency: false, insightsStatus: InsightsStatus.NO_SPANS_DATA, - methods: [], insights: [], - canInstrumentMethod: false, - needsObservabilityFix: false + totalCount: 0 } } }; @@ -754,17 +759,19 @@ export const NoDataYet: Story = { export const ProcessingInsights: Story = { args: { data: { - spans: [], - assetId: "string", - serviceName: "string", - environment: "string", + // spans: [], + // assetId: "string", + // serviceName: "string", + // environment: "string", + // hasMissingDependency: false, + // methods: [], + // canInstrumentMethod: false, + // needsObservabilityFix: false, + viewMode: ViewMode.INSIGHTS, - hasMissingDependency: false, insightsStatus: InsightsStatus.INSIGHT_PENDING, - methods: [], insights: [], - canInstrumentMethod: false, - needsObservabilityFix: false + totalCount: 0 } } }; @@ -772,17 +779,19 @@ export const ProcessingInsights: Story = { export const NoObservability: Story = { args: { data: { - spans: [], - assetId: "string", - serviceName: "string", - environment: "string", + // spans: [], + // assetId: "string", + // serviceName: "string", + // environment: "string", + // hasMissingDependency: false, + // methods: [], + // canInstrumentMethod: true, + // needsObservabilityFix: true + viewMode: ViewMode.INSIGHTS, - hasMissingDependency: false, insightsStatus: InsightsStatus.NO_OBSERVABILITY, - methods: [], insights: [], - canInstrumentMethod: true, - needsObservabilityFix: true + totalCount: 0 } } }; @@ -845,17 +854,19 @@ const errorsInsight: CodeObjectErrorsInsight = { export const NoObservabilityWithInsights: Story = { args: { data: { - spans: [], - assetId: "string", - serviceName: "string", - environment: "string", viewMode: ViewMode.INSIGHTS, - hasMissingDependency: false, insightsStatus: InsightsStatus.DEFAULT, - methods: [], insights: [errorsInsight], - canInstrumentMethod: true, - needsObservabilityFix: true + totalCount: 0 + + // spans: [], + // assetId: "string", + // serviceName: "string", + // environment: "string", + // hasMissingDependency: false, + // methods: [], + // canInstrumentMethod: true, + // needsObservabilityFix: true } } }; @@ -863,17 +874,18 @@ export const NoObservabilityWithInsights: Story = { export const HasMissingDependency: Story = { args: { data: { - spans: [], - assetId: "string", - serviceName: "string", - environment: "string", + // spans: [], + // assetId: "string", + // serviceName: "string", + // environment: "string", + // hasMissingDependency: true, + // methods: [], + // canInstrumentMethod: true, + // needsObservabilityFix: true viewMode: ViewMode.INSIGHTS, - hasMissingDependency: true, + totalCount: 0, insightsStatus: InsightsStatus.NO_OBSERVABILITY, - methods: [], - insights: [], - canInstrumentMethod: true, - needsObservabilityFix: true + insights: [] } } }; @@ -881,17 +893,19 @@ export const HasMissingDependency: Story = { export const HasMissingDependencyWithInsights: Story = { args: { data: { - spans: [], - assetId: "string", - serviceName: "string", - environment: "string", + // spans: [], + // assetId: "string", + // serviceName: "string", + // environment: "string", + // hasMissingDependency: true, + // methods: [], + // canInstrumentMethod: true, + // needsObservabilityFix: true, + viewMode: ViewMode.INSIGHTS, - hasMissingDependency: true, insightsStatus: InsightsStatus.DEFAULT, - methods: [], insights: [errorsInsight], - canInstrumentMethod: true, - needsObservabilityFix: true + totalCount: 0 } } }; @@ -899,16 +913,17 @@ export const HasMissingDependencyWithInsights: Story = { export const Startup: Story = { args: { data: { - spans: [], - serviceName: "string", - environment: "string", viewMode: ViewMode.INSIGHTS, - hasMissingDependency: false, - insightsStatus: InsightsStatus.STARTUP, - methods: [], insights: [], - canInstrumentMethod: false, - needsObservabilityFix: false + insightsStatus: InsightsStatus.STARTUP, + totalCount: 0 + // spans: [], + // serviceName: "string", + // environment: "string", + // methods: [], + // canInstrumentMethod: false, + // needsObservabilityFix: false + // hasMissingDependency: false, } } }; @@ -916,29 +931,30 @@ export const Startup: Story = { export const Preview: Story = { args: { data: { - spans: [], - serviceName: "string", - environment: "string", viewMode: ViewMode.PREVIEW, - hasMissingDependency: false, + totalCount: 0, insightsStatus: InsightsStatus.DEFAULT, - canInstrumentMethod: false, - methods: [ - { - id: "method1", - name: "method1" - }, - { - id: "method2", - name: "method2" - }, - { - id: "method3", - name: "method3" - } - ], - insights: [], - needsObservabilityFix: false + // spans: [], + // serviceName: "string", + // environment: "string", + // hasMissingDependency: false, + // canInstrumentMethod: false, + // methods: [ + // { + // id: "method1", + // name: "method1" + // }, + // { + // id: "method2", + // name: "method2" + // }, + // { + // id: "method3", + // name: "method3" + // } + // ], + insights: [] + // needsObservabilityFix: false } } }; diff --git a/src/components/Insights/InsightsCatalog/index.tsx b/src/components/Insights/InsightsCatalog/index.tsx new file mode 100644 index 000000000..9b1322daa --- /dev/null +++ b/src/components/Insights/InsightsCatalog/index.tsx @@ -0,0 +1,111 @@ +import { useEffect, useState } from "react"; +import { usePrevious } from "../../../hooks/usePrevious"; + +import { isNumber } from "../../../typeGuards/isNumber"; +import { isString } from "../../../typeGuards/isString"; +import { Pagination } from "../../common/Pagination"; +import { SearchInput } from "../../common/SearchInput"; +import { SortingSelector } from "../../common/SortingSelector"; +import { SORTING_ORDER, Sorting } from "../../common/SortingSelector/types"; +import { InsightsPage } from "../InsightsPage"; +import * as s from "./styles"; +import { InsightsCatalogProps, SORTING_CRITERION } from "./types"; + +const PAGE_SIZE = 10; +export const InsightsCatalog = (props: InsightsCatalogProps) => { + const { insights, onJiraTicketCreate, defaultQuery, totalCount } = props; + const [page, setPage] = useState(0); + const previousPage = usePrevious(page); + const [searchInputValue, setSearchInputValue] = useState( + defaultQuery.searchQuery + ); + const [sorting, setSorting] = useState(defaultQuery.sorting); + const previousSorting = usePrevious(sorting); + const previousSearchQuery = usePrevious(searchInputValue); + const pageStartItemNumber = page * PAGE_SIZE + 1; + const pageEndItemNumber = Math.min( + pageStartItemNumber + PAGE_SIZE - 1, + totalCount + ); + + useEffect(() => { + props.onQueryChange({ + page, + sorting, + searchQuery: searchInputValue + }); + }, []); + + useEffect(() => { + if ( + (isNumber(previousPage) && previousPage !== page) || + (previousSorting && previousSorting !== sorting) || + (isString(previousSearchQuery) && + previousSearchQuery !== searchInputValue) + ) { + props.onQueryChange({ + page, + sorting, + searchQuery: searchInputValue + }); + } + }, [ + previousSorting, + sorting, + previousPage, + page, + searchInputValue, + previousSearchQuery + ]); + + return ( + <> + + { + setSearchInputValue(val); + }} + default={defaultQuery.searchQuery || ""} + /> + { + setSorting(val); + }} + options={[ + { + value: SORTING_CRITERION.CRITICAL_INSIGHTS, + label: "Critical insights", + defaultOrder: SORTING_ORDER.DESC + }, + { + value: SORTING_CRITERION.LATEST, + label: "Latest", + defaultOrder: SORTING_ORDER.DESC + } + ]} + default={defaultQuery.sorting} + /> + + + + + Showing{" "} + + {pageStartItemNumber} - {pageEndItemNumber} + {" "} + of {totalCount} + + + + + ); +}; diff --git a/src/components/Insights/InsightsCatalog/styles.ts b/src/components/Insights/InsightsCatalog/styles.ts new file mode 100644 index 000000000..0271cd51f --- /dev/null +++ b/src/components/Insights/InsightsCatalog/styles.ts @@ -0,0 +1,40 @@ +import styled from "styled-components"; + +export const Footer = styled.div` + display: flex; + justify-content: space-between; + padding: 8px; + font-size: 14px; +`; + +export const FooterItemsCount = styled.span` + font-weight: 500; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#818594"; + case "dark": + case "dark-jetbrains": + return "#b4b8bf"; + } + }}; +`; + +export const FooterPageItemsCount = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; + +export const Toolbar = styled.div` + display: flex; + justify-content: space-between; + padding: 8px; + gap: 8px; +`; diff --git a/src/components/Insights/InsightsCatalog/types.ts b/src/components/Insights/InsightsCatalog/types.ts new file mode 100644 index 000000000..56245d1a2 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/types.ts @@ -0,0 +1,22 @@ +import { GenericCodeObjectInsight, InsightsQuery } from "../types"; + +export interface InsightsCatalogProps { + insights: GenericCodeObjectInsight[]; + totalCount: number; + onJiraTicketCreate: ( + insight: GenericCodeObjectInsight, + spanCodeObjectId?: string + ) => void; + onQueryChange: (query: InsightsQuery) => void; + defaultQuery: InsightsQuery; +} + +export enum SORTING_CRITERION { + CRITICAL_INSIGHTS = "criticalinsights", + LATEST = "latest" +} + +export interface PagedData { + items: TData; + totalCount: number; +} diff --git a/src/components/Insights/InsightsPage/index.tsx b/src/components/Insights/InsightsPage/index.tsx new file mode 100644 index 000000000..0e8a30d59 --- /dev/null +++ b/src/components/Insights/InsightsPage/index.tsx @@ -0,0 +1,544 @@ +import { useEffect } from "react"; +import { usePersistence } from "../../../hooks/usePersistence"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { trackingEvents as globalTrackingEvents } from "../../../trackingEvents"; +import { InsightType } from "../../../types"; +import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; +import { Card } from "../../common/Card"; +import { BottleneckInsight } from "../BottleneckInsight"; +import { DurationBreakdownInsight } from "../DurationBreakdownInsight"; +import { DurationInsight } from "../DurationInsight"; +import { DurationSlowdownSourceInsight } from "../DurationSlowdownSourceInsight"; +import { EndpointNPlusOneInsight } from "../EndpointNPlusOneInsight"; +import { EndpointQueryOptimizationInsight } from "../EndpointQueryOptimizationInsight"; +import { ErrorsInsight } from "../ErrorsInsight"; +import { ExcessiveAPICallsInsight } from "../ExcessiveAPICallsInsight"; +import { HighNumberOfQueriesInsight } from "../HighNumberOfQueriesInsight"; +import { InsightCard } from "../InsightCard"; +import { NPlusOneInsight } from "../NPlusOneInsight"; +import { NoScalingIssueInsight } from "../NoScalingIssueInsight"; +import { PerformanceAtScaleInsight } from "../PerformanceAtScaleInsight"; +import { QueryOptimizationInsight } from "../QueryOptimizationInsight"; +import { RequestBreakdownInsight } from "../RequestBreakdownInsight"; +import { ScalingIssueInsight } from "../ScalingIssueInsight"; +import { SessionInViewInsight } from "../SessionInViewInsight"; +import { SlowEndpointInsight } from "../SlowEndpointInsight"; +import { SpanBottleneckInsight } from "../SpanBottleneckInsight"; +import { SpanNexusInsight } from "../SpanNexusInsight"; +import { TopUsageInsight } from "../TopUsageInsight"; +import { TrafficInsight } from "../TrafficInsight"; +import { actions } from "../actions"; +import { Description } from "../styles"; +import { trackingEvents } from "../tracking"; +import { + isChattyApiEndpointInsight, + isCodeObjectErrorsInsight, + isCodeObjectHotSpotInsight, + isEndpointBreakdownInsight, + isEndpointDurationSlowdownInsight, + isEndpointHighNumberOfQueriesInsight, + isEndpointHighUsageInsight, + isEndpointLowUsageInsight, + isEndpointNormalUsageInsight, + isEndpointQueryOptimizationInsight, + isEndpointSlowestSpansInsight, + isEndpointSuspectedNPlusOneInsight, + isSessionInViewEndpointInsight, + isSlowEndpointInsight, + isSpanDurationBreakdownInsight, + isSpanDurationsInsight, + isSpanEndpointBottleneckInsight, + isSpanNPlusOneInsight, + isSpanNexusInsight, + isSpanQueryOptimizationInsight, + isSpanScalingBadlyInsight, + isSpanScalingInsufficientDataInsight, + isSpanScalingWellInsight, + isSpanUsagesInsight +} from "../typeGuards"; +import { CodeObjectInsight, GenericCodeObjectInsight, Trace } from "../types"; +import * as s from "./styles"; +import { InsightPageProps, isInsightJiraTicketHintShownPayload } from "./types"; + +const getInsightToShowJiraHint = (insights: CodeObjectInsight[]): number => { + const insightsWithJiraButton = [ + InsightType.EndpointSpanNPlusOne, + InsightType.SpanNPlusOne, + InsightType.SpanEndpointBottleneck, + InsightType.SlowestSpans, + InsightType.SpanQueryOptimization, + InsightType.EndpointHighNumberOfQueries, + InsightType.EndpointQueryOptimization + ]; + + return insights.findIndex((insight) => + insightsWithJiraButton.includes(insight.type) + ); +}; + +const renderInsightCard = ( + insight: GenericCodeObjectInsight, + onJiraTicketCreate: ( + insight: GenericCodeObjectInsight, + spanCodeObjectId: string | undefined, + event?: string + ) => void, + isJiraHintEnabled: boolean +): JSX.Element | undefined => { + const handleErrorSelect = (errorId: string, insightType: InsightType) => { + sendTrackingEvent(globalTrackingEvents.USER_ACTION, { + action: `Follow ${insightType} link` + }); + window.sendMessageToDigma({ + action: actions.GO_TO_ERROR, + payload: { + errorId + } + }); + }; + + const handleErrorsExpandButtonClick = () => { + window.sendMessageToDigma({ + action: actions.GO_TO_ERRORS + }); + }; + + const handleHistogramButtonClick = ( + instrumentationLibrary: string, + name: string, + insightType: InsightType, + displayName: string + ) => { + window.sendMessageToDigma({ + action: actions.OPEN_HISTOGRAM, + payload: { + instrumentationLibrary, + name, + insightType, + displayName + } + }); + }; + + const handleLiveButtonClick = (prefixedCodeObjectId: string) => { + window.sendMessageToDigma({ + action: actions.OPEN_LIVE_VIEW, + payload: { + prefixedCodeObjectId + } + }); + }; + + const handleTraceButtonClick = ( + trace: Trace, + insightType: InsightType, + spanCodeObjectId?: string + ) => { + window.sendMessageToDigma({ + action: actions.GO_TO_TRACE, + payload: { + trace, + insightType, + spanCodeObjectId + } + }); + }; + + const handleCompareButtonClick = ( + traces: [Trace, Trace], + insightType: InsightType + ) => { + window.sendMessageToDigma({ + action: actions.GO_TO_TRACE_COMPARISON, + payload: { + traces, + insightType + } + }); + }; + + const handleAssetLinkClick = ( + spanCodeObjectId: string, + insightType: InsightType + ) => { + sendTrackingEvent(globalTrackingEvents.USER_ACTION, { + action: `Follow ${insightType} link` + }); + window.sendMessageToDigma({ + action: actions.GO_TO_ASSET, + payload: { + spanCodeObjectId + } + }); + }; + + const handleRecalculate = ( + prefixedCodeObjectId: string, + insightType: InsightType + ) => { + window.sendMessageToDigma({ + action: actions.RECALCULATE, + payload: { + prefixedCodeObjectId, + insightType + } + }); + }; + + const handleRefresh = (insightType: InsightType) => { + window.sendMessageToDigma({ + action: actions.REFRESH_ALL, + payload: { + insightType + } + }); + }; + + if (isSpanDurationsInsight(insight)) { + return ( + + ); + } + if (isSpanDurationBreakdownInsight(insight)) { + return ( + + ); + } + if (isSpanUsagesInsight(insight)) { + return ( + + ); + } + if (isSpanEndpointBottleneckInsight(insight)) { + return ( + + ); + } + if (isEndpointSlowestSpansInsight(insight)) { + return ( + + ); + } + if (isSlowEndpointInsight(insight)) { + return ( + + ); + } + if ( + isEndpointLowUsageInsight(insight) || + isEndpointNormalUsageInsight(insight) || + isEndpointHighUsageInsight(insight) + ) { + return ( + + ); + } + if (isCodeObjectErrorsInsight(insight)) { + return ( + + ); + } + if (isEndpointSuspectedNPlusOneInsight(insight)) { + return ( + + ); + } + if (isSpanNPlusOneInsight(insight)) { + return ( + + ); + } + if (isSpanScalingBadlyInsight(insight)) { + return ( + + ); + } + if (isCodeObjectHotSpotInsight(insight)) { + return ( + + Major errors occur or propagate through this function + + } + onRecalculate={handleRecalculate} + onRefresh={handleRefresh} + /> + ); + } + if (isEndpointDurationSlowdownInsight(insight)) { + return ( + + ); + } + + if (isEndpointBreakdownInsight(insight)) { + return ( + + ); + } + + if (isSpanScalingWellInsight(insight)) { + return ( + + ); + } + + if (isSpanScalingInsufficientDataInsight(insight)) { + return ( + + ); + } + + if (isSessionInViewEndpointInsight(insight)) { + return ( + + ); + } + + if (isChattyApiEndpointInsight(insight)) { + return ( + + ); + } + + if (isEndpointHighNumberOfQueriesInsight(insight)) { + return ( + + ); + } + + if (isSpanNexusInsight(insight)) { + return ( + + ); + } + + if (isSpanQueryOptimizationInsight(insight)) { + return ( + + ); + } + + if (isEndpointQueryOptimizationInsight(insight)) { + return ( + + ); + } +}; + +const IS_INSIGHT_JIRA_TICKET_HINT_SHOWN_PERSISTENCE_KEY = + "isInsightJiraTicketHintShown"; + +export const InsightsPage = (props: InsightPageProps) => { + const previousInsights = usePrevious(props.insights); + const [isInsightJiraTicketHintShown, setIsInsightJiraTicketHintShown] = + usePersistence( + IS_INSIGHT_JIRA_TICKET_HINT_SHOWN_PERSISTENCE_KEY, + "application" + ); + + const insightIndexWithJiraHint = getInsightToShowJiraHint(props.insights); + + useEffect(() => { + if (props.insights !== previousInsights) { + window.scrollTo(0, 0); + } + }, [props.insights, previousInsights]); + + useEffect(() => { + window.sendMessageToDigma({ + action: actions.MARK_INSIGHT_TYPES_AS_VIEWED, + payload: { + insightTypes: props.insights.map((x) => ({ + type: x.type, + reopenCount: x.reopenCount + })) + } + }); + }, [props.insights]); + + const handleShowJiraTicket = ( + insight: GenericCodeObjectInsight, + spanCodeObjectId: string | undefined, + event?: string + ) => { + props.onJiraTicketCreate(insight, spanCodeObjectId); + if (!isInsightJiraTicketHintShown?.value) { + sendTrackingEvent(trackingEvents.JIRA_TICKET_HINT_CLOSED, { event }); + } + setIsInsightJiraTicketHintShown({ value: true }); + }; + + return ( + + {props.insights.length > 0 ? ( + props.insights.map((insight, j) => { + return renderInsightCard( + insight, + handleShowJiraTicket, + j === insightIndexWithJiraHint + ); + }) + ) : ( + No data yet} + content={ + + No data received yet for this span, please trigger some actions + using this code to see more insights. + + } + /> + )} + + ); +}; diff --git a/src/components/Insights/InsightsPage/styles.ts b/src/components/Insights/InsightsPage/styles.ts new file mode 100644 index 000000000..610e8c7b6 --- /dev/null +++ b/src/components/Insights/InsightsPage/styles.ts @@ -0,0 +1,8 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + flex-grow: 1; +`; diff --git a/src/components/Insights/InsightsPage/types.ts b/src/components/Insights/InsightsPage/types.ts new file mode 100644 index 000000000..05b444be8 --- /dev/null +++ b/src/components/Insights/InsightsPage/types.ts @@ -0,0 +1,13 @@ +import { GenericCodeObjectInsight } from "../types"; + +export interface InsightPageProps { + insights: GenericCodeObjectInsight[]; + onJiraTicketCreate: ( + insight: GenericCodeObjectInsight, + spanCodeObjectId?: string + ) => void; +} + +export interface isInsightJiraTicketHintShownPayload { + value: boolean; +} diff --git a/src/components/Insights/actions.ts b/src/components/Insights/actions.ts index 19b6d1df9..6d619d731 100644 --- a/src/components/Insights/actions.ts +++ b/src/components/Insights/actions.ts @@ -27,5 +27,7 @@ export const actions = addPrefix(ACTION_PREFIX, { SET_COMMIT_INFO: "SET_COMMIT_INFO", LINK_TICKET: "LINK_TICKET", UNLINK_TICKET: "UNLINK_TICKET", - SET_TICKET_LINK: "SET_TICKET_LINK" + SET_TICKET_LINK: "SET_TICKET_LINK", + SET_DATA_LIST: "SET_DATA_LIST", + GET_DATA_LIST: "GET_DATA_LIST" }); diff --git a/src/components/Insights/common/useInsightsData.ts b/src/components/Insights/common/useInsightsData.ts new file mode 100644 index 000000000..c8bbab4eb --- /dev/null +++ b/src/components/Insights/common/useInsightsData.ts @@ -0,0 +1,132 @@ +import { useContext, useEffect, useRef, useState } from "react"; +import { dispatcher } from "../../../dispatcher"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { ConfigContext } from "../../common/App/ConfigContext"; +import { actions } from "../actions"; +import { + InsightsData, + InsightsQuery, + InsightsStatus, + ScopedInsightsQuery, + ViewMode +} from "../types"; + +interface UseInsightDataProps { + refreshInterval: number; + data?: InsightsData; + query: InsightsQuery; +} + +const getData = (query: ScopedInsightsQuery) => { + window.sendMessageToDigma({ + action: actions.GET_DATA_LIST, + payload: { + query: { + displayName: query.searchQuery, + sortBy: query.sorting.criterion, + sortOrder: query.sorting.order, + page: query.page, + scopedSpanCodeObjectId: query.scopedSpanCodeObjectId + } + } + }); +}; + +export const useInsightsData = (props: UseInsightDataProps) => { + const [data, setData] = useState({ + insightsStatus: InsightsStatus.LOADING, + insights: [], + viewMode: ViewMode.INSIGHTS, + totalCount: 0 + }); + const previousData = usePrevious(data); + const [isInitialLoading, setIsInitialLoading] = useState(false); + const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const [isLoading, setIsLoading] = useState(false); + const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); + const refreshTimerId = useRef(); + const { scope, environment } = useContext(ConfigContext); + + useEffect(() => { + getData({ + ...props.query, + scopedSpanCodeObjectId: scope?.span?.spanCodeObjectId || null + }); + + setIsInitialLoading(true); + const handleInsightsData = (data: unknown, timeStamp: number) => { + const insightsData = data as InsightsData; + insightsData.insightsStatus = InsightsStatus.DEFAULT; + insightsData.viewMode = ViewMode.INSIGHTS; + // insightsData.methods = []; + // insightsData.spans = []; + + setIsLoading(false); + setData(insightsData); + setLastSetDataTimeStamp(timeStamp); + }; + + dispatcher.addActionListener(actions.SET_DATA_LIST, handleInsightsData); + + return () => { + dispatcher.removeActionListener( + actions.SET_DATA_LIST, + handleInsightsData + ); + }; + }, []); + + useEffect(() => { + if (!props.data) { + return; + } + + setData(props.data); + }, [data]); + + useEffect(() => { + if (!previousData && data) { + setIsInitialLoading(false); + } + }, [previousData, data]); + + useEffect(() => { + if (previousLastSetDataTimeStamp !== lastSetDataTimeStamp) { + window.clearTimeout(refreshTimerId.current); + refreshTimerId.current = window.setTimeout( + () => { + getData({ + ...props.query, + scopedSpanCodeObjectId: scope?.span?.spanCodeObjectId || null + }); + }, + props.refreshInterval, + props.query + ); + } + + return () => { + window.clearTimeout(refreshTimerId.current); + }; + }, [ + lastSetDataTimeStamp, + previousLastSetDataTimeStamp, + props.query, + scope, + environment + ]); + + useEffect(() => { + setIsLoading(true); + getData({ + ...props.query, + scopedSpanCodeObjectId: scope?.span?.spanCodeObjectId || null + }); + }, [props.query, scope, environment]); + return { + isInitialLoading, + previousData, + data, + isLoading + }; +}; diff --git a/src/components/Insights/index.tsx b/src/components/Insights/index.tsx index 058b3fff8..d8c9e8119 100644 --- a/src/components/Insights/index.tsx +++ b/src/components/Insights/index.tsx @@ -1,27 +1,26 @@ import { useContext, useEffect, useState } from "react"; import { actions as globalActions } from "../../actions"; import { SLACK_WORKSPACE_URL } from "../../constants"; -import { dispatcher } from "../../dispatcher"; import { usePrevious } from "../../hooks/usePrevious"; import { trackingEvents as globalTrackingEvents } from "../../trackingEvents"; import { isNumber } from "../../typeGuards/isNumber"; import { openURLInDefaultBrowser } from "../../utils/openURLInDefaultBrowser"; import { sendTrackingEvent } from "../../utils/sendTrackingEvent"; import { ConfigContext } from "../common/App/ConfigContext"; -import { Button } from "../common/Button"; import { CircleLoader } from "../common/CircleLoader"; import { EmptyState } from "../common/EmptyState"; import { RegistrationDialog } from "../common/RegistrationDialog"; import { RegistrationFormValues } from "../common/RegistrationDialog/types"; +import { SORTING_ORDER } from "../common/SortingSelector/types"; import { CardsIcon } from "../common/icons/CardsIcon"; import { DocumentWithMagnifierIcon } from "../common/icons/DocumentWithMagnifierIcon"; import { LightBulbSmallCrossedIcon } from "../common/icons/LightBulbSmallCrossedIcon"; import { LightBulbSmallIcon } from "../common/icons/LightBulbSmallIcon"; import { OpenTelemetryLogoCrossedSmallIcon } from "../common/icons/OpenTelemetryLogoCrossedSmallIcon"; import { SlackLogoIcon } from "../common/icons/SlackLogoIcon"; -import { InsightList } from "./InsightList"; -import { Preview } from "./Preview"; -import { actions } from "./actions"; +import { InsightsCatalog } from "./InsightsCatalog"; +import { SORTING_CRITERION } from "./InsightsCatalog/types"; +import { useInsightsData } from "./common/useInsightsData"; import * as s from "./styles"; import { BottleneckInsightTicket } from "./tickets/BottleneckInsightTicket"; import { EndpointHighNumberOfQueriesInsightTicket } from "./tickets/EndpointHighNumberOfQueriesInsightTicket"; @@ -48,18 +47,26 @@ import { InsightTicketInfo, InsightsData, InsightsProps, + InsightsQuery, InsightsStatus, - Method, QueryOptimizationInsight, SpanEndpointBottleneckInsight, - SpanNPlusOneInsight, - ViewMode + SpanNPlusOneInsight } from "./types"; const REFRESH_INTERVAL = isNumber(window.insightsRefreshInterval) ? window.insightsRefreshInterval : 10 * 1000; // in milliseconds +const DEFAULT_QUERY = { + page: 0, + sorting: { + criterion: SORTING_CRITERION.CRITICAL_INSIGHTS, + order: SORTING_ORDER.DESC + }, + searchQuery: null +}; + const renderInsightTicket = ( data: InsightTicketInfo, onClose: () => void @@ -125,82 +132,37 @@ const renderInsightTicket = ( return null; }; +const sendMessage = (action: string, data?: any) => { + return window.sendMessageToDigma({ + action, + payload: { + ...data + } + }); +}; + export const Insights = (props: InsightsProps) => { - const [data, setData] = useState(); - const previousData = usePrevious(data); - const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); - const [isInitialLoading, setIsInitialLoading] = useState(false); - const [isLoading, setIsLoading] = useState(false); const [isAutofixing, setIsAutofixing] = useState(false); + const [query, setQuery] = useState(DEFAULT_QUERY); + const { isLoading, isInitialLoading, data, previousData } = useInsightsData({ + refreshInterval: REFRESH_INTERVAL, + data: props.data, + query + }); const [infoToOpenJiraTicket, setInfoToOpenJiraTicket] = useState>(); const config = useContext(ConfigContext); const previousUserRegistrationEmail = usePrevious( config.userRegistrationEmail ); - useState(false); const [isRegistrationInProgress, setIsRegistrationInProgress] = useState(false); - useEffect(() => { - window.sendMessageToDigma({ - action: actions.INITIALIZE - }); - - window.sendMessageToDigma({ - action: actions.GET_DATA - }); - setIsInitialLoading(true); - - const handleInsightsData = (data: unknown, timeStamp: number) => { - const insightsData = data as InsightsData; - - setIsLoading(insightsData.insightsStatus === InsightsStatus.LOADING); - - if (insightsData.insightsStatus !== InsightsStatus.LOADING) { - setData(insightsData); - } - setLastSetDataTimeStamp(timeStamp); - }; - - dispatcher.addActionListener(actions.SET_DATA, handleInsightsData); - - return () => { - dispatcher.removeActionListener(actions.SET_DATA, handleInsightsData); - }; - }, []); - - useEffect(() => { - const timerId = window.setTimeout(() => { - window.sendMessageToDigma({ - action: actions.GET_DATA - }); - }, REFRESH_INTERVAL); - - return () => { - window.clearTimeout(timerId); - }; - }, [lastSetDataTimeStamp]); - - useEffect(() => { - if (!props.data) { - return; - } - - setData(props.data); - }, [props.data]); - - useEffect(() => { - if (!previousData && data) { - setIsInitialLoading(false); - } - }, [previousData, data]); - - useEffect(() => { - if (previousData && data && previousData.assetId !== data.assetId) { - setIsAutofixing(false); - } - }, [previousData, data]); + // useEffect(() => { + // if (previousData && data && previousData.assetId !== data.assetId) { + // setIsAutofixing(false); + // } + // }, [previousData, data]); useEffect(() => { if ( @@ -215,48 +177,35 @@ export const Insights = (props: InsightsProps) => { previousUserRegistrationEmail ]); - const handleMethodSelect = (method: Method) => { - window.sendMessageToDigma({ - action: actions.GO_TO_METHOD, - payload: { - ...method - } - }); - }; + // const handleMethodSelect = (method: Method) => { + // sendMessage(actions.GO_TO_METHOD, method); + // }; const handleSlackLinkClick = () => { openURLInDefaultBrowser(SLACK_WORKSPACE_URL); }; - const handleAddAnnotationButtonClick = () => { - window.sendMessageToDigma({ - action: actions.ADD_ANNOTATION, - payload: { - methodId: data?.assetId - } - }); - }; - - const handleAutofixLinkClick = () => { - if (!isAutofixing) { - window.sendMessageToDigma({ - action: actions.AUTOFIX_MISSING_DEPENDENCY, - payload: { - methodId: data?.assetId - } - }); - setIsAutofixing(true); - } - }; + // const handleAddAnnotationButtonClick = () => { + // sendMessage(actions.ADD_ANNOTATION, { + // methodId: data?.assetId + // }); + // }; + + // const handleAutofixLinkClick = () => { + // if (!isAutofixing) { + // sendMessage(actions.AUTOFIX_MISSING_DEPENDENCY, { + // methodId: data?.assetId + // }); + // setIsAutofixing(true); + // } + // }; const handleTroubleshootingLinkClick = () => { sendTrackingEvent(globalTrackingEvents.TROUBLESHOOTING_LINK_CLICKED, { origin: "insights" }); - window.sendMessageToDigma({ - action: globalActions.OPEN_TROUBLESHOOTING_GUIDE - }); + sendMessage(globalActions.OPEN_TROUBLESHOOTING_GUIDE); }; const handleJiraTicketPopupOpen = ( @@ -271,12 +220,9 @@ export const Insights = (props: InsightsProps) => { }; const handleRegistrationSubmit = (formData: RegistrationFormValues) => { - window.sendMessageToDigma({ - action: globalActions.REGISTER, - payload: { - ...formData, - scope: "insights view jira ticket info" - } + sendMessage(globalActions.REGISTER, { + ...formData, + scope: "insights view jira ticket info" }); setIsRegistrationInProgress(true); @@ -286,35 +232,29 @@ export const Insights = (props: InsightsProps) => { setInfoToOpenJiraTicket(undefined); }; - const renderDefaultContent = (data?: InsightsData): JSX.Element => { - if (data?.viewMode === ViewMode.PREVIEW) { - return ( - - ); - } - - if (data?.viewMode === ViewMode.INSIGHTS && data.assetId) { - return ( - - ); + const renderDefaultContent = (data: InsightsData): JSX.Element => { + if (data.insights.length === 0 && !isLoading) { + const emptyMsg = + query.searchQuery?.length === 0 + ? "No insights" + : "There are no insights for this criteria"; + return ; } - - return <>; + return ( + { + setQuery(query); + }} + defaultQuery={DEFAULT_QUERY} + /> + ); }; const renderContent = ( - data: InsightsData | undefined, + data: InsightsData, isInitialLoading: boolean ): JSX.Element => { if (isInitialLoading) { @@ -388,22 +328,22 @@ export const Insights = (props: InsightsProps) => { Add an annotation to observe this method and collect data about its runtime behavior - {data.hasMissingDependency && ( + {/* {data.hasMissingDependency && ( missing dependency: opentelemetry.annotation Autofix - )} - {data.canInstrumentMethod && ( + )} */} + {/* {data.canInstrumentMethod && ( - )} + )} */} } /> diff --git a/src/components/Insights/styles.ts b/src/components/Insights/styles.ts index f56c94656..d8157ba55 100644 --- a/src/components/Insights/styles.ts +++ b/src/components/Insights/styles.ts @@ -126,3 +126,35 @@ export const PopupContainer = styled.div` height: 100%; padding: 0 4%; `; + +export const Footer = styled.div` + display: flex; + justify-content: space-between; + padding: 8px; + font-size: 14px; +`; + +export const FooterItemsCount = styled.span` + font-weight: 500; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#818594"; + case "dark": + case "dark-jetbrains": + return "#b4b8bf"; + } + }}; +`; + +export const FooterPageItemsCount = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; diff --git a/src/components/Insights/types.ts b/src/components/Insights/types.ts index 81fe2b624..07dbdabd4 100644 --- a/src/components/Insights/types.ts +++ b/src/components/Insights/types.ts @@ -1,6 +1,7 @@ import { MemoExoticComponent } from "react"; import { Duration } from "../../globals"; import { InsightType, SpanInfo, SpanInstanceInfo } from "../../types"; +import { Sorting } from "../common/SortingSelector/types"; import { IconProps } from "../common/icons/types"; export enum ViewMode { @@ -54,16 +55,18 @@ export interface Method { export interface InsightsData { insights: GenericCodeObjectInsight[]; - spans: MethodSpan[]; - assetId?: string; - serviceName: string; - environment: string; - insightsStatus: InsightsStatus; - viewMode: ViewMode; - methods: Method[]; - hasMissingDependency: boolean; - canInstrumentMethod: boolean; - needsObservabilityFix: boolean; + totalCount: number; + insightsStatus: InsightsStatus; // ?? default + viewMode: ViewMode; // Insights + + // methods: Method[]; // empty + // assetId?: string; // remove + // environment: string; // remove + // serviceName: string; // + // spans: MethodSpan[]; // to add on plugin + // hasMissingDependency: boolean; // remove + // canInstrumentMethod: boolean; // remove + // needsObservabilityFix: boolean; //remove } export interface InsightsProps { @@ -732,3 +735,13 @@ export interface EndpointQueryOptimizationInsight extends EndpointInsight { ticketLink: string | null; }[]; } + +export interface InsightsQuery { + page: number; + sorting: Sorting; + searchQuery: string | null; +} + +export interface ScopedInsightsQuery extends InsightsQuery { + scopedSpanCodeObjectId: string | null; +} diff --git a/src/components/Tests/EnvironmentFilter/index.tsx b/src/components/Tests/EnvironmentFilter/index.tsx index 5d7e2d6f8..01ef9a51b 100644 --- a/src/components/Tests/EnvironmentFilter/index.tsx +++ b/src/components/Tests/EnvironmentFilter/index.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { FilterMenu } from "../../Assets/FilterMenu"; +import { FilterMenu } from "../../common/FilterMenu"; import { NewPopover } from "../../common/NewPopover"; import { ChevronIcon } from "../../common/icons/ChevronIcon"; import { GlobeIcon } from "../../common/icons/GlobeIcon"; diff --git a/src/components/Tests/EnvironmentFilter/types.ts b/src/components/Tests/EnvironmentFilter/types.ts index 5bd73c00a..988e33d46 100644 --- a/src/components/Tests/EnvironmentFilter/types.ts +++ b/src/components/Tests/EnvironmentFilter/types.ts @@ -1,4 +1,4 @@ -import { MenuItem } from "../../Assets/FilterMenu/types"; +import { MenuItem } from "../../common/FilterMenu/types"; export interface EnvironmentFilterProps { items: MenuItem[]; diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index 9db52a993..280a6b528 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -4,8 +4,8 @@ import { dispatcher } from "../../dispatcher"; import { usePrevious } from "../../hooks/usePrevious"; import { isNumber } from "../../typeGuards/isNumber"; import { sendTrackingEvent } from "../../utils/sendTrackingEvent"; -import { MenuItem } from "../Assets/FilterMenu/types"; 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"; diff --git a/src/components/Assets/AssetsFilter/FilterButton/index.tsx b/src/components/common/FilterButton/index.tsx similarity index 80% rename from src/components/Assets/AssetsFilter/FilterButton/index.tsx rename to src/components/common/FilterButton/index.tsx index d1be0ef25..2f9d0d95f 100644 --- a/src/components/Assets/AssetsFilter/FilterButton/index.tsx +++ b/src/components/common/FilterButton/index.tsx @@ -1,5 +1,5 @@ -import { isNumber } from "../../../../typeGuards/isNumber"; -import { FunnelIcon } from "../../../common/icons/FunnelIcon"; +import { isNumber } from "../../../typeGuards/isNumber"; +import { FunnelIcon } from "../icons/FunnelIcon"; import * as s from "./styles"; import { FilterButtonProps } from "./types"; diff --git a/src/components/Assets/AssetsFilter/FilterButton/styles.ts b/src/components/common/FilterButton/styles.ts similarity index 94% rename from src/components/Assets/AssetsFilter/FilterButton/styles.ts rename to src/components/common/FilterButton/styles.ts index d4fe160f3..b147cbb5d 100644 --- a/src/components/Assets/AssetsFilter/FilterButton/styles.ts +++ b/src/components/common/FilterButton/styles.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { grayScale } from "../../../common/App/v2colors"; +import { grayScale } from "../App/v2colors"; import { ButtonProps } from "./types"; export const Button = styled.button` diff --git a/src/components/Assets/AssetsFilter/FilterButton/types.ts b/src/components/common/FilterButton/types.ts similarity index 100% rename from src/components/Assets/AssetsFilter/FilterButton/types.ts rename to src/components/common/FilterButton/types.ts diff --git a/src/components/Assets/FilterMenu/FilterMenu.stories.tsx b/src/components/common/FilterMenu/FilterMenu.stories.tsx similarity index 98% rename from src/components/Assets/FilterMenu/FilterMenu.stories.tsx rename to src/components/common/FilterMenu/FilterMenu.stories.tsx index a4b83f312..5a00dd45b 100644 --- a/src/components/Assets/FilterMenu/FilterMenu.stories.tsx +++ b/src/components/common/FilterMenu/FilterMenu.stories.tsx @@ -5,7 +5,7 @@ import { FilterMenu } from "."; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta: Meta = { - title: "Assets/FilterMenu", + title: "Common/FilterMenu", component: FilterMenu, parameters: { // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout diff --git a/src/components/Assets/FilterMenu/index.tsx b/src/components/common/FilterMenu/index.tsx similarity index 89% rename from src/components/Assets/FilterMenu/index.tsx rename to src/components/common/FilterMenu/index.tsx index 26c902a31..d2590530b 100644 --- a/src/components/Assets/FilterMenu/index.tsx +++ b/src/components/common/FilterMenu/index.tsx @@ -1,9 +1,9 @@ import { ChangeEvent, useState } from "react"; -import { Checkbox } from "../../common/Checkbox"; -import { NewCircleLoader } from "../../common/NewCircleLoader"; -import { Tooltip } from "../../common/Tooltip"; -import { CrossIcon } from "../../common/icons/CrossIcon"; -import { MagnifierIcon } from "../../common/icons/MagnifierIcon"; +import { Checkbox } from "../Checkbox"; +import { NewCircleLoader } from "../NewCircleLoader"; +import { Tooltip } from "../Tooltip"; +import { CrossIcon } from "../icons/CrossIcon"; +import { MagnifierIcon } from "../icons/MagnifierIcon"; import * as s from "./styles"; import { FilterMenuProps } from "./types"; diff --git a/src/components/Assets/FilterMenu/styles.ts b/src/components/common/FilterMenu/styles.ts similarity index 100% rename from src/components/Assets/FilterMenu/styles.ts rename to src/components/common/FilterMenu/styles.ts diff --git a/src/components/Assets/FilterMenu/types.ts b/src/components/common/FilterMenu/types.ts similarity index 100% rename from src/components/Assets/FilterMenu/types.ts rename to src/components/common/FilterMenu/types.ts diff --git a/src/components/common/SearchInput/index.tsx b/src/components/common/SearchInput/index.tsx new file mode 100644 index 000000000..3b3fce996 --- /dev/null +++ b/src/components/common/SearchInput/index.tsx @@ -0,0 +1,28 @@ +import { ChangeEvent, useEffect, useState } from "react"; +import { useDebounce } from "../../../hooks/useDebounce"; +import { MagnifierIcon } from "../icons/MagnifierIcon"; +import * as s from "./styles"; +import { SearchInputProps } from "./types"; + +export const SearchInput = (props: SearchInputProps) => { + const [searchInputValue, setSearchInputValue] = useState(props.default); + const debouncedSearchInputValue = useDebounce(searchInputValue, 1000); + + useEffect(() => { + props.onChange(debouncedSearchInputValue); + }, [debouncedSearchInputValue]); + + return ( + + + + + ) => + setSearchInputValue(e.target.value) + } + /> + + ); +}; diff --git a/src/components/common/SearchInput/styles.ts b/src/components/common/SearchInput/styles.ts new file mode 100644 index 000000000..b84a54369 --- /dev/null +++ b/src/components/common/SearchInput/styles.ts @@ -0,0 +1,78 @@ +import styled from "styled-components"; +import { grayScale } from "../App/v2colors"; + +export const SearchInputContainer = styled.div` + display: flex; + position: relative; + flex-grow: 1; +`; + +export const SearchInputIconContainer = styled.div` + display: flex; + align-items: center; + margin: auto; + position: absolute; + top: 0; + bottom: 0; + left: 4px; + color: ${({ theme }) => theme.colors.icon.disabledAlt}; +`; + +export const SearchInput = styled.input` + width: 100%; + font-size: 14px; + padding: 4px 4px 4px 20px; + border-radius: 4px; + outline: none; + border: 1px solid ${({ theme }) => theme.colors.stroke.primary}; + background: ${({ theme }) => { + switch (theme.mode) { + case "light": + return grayScale[50]; + case "dark": + case "dark-jetbrains": + return grayScale[1000]; + } + }}; + box-shadow: 1px 1px 4px 0 rgba(0 0 0 / 25%); + caret-color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#4d668a"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } + }}; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#4d668a"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } + }}; + + &:focus, + &:hover { + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#7891d0"; + case "dark": + case "dark-jetbrains": + return "#9b9b9b"; + } + }}; + } + + &::placeholder { + color: ${({ theme }) => theme.colors.text.disabledAlt}; + } + + &:focus::placeholder { + color: transparent; + } +`; diff --git a/src/components/common/SearchInput/types.ts b/src/components/common/SearchInput/types.ts new file mode 100644 index 000000000..388cd2b48 --- /dev/null +++ b/src/components/common/SearchInput/types.ts @@ -0,0 +1,4 @@ +export interface SearchInputProps { + onChange: (value: string) => void; + default: string; +} diff --git a/src/components/common/SortingSelector/index.tsx b/src/components/common/SortingSelector/index.tsx new file mode 100644 index 000000000..ea7a56026 --- /dev/null +++ b/src/components/common/SortingSelector/index.tsx @@ -0,0 +1,137 @@ +import { useCallback, useEffect, useState } from "react"; +import { DefaultTheme, useTheme } from "styled-components"; +import { Menu } from "../Menu"; +import { Popover } from "../Popover"; +import { PopoverContent } from "../Popover/PopoverContent"; +import { PopoverTrigger } from "../Popover/PopoverTrigger"; +import { ChevronIcon } from "../icons/ChevronIcon"; +import { SortIcon } from "../icons/SortIcon"; +import { Direction } from "../icons/types"; +import * as s from "./styles"; +import { SORTING_ORDER, Sorting, SortingSelectorProps } from "./types"; + +const getSortingMenuChevronColor = (theme: DefaultTheme) => { + switch (theme.mode) { + case "light": + return "#4d668a"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } +}; + +const getSortIconColor = (theme: DefaultTheme, selected: boolean) => { + if (selected) { + switch (theme.mode) { + case "light": + return "#f1f5fa"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } + } + switch (theme.mode) { + case "light": + return "#828797"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } +}; + +export const SortingSelector = (props: SortingSelectorProps) => { + const theme = useTheme(); + const [sorting, setSorting] = useState(props.default); + const [isSortingMenuOpen, setIsSortingMenuOpen] = useState(false); + const sortingMenuChevronColor = getSortingMenuChevronColor(theme); + + useEffect(() => { + props.onChange(sorting); + }, [sorting, props.default]); + + const getSortingCriterionInfo = useCallback( + (value: string) => { + return props.options.find((x) => x.value === value); + }, + [props.options] + ); + + const handleSortingMenuItemSelect = (value: string) => { + if (sorting.criterion === value) { + setSorting({ + ...sorting, + order: + sorting.order === SORTING_ORDER.DESC + ? SORTING_ORDER.ASC + : SORTING_ORDER.DESC + }); + } else { + setSorting({ + criterion: value, + order: + getSortingCriterionInfo(value)?.defaultOrder || SORTING_ORDER.DESC + }); + } + handleSortingMenuToggle(); + }; + + const handleSortingMenuToggle = () => { + setIsSortingMenuOpen(!isSortingMenuOpen); + }; + + const handleSortingOrderToggleOptionButtonClick = (order: SORTING_ORDER) => { + setSorting({ + ...sorting, + order + }); + }; + + return ( + + + + + Sort by + + + + + ({ + value, + label + }))} + onSelect={handleSortingMenuItemSelect} + /> + + + + + {[SORTING_ORDER.DESC, SORTING_ORDER.ASC].map((order) => { + const isSelected = sorting.order === order; + const iconColor = getSortIconColor(theme, isSelected); + + return ( + handleSortingOrderToggleOptionButtonClick(order)} + > + + + + + ); + })} + + + ); +}; diff --git a/src/components/common/SortingSelector/styles.ts b/src/components/common/SortingSelector/styles.ts new file mode 100644 index 000000000..70988769f --- /dev/null +++ b/src/components/common/SortingSelector/styles.ts @@ -0,0 +1,103 @@ +import styled from "styled-components"; +import { + SORTING_ORDER, + SortingMenuButtonProps, + SortingOrderIconContainerProps, + SortingOrderOptionProps +} from "./types"; + +export const PopoverContainer = styled.div` + margin-left: auto; + display: flex; + gap: 8px; +`; + +export const SortingMenuButton = styled.button` + background: none; + cursor: pointer; + display: flex; + gap: 4px; + font-weight: 500; + font-size: 14px; + text-wrap: nowrap; + align-items: center; + border-radius: 4px; + padding: 4px 8px; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#828797"; + case "dark": + case "dark-jetbrains": + return "#9b9b9b"; + } + }}; + border: 1px solid + ${({ theme, $isOpen }) => { + if ($isOpen) { + switch (theme.mode) { + case "light": + return "#7891d0"; + case "dark": + case "dark-jetbrains": + return "#9b9b9b"; + } + } + + switch (theme.mode) { + case "light": + return "#d0d6eb"; + case "dark": + case "dark-jetbrains": + return "#49494d"; + } + }}; + + &:hover, + &:focus { + outline: none; + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#7891d0"; + case "dark": + case "dark-jetbrains": + return "#9b9b9b"; + } + }}; + } +`; + +export const SortingOrderToggle = styled.div` + display: flex; + border-radius: 4px; + padding: 4px; + gap: 4px; + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#b9c0d4"; + case "dark": + case "dark-jetbrains": + return "#49494d"; + } + }}; +`; + +export const SortingOrderToggleOptionButton = styled.button` + border: none; + outline: none; + padding: 0 1px; + border-radius: 2px; + cursor: pointer; + background: ${({ $selected }) => ($selected ? "#3538cd" : "transparent")}; +`; + +export const SortingOrderIconContainer = styled.div` + display: flex; + transform: scaleY( + ${({ $sortingOrder }) => ($sortingOrder === SORTING_ORDER.DESC ? -1 : 1)} + ); +`; diff --git a/src/components/common/SortingSelector/types.ts b/src/components/common/SortingSelector/types.ts new file mode 100644 index 000000000..a0872bd46 --- /dev/null +++ b/src/components/common/SortingSelector/types.ts @@ -0,0 +1,41 @@ +export interface SortingSelectorProps { + options: SortingOption[]; + default: Sorting; + onChange: (val: Sorting) => void; +} + +export interface SortingOption { + label: string; + defaultOrder: SORTING_ORDER; + value: string; +} + +export interface SortingMenuButtonProps { + $isOpen: boolean; +} + +export interface SortingOrderOptionProps { + $selected: boolean; +} + +export enum SORTING_ORDER { + ASC = "asc", + DESC = "desc" +} + +export interface SortingMenuButtonProps { + $isOpen: boolean; +} + +export interface SortingOrderOptionProps { + $selected: boolean; +} + +export interface SortingOrderIconContainerProps { + $sortingOrder: SORTING_ORDER; +} + +export interface Sorting { + criterion: string; + order: SORTING_ORDER; +}