From 361077b94f2a7b43206cceda268a4f60190427e0 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Mon, 16 Sep 2024 12:35:29 +0300 Subject: [PATCH 01/26] inintial --- package-lock.json | 29 +++++ package.json | 2 + .../NewReport/Chart/Chart.stories.tsx | 20 +++ .../Dashboard/NewReport/Chart/index.tsx | 90 +++++++++++++ .../Dashboard/NewReport/Chart/styles.ts | 7 ++ .../Dashboard/NewReport/Chart/types.ts | 11 ++ .../Dashboard/NewReport/NewReport.stories.tsx | 105 ++++++++++++++++ .../ReportHeader/ReportHeader.stories.tsx | 31 +++++ .../NewReport/ReportHeader/index.tsx | 118 ++++++++++++++++++ .../NewReport/ReportHeader/styles.ts | 35 ++++++ .../Dashboard/NewReport/ReportHeader/types.ts | 9 ++ .../NewReport/Toggle/Toggle.stories.tsx | 28 +++++ .../Dashboard/NewReport/Toggle/index.tsx | 26 ++++ .../Dashboard/NewReport/Toggle/styles.ts | 41 ++++++ .../Dashboard/NewReport/Toggle/types.ts | 19 +++ src/components/Dashboard/NewReport/index.tsx | 39 ++++++ src/components/Dashboard/NewReport/styles.ts | 27 ++++ .../Dashboard/NewReport/tracking.ts | 14 +++ src/components/Dashboard/NewReport/types.ts | 25 ++++ .../Dashboard/NewReport/useReportsData.ts | 35 ++++++ src/components/Dashboard/actions.ts | 4 +- 21 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 src/components/Dashboard/NewReport/Chart/Chart.stories.tsx create mode 100644 src/components/Dashboard/NewReport/Chart/index.tsx create mode 100644 src/components/Dashboard/NewReport/Chart/styles.ts create mode 100644 src/components/Dashboard/NewReport/Chart/types.ts create mode 100644 src/components/Dashboard/NewReport/NewReport.stories.tsx create mode 100644 src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx create mode 100644 src/components/Dashboard/NewReport/ReportHeader/index.tsx create mode 100644 src/components/Dashboard/NewReport/ReportHeader/styles.ts create mode 100644 src/components/Dashboard/NewReport/ReportHeader/types.ts create mode 100644 src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx create mode 100644 src/components/Dashboard/NewReport/Toggle/index.tsx create mode 100644 src/components/Dashboard/NewReport/Toggle/styles.ts create mode 100644 src/components/Dashboard/NewReport/Toggle/types.ts create mode 100644 src/components/Dashboard/NewReport/index.tsx create mode 100644 src/components/Dashboard/NewReport/styles.ts create mode 100644 src/components/Dashboard/NewReport/tracking.ts create mode 100644 src/components/Dashboard/NewReport/types.ts create mode 100644 src/components/Dashboard/NewReport/useReportsData.ts diff --git a/package-lock.json b/package-lock.json index 6b916cbac..99e6bf6b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^2.29.3", "free-email-domains": "^1.2.5", + "highcharts": "^11.4.8", + "highcharts-react-official": "^3.2.1", "react": "^18.2.0", "react-cool-dimensions": "^3.0.1", "react-dom": "^18.2.0", @@ -9401,6 +9403,22 @@ "he": "bin/he" } }, + "node_modules/highcharts": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.4.8.tgz", + "integrity": "sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==", + "license": "https://www.highcharts.com/license" + }, + "node_modules/highcharts-react-official": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.1.tgz", + "integrity": "sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA==", + "license": "MIT", + "peerDependencies": { + "highcharts": ">=6.0.0", + "react": ">=16.8.0" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -22521,6 +22539,17 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "highcharts": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.4.8.tgz", + "integrity": "sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==" + }, + "highcharts-react-official": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.1.tgz", + "integrity": "sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA==", + "requires": {} + }, "highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", diff --git a/package.json b/package.json index 69dd78898..49a5a184c 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,8 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^2.29.3", "free-email-domains": "^1.2.5", + "highcharts": "^11.4.8", + "highcharts-react-official": "^3.2.1", "react": "^18.2.0", "react-cool-dimensions": "^3.0.1", "react-dom": "^18.2.0", diff --git a/src/components/Dashboard/NewReport/Chart/Chart.stories.tsx b/src/components/Dashboard/NewReport/Chart/Chart.stories.tsx new file mode 100644 index 000000000..c5d33114b --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/Chart.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { Chart } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Dashboard/NewReport/Chart", + component: Chart, + 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/Dashboard/NewReport/Chart/index.tsx b/src/components/Dashboard/NewReport/Chart/index.tsx new file mode 100644 index 000000000..fddf9d153 --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/index.tsx @@ -0,0 +1,90 @@ +import Highcharts from "highcharts"; +import HighchartsReact from "highcharts-react-official"; +import addHeatmap from "highcharts/modules/heatmap"; +import addTreemapModule from "highcharts/modules/treemap"; +import * as s from "./styles"; +import { ChartProps } from "./types"; +addTreemapModule(Highcharts); +addHeatmap(Highcharts); + +const baseOptions = ( + type: "squarified" | "stripes" | "strip" | "sliceAndDice" +) => ({ + colorAxis: { + maxColor: "#B92B2B", + minColor: "#2BA0B9" + }, + legend: { + enabled: false + }, + series: [ + { + type: "treemap", + layoutStartingDirection: "horizontal", + layoutAlgorithm: type ?? "sliceAndDice", + alternateStartingDirection: true, + borderRadius: 12, + clip: false, + levels: [ + { + level: 1, + borderWidth: 12, + borderColor: "#1A1B1E", // todo color depends on theme + dataLabels: { + enabled: true, + align: "center", + verticalAlign: "middle", + padding: 24, + + style: { + fontSize: 32, + color: "#FFFFFF", + fontWeight: "400" + } + } + } + ], + data: [ + { + name: "Payment Service
12 / 1500", + value: 6, + colorValue: 6 + }, + { + name: "Transaction Service
15 / 710", + value: 5, + colorValue: 5 + }, + { + name: "Share Service
5 / 530", + value: 3, + colorValue: 3 + }, + { + name: "Metadata Service
2 / 100", + value: 1, + colorValue: 1 + } + ] + } + ], + chart: { + backgroundColor: "transparent", + height: "100%", // 16:9 ratio,Transaction Service + margin: 0 + } +}); + +export const Chart = ({ type }: ChartProps) => { + return ( + + + + ); +}; diff --git a/src/components/Dashboard/NewReport/Chart/styles.ts b/src/components/Dashboard/NewReport/Chart/styles.ts new file mode 100644 index 000000000..459103c5b --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/styles.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + height: 100%; +`; diff --git a/src/components/Dashboard/NewReport/Chart/types.ts b/src/components/Dashboard/NewReport/Chart/types.ts new file mode 100644 index 000000000..f97d8383d --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/types.ts @@ -0,0 +1,11 @@ +export interface ChartDataItem { + id: string; + name: string; + value: string; +} + +export interface ChartProps { + labelFormat?: string; + data: ChartDataItem[]; + type: "squarified" | "stripes" | "strip" | "sliceAndDice"; +} diff --git a/src/components/Dashboard/NewReport/NewReport.stories.tsx b/src/components/Dashboard/NewReport/NewReport.stories.tsx new file mode 100644 index 000000000..968aadb50 --- /dev/null +++ b/src/components/Dashboard/NewReport/NewReport.stories.tsx @@ -0,0 +1,105 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { NewReport } from "."; +import { actions as globalActions } from "../../../actions"; +import { actions } from "../actions"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Dashboard/NewReport", + component: NewReport, + 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 = { + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_REPORT_ASSETS_STATS, + payload: { + totalCount: 10, + data: [ + { + name: "Endpoint", + count: 1 + }, + { + name: "DatabaseQueries", + count: 2 + }, + { + name: "Consumer", + count: 3 + }, + { + name: "EndpointClient", + count: 4 + }, + { + name: "CodeLocation", + count: 5 + }, + { + name: "Cache", + count: 6 + }, + { + name: "Other", + count: 7 + } + ] + } + }); + }, 500); + + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_REPORT_ISSUES_STATS, + payload: { + totalCount: 10, + fixedCount: 9, + activeCount: 8, + regressionCount: 7, + criticalCount: 6 + } + }); + }, 500); + + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_SERVICES, + payload: ["service 1", "service 2", "service 3", "service 4"] + }); + }, 500); + + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: globalActions.SET_ENVIRONMENTS, + payload: [ + { + id: "test1", + name: "test1", + type: "Public" + }, + { + id: "test2", + name: "test2", + type: "Public" + } + ] + }); + }, 500); + } +}; diff --git a/src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx b/src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx new file mode 100644 index 000000000..cb5b357b3 --- /dev/null +++ b/src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { ReportHeader } from "."; +import { actions } from "../../actions"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Dashboard/NewReport/ReportHeader", + component: ReportHeader, + 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 = { + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_SERVICES, + payload: ["service 3", "service 1", "service 2", "service 4"] + }); + }, 500); + } +}; diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx new file mode 100644 index 000000000..222e59337 --- /dev/null +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -0,0 +1,118 @@ +import { useEffect, useMemo, useState } from "react"; +import { + DataFetcherConfiguration, + useFetchData +} from "../../../../hooks/useFetchData"; +import { useConfigSelector } from "../../../../store/config/useConfigSelector"; +import { GlobeIcon } from "../../../common/icons/12px/GlobeIcon"; +import { WrenchIcon } from "../../../common/icons/12px/WrenchIcon"; +import { actions } from "../../actions"; +import { Toggle } from "../Toggle"; +import { GetServicesPayload } from "../types"; +import * as s from "./styles"; +import { ReportHeaderProps } from "./types"; + +const baseFetchConfig = { + refreshWithInterval: false, + refreshOnPayloadChange: true +}; + +const dataFetcherFiltersConfiguration: DataFetcherConfiguration = { + requestAction: actions.GET_SERVICES, + responseAction: actions.SET_SERVICES, + ...baseFetchConfig +}; + +export const ReportHeader = ({ onFilterChanged }: ReportHeaderProps) => { + const [selectedServices, setSelectedServices] = useState([]); + const [selectedEnvironment, setSelectedEnvironment] = useState( + null + ); + const { environments } = useConfigSelector(); + + const getServicesPayload = useMemo( + () => ({ environment: selectedEnvironment }), + [selectedEnvironment] + ); + + const { data: services, getData } = useFetchData< + GetServicesPayload, + string[] + >(dataFetcherFiltersConfiguration, getServicesPayload); + + useEffect(() => { + getData(); + }, []); + + const handleSelectedEnvironmentChanged = (option: string | string[]) => { + const newItem = + option === selectedEnvironment + ? [""] + : Array.isArray(option) + ? option + : [option]; + + setSelectedEnvironment(newItem[0]); + }; + + const handleSelectedServicesChanged = (option: string | string[]) => { + const newItem = Array.isArray(option) ? option : [option]; + setSelectedServices(newItem); + onFilterChanged({ environmentId: selectedEnvironment, services: newItem }); + }; + + return ( + + + Services with critical issues + { + // + }} + /> + + + a.name.localeCompare(b.name)) + .map((x) => ({ + label: x.name, + value: x.id, + enabled: true, + selected: x.id === selectedEnvironment + })) ?? [] + } + icon={GlobeIcon} + onChange={handleSelectedEnvironmentChanged} + placeholder={ + environments?.find((x) => x.id === selectedEnvironment)?.name ?? + "Select Environments" + } + /> + + ({ + label: service, + value: service, + enabled: true, + selected: selectedServices.includes(service) + })) ?? [] + } + multiselect={true} + icon={WrenchIcon} + onChange={handleSelectedServicesChanged} + placeholder={ + selectedServices.length > 0 ? "Services" : "All Services" + } + /> + + + ); +}; diff --git a/src/components/Dashboard/NewReport/ReportHeader/styles.ts b/src/components/Dashboard/NewReport/ReportHeader/styles.ts new file mode 100644 index 000000000..50273d06f --- /dev/null +++ b/src/components/Dashboard/NewReport/ReportHeader/styles.ts @@ -0,0 +1,35 @@ +import styled from "styled-components"; +import { Select } from "../../../common/v3/Select"; + +export const Container = styled.div` + display: flex; + gap: 24px; + flex-direction: column; +`; + +export const Title = styled.div` + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: normal; + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const Row = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const FilterSelect = styled(Select)` + height: 36px; + width: 180px; + border-radius: 8px; + background: transparent; +`; + +export const FilterRow = styled(Row)` + display: flex; + justify-content: start; + gap: 18px; +`; diff --git a/src/components/Dashboard/NewReport/ReportHeader/types.ts b/src/components/Dashboard/NewReport/ReportHeader/types.ts new file mode 100644 index 000000000..2bc89b9b5 --- /dev/null +++ b/src/components/Dashboard/NewReport/ReportHeader/types.ts @@ -0,0 +1,9 @@ +import { ReportFilterQuery } from "../types"; + +export interface GetServicesPayload { + environment: string | null; +} + +export interface ReportHeaderProps { + onFilterChanged: (query: ReportFilterQuery) => void; +} diff --git a/src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx b/src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx new file mode 100644 index 000000000..37f8d950a --- /dev/null +++ b/src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx @@ -0,0 +1,28 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { Toggle } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Dashboard/NewReport/Toggle", + component: Toggle, + 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: { + options: [ + { value: "Baseline", label: "Baseline" }, + { value: "Changes", label: "Changes" } + ], + value: "Baseline" + } +}; diff --git a/src/components/Dashboard/NewReport/Toggle/index.tsx b/src/components/Dashboard/NewReport/Toggle/index.tsx new file mode 100644 index 000000000..9472c9801 --- /dev/null +++ b/src/components/Dashboard/NewReport/Toggle/index.tsx @@ -0,0 +1,26 @@ +import * as s from "./styles"; +import { ToggleProps, ToggleValue } from "./types"; + +export const Toggle = ({ + options, + value, + onValueChange +}: ToggleProps) => { + const handleOptionButtonClick = (value: T) => { + onValueChange(value); + }; + + return ( + + {options.map((option) => ( + handleOptionButtonClick(option.value)} + > + {option.label} + + ))} + + ); +}; diff --git a/src/components/Dashboard/NewReport/Toggle/styles.ts b/src/components/Dashboard/NewReport/Toggle/styles.ts new file mode 100644 index 000000000..23e835596 --- /dev/null +++ b/src/components/Dashboard/NewReport/Toggle/styles.ts @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +import { subscriptRegularTypography } from "../../../common/App/typographies"; +import { OptionButtonProps } from "./types"; + +export const Container = styled.div` + display: flex; + border-radius: 8px; + height: 36px; + width: fit-content; + border: 1px solid ${({ theme }) => theme.colors.v3.stroke.dark}; + background: ${({ theme }) => theme.colors.v3.surface.secondary}; +`; + +export const OptionButton = styled.button` + ${subscriptRegularTypography} + + display: flex; + font-family: inherit; + border: none; + outline: none; + cursor: pointer; + user-select: none; + border-radius: 8px; + align-items: center; + padding: 0 16px; + color: ${({ theme, $selected }) => + $selected ? theme.colors.v3.text.primary : theme.colors.v3.text.secondary}; + background: ${({ theme, $selected }) => + $selected ? theme.colors.v3.surface.brandPrimary : "transparent"}; + + &:first-child { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + + &:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } +`; diff --git a/src/components/Dashboard/NewReport/Toggle/types.ts b/src/components/Dashboard/NewReport/Toggle/types.ts new file mode 100644 index 000000000..de145fa85 --- /dev/null +++ b/src/components/Dashboard/NewReport/Toggle/types.ts @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; + +export type ToggleValue = string | number; +export type ToggleSize = "small" | "large"; + +export interface ToggleOption { + value: T; + label?: string | ReactNode; +} + +export interface ToggleProps { + options: ToggleOption[]; + onValueChange: (value: T) => void; + value: T; +} + +export interface OptionButtonProps { + $selected: boolean; +} diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx new file mode 100644 index 000000000..d6caf0672 --- /dev/null +++ b/src/components/Dashboard/NewReport/index.tsx @@ -0,0 +1,39 @@ +import { useLayoutEffect } from "react"; +import { actions } from "../actions"; + +import { Chart } from "./Chart"; +import { ReportHeader } from "./ReportHeader"; +import * as s from "./styles"; + +// const DefaultQuery: ReportFilterQuery = { +// environmentId: "", +// services: [] +// }; + +export const NewReport = ({ + type +}: { + type: "squarified" | "stripes" | "strip" | "sliceAndDice"; +}) => { + // const [query, setQuery] = useState(DefaultQuery); + // const { data } = useReportsData(query); + + useLayoutEffect(() => { + window.sendMessageToDigma({ + action: actions.INITIALIZE + }); + }, []); + + const handleFilterChanged = () => + // query: ReportFilterQuery + { + // setQuery(query); + }; + + return ( + + + + + ); +}; diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts new file mode 100644 index 000000000..438eb3bb5 --- /dev/null +++ b/src/components/Dashboard/NewReport/styles.ts @@ -0,0 +1,27 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + min-height: 100%; + min-width: fit-content; + background: ${({ theme }) => theme.colors.v3.surface.primary}; + padding: 24px; + gap: 24px; +`; + +export const Content = styled.div` + padding: 32px; + display: flex; + gap: 16px; +`; + +export const Column = styled.div` + display: flex; + flex-direction: column; + padding: 16px; + align-self: stretch; + background: ${({ theme }) => theme.colors.v3.surface.secondary}; + border-radius: 12px; + width: 100%; +`; diff --git a/src/components/Dashboard/NewReport/tracking.ts b/src/components/Dashboard/NewReport/tracking.ts new file mode 100644 index 000000000..77899a324 --- /dev/null +++ b/src/components/Dashboard/NewReport/tracking.ts @@ -0,0 +1,14 @@ +import { addPrefix } from "../../../utils/addPrefix"; + +const TRACKING_PREFIX = "report"; + +export const trackingEvents = addPrefix( + TRACKING_PREFIX, + { + REFRESH_DATA_CLICKED: "refresh report data clicked", + DOWNLOAD_REPORT_CLICKED: "download report data clicked", + ENVIRONMENT_FILTER_SELECTED: "environment filter selected", + SERVICES_FILTER_SELECTED: "service filter selected" + }, + " " +); diff --git a/src/components/Dashboard/NewReport/types.ts b/src/components/Dashboard/NewReport/types.ts new file mode 100644 index 000000000..ab5638233 --- /dev/null +++ b/src/components/Dashboard/NewReport/types.ts @@ -0,0 +1,25 @@ +export interface ReportFilterQuery { + environmentId: string | null; + services: string[]; + scope?: string; +} + +export interface GetServicesPayload { + environment: string | null; +} + +export interface ReportFilterQuery { + environmentId: string | null; + services: string[]; +} + +export interface ServiceData { + name: string; + criticalIssuesCount: number; + impactScore: string; + totalIssuesCount: number; +} + +export interface ServiceDashboardsData { + data: ServiceData[]; +} diff --git a/src/components/Dashboard/NewReport/useReportsData.ts b/src/components/Dashboard/NewReport/useReportsData.ts new file mode 100644 index 000000000..ae1467313 --- /dev/null +++ b/src/components/Dashboard/NewReport/useReportsData.ts @@ -0,0 +1,35 @@ +import { useEffect, useMemo } from "react"; +import { + DataFetcherConfiguration, + useFetchData +} from "../../../hooks/useFetchData"; +import { actions } from "../actions"; +import { ReportFilterQuery, ServiceDashboardsData } from "./types"; + +const baseFetchConfig = { + refreshWithInterval: false, + refreshOnPayloadChange: true +}; + +const dataFetcherIssuesStatsConfiguration: DataFetcherConfiguration = { + requestAction: actions.GET_NEW_REPORT_DATA, + responseAction: actions.SET_NEW_REPORT_DATA, + ...baseFetchConfig +}; + +export const useReportsData = (query: ReportFilterQuery) => { + const payload = useMemo(() => query, [query]); + + const { data, getData } = useFetchData< + ReportFilterQuery, + ServiceDashboardsData + >(dataFetcherIssuesStatsConfiguration, payload); + + useEffect(() => { + getData(); + }, []); + + return { + data + }; +}; diff --git a/src/components/Dashboard/actions.ts b/src/components/Dashboard/actions.ts index 80d0d25db..49e519483 100644 --- a/src/components/Dashboard/actions.ts +++ b/src/components/Dashboard/actions.ts @@ -13,5 +13,7 @@ export const actions = addPrefix(ACTION_PREFIX, { GET_REPORT_ISSUES_STATS: "GET_REPORT_ISSUES_STATS", SET_REPORT_ISSUES_STATS: "SET_REPORT_ISSUES_STATS", GET_REPORT_ASSETS_STATS: "GET_REPORT_ASSETS_STATS", - SET_REPORT_ASSETS_STATS: "SET_REPORT_ASSETS_STATS" + SET_REPORT_ASSETS_STATS: "SET_REPORT_ASSETS_STATS", + GET_NEW_REPORT_DATA: "GET_NEW_REPORT_DATA", + SET_NEW_REPORT_DATA: "SET_NEW_REPORT_DATA" }); From f90df8996a93b40ff392a9f15b71acb9aa502716 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Mon, 16 Sep 2024 17:33:55 +0300 Subject: [PATCH 02/26] Added icons and styled toggles --- .../NewReport/ReportHeader/index.tsx | 125 ++++++++++++------ .../NewReport/ReportHeader/styles.ts | 3 +- .../Dashboard/NewReport/ReportHeader/types.ts | 4 + .../NewReport/Toggle/Toggle.stories.tsx | 28 ---- .../Dashboard/NewReport/Toggle/index.tsx | 26 ---- .../Dashboard/NewReport/Toggle/styles.ts | 41 ------ .../Dashboard/NewReport/Toggle/types.ts | 19 --- src/components/Dashboard/NewReport/styles.ts | 36 +++++ src/components/Navigation/KebabMenu/index.tsx | 30 ++--- .../common/icons/12px/MetricsIcon.tsx | 20 +++ .../common/icons/16px/TableIcon.tsx | 20 +++ .../common/icons/16px/TreemapIcon.tsx | 19 +++ src/components/common/v3/Toggle/index.tsx | 10 +- src/components/common/v3/Toggle/types.ts | 1 + 14 files changed, 204 insertions(+), 178 deletions(-) delete mode 100644 src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx delete mode 100644 src/components/Dashboard/NewReport/Toggle/index.tsx delete mode 100644 src/components/Dashboard/NewReport/Toggle/styles.ts delete mode 100644 src/components/Dashboard/NewReport/Toggle/types.ts create mode 100644 src/components/common/icons/12px/MetricsIcon.tsx create mode 100644 src/components/common/icons/16px/TableIcon.tsx create mode 100644 src/components/common/icons/16px/TreemapIcon.tsx diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 222e59337..72489ea5c 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -6,11 +6,15 @@ import { import { useConfigSelector } from "../../../../store/config/useConfigSelector"; import { GlobeIcon } from "../../../common/icons/12px/GlobeIcon"; import { WrenchIcon } from "../../../common/icons/12px/WrenchIcon"; + import { actions } from "../../actions"; -import { Toggle } from "../Toggle"; + +import { TableIcon } from "../../../common/icons/16px/TableIcon"; +import { TreemapIcon } from "../../../common/icons/16px/TreemapIcon"; +import { TimeModeToggle, ViewModeToggle } from "../styles"; import { GetServicesPayload } from "../types"; import * as s from "./styles"; -import { ReportHeaderProps } from "./types"; +import { ReportHeaderProps, ReportTimeMode, ReportViewMode } from "./types"; const baseFetchConfig = { refreshWithInterval: false, @@ -23,7 +27,13 @@ const dataFetcherFiltersConfiguration: DataFetcherConfiguration = { ...baseFetchConfig }; -export const ReportHeader = ({ onFilterChanged }: ReportHeaderProps) => { +export const ReportHeader = ({ + onFilterChanged, + onViewModeChanged, + onTimeModeChanged +}: ReportHeaderProps) => { + const [viewMode, setVieMode] = useState("table"); + const [timeMode, setTimeMode] = useState("baseline"); const [selectedServices, setSelectedServices] = useState([]); const [selectedEnvironment, setSelectedEnvironment] = useState( null @@ -61,58 +71,85 @@ export const ReportHeader = ({ onFilterChanged }: ReportHeaderProps) => { onFilterChanged({ environmentId: selectedEnvironment, services: newItem }); }; + const handleViewModeChanged = (value: string) => { + const newMode = value as ReportViewMode; + setVieMode(newMode); + onViewModeChanged(newMode); + }; + + const handleTimeModeChanged = (value: string) => { + const newMode = value as ReportTimeMode; + setTimeMode(newMode); + onTimeModeChanged(newMode); + }; + return ( Services with critical issues - { - // - }} + value={timeMode} + onValueChange={handleTimeModeChanged} /> - - a.name.localeCompare(b.name)) - .map((x) => ({ - label: x.name, - value: x.id, + + + a.name.localeCompare(b.name)) + .map((x) => ({ + label: x.name, + value: x.id, + enabled: true, + selected: x.id === selectedEnvironment + })) ?? [] + } + icon={GlobeIcon} + onChange={handleSelectedEnvironmentChanged} + placeholder={ + environments?.find((x) => x.id === selectedEnvironment)?.name ?? + "Select Environments" + } + /> + + ({ + label: service, + value: service, enabled: true, - selected: x.id === selectedEnvironment + selected: selectedServices.includes(service) })) ?? [] - } - icon={GlobeIcon} - onChange={handleSelectedEnvironmentChanged} - placeholder={ - environments?.find((x) => x.id === selectedEnvironment)?.name ?? - "Select Environments" - } - /> - - ({ - label: service, - value: service, - enabled: true, - selected: selectedServices.includes(service) - })) ?? [] - } - multiselect={true} - icon={WrenchIcon} - onChange={handleSelectedServicesChanged} - placeholder={ - selectedServices.length > 0 ? "Services" : "All Services" - } + } + multiselect={true} + icon={WrenchIcon} + onChange={handleSelectedServicesChanged} + placeholder={ + selectedServices.length > 0 ? "Services" : "All Services" + } + /> + + + }, + { + value: "treemap", + icon: (props) => + } + ]} + value={viewMode} + onValueChange={handleViewModeChanged} /> - + ); }; diff --git a/src/components/Dashboard/NewReport/ReportHeader/styles.ts b/src/components/Dashboard/NewReport/ReportHeader/styles.ts index 50273d06f..c49d7f0f3 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/styles.ts +++ b/src/components/Dashboard/NewReport/ReportHeader/styles.ts @@ -28,8 +28,7 @@ export const FilterSelect = styled(Select)` background: transparent; `; -export const FilterRow = styled(Row)` +export const Filters = styled(Row)` display: flex; - justify-content: start; gap: 18px; `; diff --git a/src/components/Dashboard/NewReport/ReportHeader/types.ts b/src/components/Dashboard/NewReport/ReportHeader/types.ts index 2bc89b9b5..a87d09fd0 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/types.ts +++ b/src/components/Dashboard/NewReport/ReportHeader/types.ts @@ -4,6 +4,10 @@ export interface GetServicesPayload { environment: string | null; } +export type ReportViewMode = "treemap" | "table"; +export type ReportTimeMode = "baseline" | "changes"; export interface ReportHeaderProps { onFilterChanged: (query: ReportFilterQuery) => void; + onViewModeChanged: (viewMode: ReportViewMode) => void; + onTimeModeChanged: (viewMode: ReportTimeMode) => void; } diff --git a/src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx b/src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx deleted file mode 100644 index 37f8d950a..000000000 --- a/src/components/Dashboard/NewReport/Toggle/Toggle.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import { Toggle } from "."; - -// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction -const meta: Meta = { - title: "Dashboard/NewReport/Toggle", - component: Toggle, - 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: { - options: [ - { value: "Baseline", label: "Baseline" }, - { value: "Changes", label: "Changes" } - ], - value: "Baseline" - } -}; diff --git a/src/components/Dashboard/NewReport/Toggle/index.tsx b/src/components/Dashboard/NewReport/Toggle/index.tsx deleted file mode 100644 index 9472c9801..000000000 --- a/src/components/Dashboard/NewReport/Toggle/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as s from "./styles"; -import { ToggleProps, ToggleValue } from "./types"; - -export const Toggle = ({ - options, - value, - onValueChange -}: ToggleProps) => { - const handleOptionButtonClick = (value: T) => { - onValueChange(value); - }; - - return ( - - {options.map((option) => ( - handleOptionButtonClick(option.value)} - > - {option.label} - - ))} - - ); -}; diff --git a/src/components/Dashboard/NewReport/Toggle/styles.ts b/src/components/Dashboard/NewReport/Toggle/styles.ts deleted file mode 100644 index 23e835596..000000000 --- a/src/components/Dashboard/NewReport/Toggle/styles.ts +++ /dev/null @@ -1,41 +0,0 @@ -import styled from "styled-components"; - -import { subscriptRegularTypography } from "../../../common/App/typographies"; -import { OptionButtonProps } from "./types"; - -export const Container = styled.div` - display: flex; - border-radius: 8px; - height: 36px; - width: fit-content; - border: 1px solid ${({ theme }) => theme.colors.v3.stroke.dark}; - background: ${({ theme }) => theme.colors.v3.surface.secondary}; -`; - -export const OptionButton = styled.button` - ${subscriptRegularTypography} - - display: flex; - font-family: inherit; - border: none; - outline: none; - cursor: pointer; - user-select: none; - border-radius: 8px; - align-items: center; - padding: 0 16px; - color: ${({ theme, $selected }) => - $selected ? theme.colors.v3.text.primary : theme.colors.v3.text.secondary}; - background: ${({ theme, $selected }) => - $selected ? theme.colors.v3.surface.brandPrimary : "transparent"}; - - &:first-child { - border-bottom-right-radius: 0; - border-top-right-radius: 0; - } - - &:last-child { - border-bottom-left-radius: 0; - border-top-left-radius: 0; - } -`; diff --git a/src/components/Dashboard/NewReport/Toggle/types.ts b/src/components/Dashboard/NewReport/Toggle/types.ts deleted file mode 100644 index de145fa85..000000000 --- a/src/components/Dashboard/NewReport/Toggle/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ReactNode } from "react"; - -export type ToggleValue = string | number; -export type ToggleSize = "small" | "large"; - -export interface ToggleOption { - value: T; - label?: string | ReactNode; -} - -export interface ToggleProps { - options: ToggleOption[]; - onValueChange: (value: T) => void; - value: T; -} - -export interface OptionButtonProps { - $selected: boolean; -} diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts index 438eb3bb5..3eb2decc4 100644 --- a/src/components/Dashboard/NewReport/styles.ts +++ b/src/components/Dashboard/NewReport/styles.ts @@ -1,4 +1,6 @@ import styled from "styled-components"; +import { Toggle } from "../../common/v3/Toggle"; +import { OptionButton } from "../../common/v3/Toggle/styles"; export const Container = styled.div` display: flex; @@ -25,3 +27,37 @@ export const Column = styled.div` border-radius: 12px; width: 100%; `; + +export const ViewModeToggle = styled(Toggle)` + border-radius: 8px; + padding: 6px; + align-items: center; + background-color: transparent; + border-color: ${({ theme }) => theme.colors.v3.stroke.dark}; + + ${OptionButton} { + padding: 6px; + } +`; + +export const TimeModeToggle = styled(Toggle)` + border-radius: 8px; + padding: 0; + align-items: center; + background-color: transparent; + border-color: ${({ theme }) => theme.colors.v3.stroke.dark}; + + ${OptionButton} { + padding: 10px 16px; + + &:first-child { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + + &:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + } +`; diff --git a/src/components/Navigation/KebabMenu/index.tsx b/src/components/Navigation/KebabMenu/index.tsx index 36a301020..199edc35f 100644 --- a/src/components/Navigation/KebabMenu/index.tsx +++ b/src/components/Navigation/KebabMenu/index.tsx @@ -5,6 +5,7 @@ import { OpenInstallationWizardPayload } from "../../../types"; import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { isDigmaEngineRunning } from "../../../utils/isDigmaEngineRunning"; +import { MetricsIcon } from "../../common/icons/12px/MetricsIcon"; import { BookIcon } from "../../common/icons/16px/BookIcon"; import { DigmaLogoFlatIcon } from "../../common/icons/16px/DigmaLogoFlatIcon"; import { FourPointedStarIcon } from "../../common/icons/16px/FourPointedStarIcon"; @@ -72,12 +73,13 @@ export const KebabMenu = ({ onClose }: KebabMenuProps) => { onClose(); }; - // const handleReportClick = () => { - // window.sendMessageToDigma({ - // action: globalActions.OPEN_REPORT - // }); - // onClose(); - // }; + const handleReportClick = () => { + window.sendMessageToDigma({ + action: globalActions.OPEN_REPORT + }); + + onClose(); + }; const handleLogoutClick = () => { sendUserActionTrackingEvent(trackingEvents.LOGOUT_CLICKED); @@ -120,17 +122,15 @@ export const KebabMenu = ({ onClose }: KebabMenuProps) => { icon: , onClick: handleDashboardClick }); - - // if (getFeatureFlagValue(backendInfo, FeatureFlag.IS_REPORT_ENABLED)) { - // items.push({ - // id: "report", - // label: "Open Report", - // icon: , - // onClick: handleReportClick - // }); - // } } + items.push({ + id: "metrics", + label: "Digma Metrics", + icon: , + onClick: handleReportClick + }); + items.push({ id: "digma_docs", label: "Digma Docs", diff --git a/src/components/common/icons/12px/MetricsIcon.tsx b/src/components/common/icons/12px/MetricsIcon.tsx new file mode 100644 index 000000000..c1b0dc661 --- /dev/null +++ b/src/components/common/icons/12px/MetricsIcon.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const MetricsIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + ); +}; + +export const MetricsIcon = React.memo(MetricsIconComponent); diff --git a/src/components/common/icons/16px/TableIcon.tsx b/src/components/common/icons/16px/TableIcon.tsx new file mode 100644 index 000000000..fe4ccbf43 --- /dev/null +++ b/src/components/common/icons/16px/TableIcon.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const TableIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + ); +}; + +export const TableIcon = React.memo(TableIconComponent); diff --git a/src/components/common/icons/16px/TreemapIcon.tsx b/src/components/common/icons/16px/TreemapIcon.tsx new file mode 100644 index 000000000..76988fff7 --- /dev/null +++ b/src/components/common/icons/16px/TreemapIcon.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const TreemapIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + + + + + ); +}; + +export const TreemapIcon = React.memo(TreemapIconComponent); diff --git a/src/components/common/v3/Toggle/index.tsx b/src/components/common/v3/Toggle/index.tsx index d70becd2c..5fdf78913 100644 --- a/src/components/common/v3/Toggle/index.tsx +++ b/src/components/common/v3/Toggle/index.tsx @@ -1,18 +1,20 @@ +import { forwardRef } from "react"; import * as s from "./styles"; import { ToggleProps, ToggleValue } from "./types"; -export const Toggle = ({ +const ToggleComponent = ({ size = "large", options, value, - onValueChange + onValueChange, + className }: ToggleProps) => { const handleOptionButtonClick = (value: T) => { onValueChange(value); }; return ( - + {options.map((option) => ( ({ ); }; + +export const Toggle = forwardRef(ToggleComponent) as typeof ToggleComponent; diff --git a/src/components/common/v3/Toggle/types.ts b/src/components/common/v3/Toggle/types.ts index 815c76cf5..6bb92ee8f 100644 --- a/src/components/common/v3/Toggle/types.ts +++ b/src/components/common/v3/Toggle/types.ts @@ -15,6 +15,7 @@ export interface ToggleProps { onValueChange: (value: T) => void; value: T; size?: ToggleSize; + className?: string; } export interface OptionButtonProps { From 3d8d7eef12ea9cc0137bfe02b4d05ad52d1eb0c6 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 00:54:29 +0300 Subject: [PATCH 03/26] Basic template --- .../MetricsTable/MetricsTable.stories.tsx | 32 +++++++ .../NewReport/MetricsTable/index.tsx | 92 +++++++++++++++++++ .../NewReport/MetricsTable/mockData.ts | 69 ++++++++++++++ .../NewReport/MetricsTable/styles.ts | 79 ++++++++++++++++ .../Dashboard/NewReport/MetricsTable/types.ts | 16 ++++ .../Dashboard/NewReport/NewReport.stories.tsx | 71 +------------- .../ReportHeader/ReportHeader.stories.tsx | 5 + .../NewReport/ReportHeader/index.tsx | 75 +++++++++++++-- .../NewReport/ReportHeader/styles.ts | 42 ++++++++- .../Dashboard/NewReport/ReportHeader/types.ts | 1 - src/components/Dashboard/NewReport/index.tsx | 40 +++++--- src/components/Dashboard/NewReport/styles.ts | 36 -------- src/components/Dashboard/NewReport/types.ts | 24 +++-- .../Dashboard/NewReport/useReportsData.ts | 42 +++++++-- src/components/Dashboard/actions.ts | 4 +- src/components/common/v3/Select/styles.ts | 4 +- 16 files changed, 484 insertions(+), 148 deletions(-) create mode 100644 src/components/Dashboard/NewReport/MetricsTable/MetricsTable.stories.tsx create mode 100644 src/components/Dashboard/NewReport/MetricsTable/index.tsx create mode 100644 src/components/Dashboard/NewReport/MetricsTable/mockData.ts create mode 100644 src/components/Dashboard/NewReport/MetricsTable/styles.ts create mode 100644 src/components/Dashboard/NewReport/MetricsTable/types.ts diff --git a/src/components/Dashboard/NewReport/MetricsTable/MetricsTable.stories.tsx b/src/components/Dashboard/NewReport/MetricsTable/MetricsTable.stories.tsx new file mode 100644 index 000000000..1e020e715 --- /dev/null +++ b/src/components/Dashboard/NewReport/MetricsTable/MetricsTable.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { MetricsTable } from "."; +import { mockedReport } from "./mockData"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Dashboard/NewReport/MetricsTable", + component: MetricsTable, + 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: mockedReport.reports + } +}; + +export const Empty: Story = { + args: { + data: [] + } +}; diff --git a/src/components/Dashboard/NewReport/MetricsTable/index.tsx b/src/components/Dashboard/NewReport/MetricsTable/index.tsx new file mode 100644 index 000000000..e67f6f652 --- /dev/null +++ b/src/components/Dashboard/NewReport/MetricsTable/index.tsx @@ -0,0 +1,92 @@ +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable +} from "@tanstack/react-table"; +import { ServiceData } from "../types"; +import * as s from "./styles"; +import { ColumnMeta, MetricsTableProps } from "./types"; + +export const MetricsTable = ({ data }: MetricsTableProps) => { + const columnHelper = createColumnHelper(); + const columns = [ + columnHelper.accessor((row) => row.key.service, { + header: "Service", + cell: (info) => info.getValue() + }), + columnHelper.accessor((row) => row.key.environment, { + header: "Environment", + cell: (info) => info.getValue(), + meta: { + contentAlign: "center" + } + }), + columnHelper.accessor((row) => row.issues, { + header: "Issues", + cell: (info) => info.getValue(), + meta: { + contentAlign: "center" + } + }), + columnHelper.accessor((row) => row.impact, { + header: "Impact", + cell: (info) => info.getValue(), + meta: { + contentAlign: "center" + } + }) + ]; + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel() + }); + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const meta = header.column.columnDef.meta as + | ColumnMeta + | undefined; + + return ( + + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const meta = cell.column.columnDef.meta as ColumnMeta | undefined; + + return ( + + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + + ); + })} + + ))} + + + ); +}; diff --git a/src/components/Dashboard/NewReport/MetricsTable/mockData.ts b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts new file mode 100644 index 000000000..11304e61b --- /dev/null +++ b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts @@ -0,0 +1,69 @@ +import { ServiceMetricsReport } from "../types"; + +export const mockedReport: ServiceMetricsReport = { + reports: [ + { + impact: 100, + key: { + environment: "TEST", + service: "Transactions", + lastDays: null + }, + issues: 10 + }, + { + impact: 10, + key: { + environment: "TEST", + service: "API", + lastDays: null + }, + issues: 10 + }, + { + impact: 101, + key: { + environment: "TEST", + service: "Orders", + lastDays: null + }, + issues: 30 + }, + { + impact: 120, + key: { + environment: "TEST", + service: "Users", + lastDays: null + }, + issues: 110 + }, + { + impact: 120, + key: { + environment: "TEST", + service: "Users", + lastDays: null + }, + issues: 110 + }, + { + impact: 120, + key: { + environment: "TEST", + service: "Users", + lastDays: null + }, + issues: 110 + }, + { + impact: 120, + key: { + environment: "TEST", + service: "Users", + lastDays: null + }, + issues: 110 + } + ] +}; diff --git a/src/components/Dashboard/NewReport/MetricsTable/styles.ts b/src/components/Dashboard/NewReport/MetricsTable/styles.ts new file mode 100644 index 000000000..78298fbac --- /dev/null +++ b/src/components/Dashboard/NewReport/MetricsTable/styles.ts @@ -0,0 +1,79 @@ +import styled from "styled-components"; +import { + footnoteRegularTypography, + subscriptRegularTypography +} from "../../../common/App/typographies"; +import { TableCellContentProps } from "./types"; + +export const Table = styled.table` + width: 100%; + table-layout: fixed; + border-spacing: 0 4px; +`; + +export const TableHead = styled.thead` + color: ${({ theme }) => theme.colors.v3.text.secondary}; + padding-bottom: 4px; +`; + +export const TableHeaderCell = styled.th` + ${footnoteRegularTypography} + + padding: 0 2px; + vertical-align: top; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } +`; + +export const TableCellContent = styled.div` + display: flex; + gap: 4px; + align-items: center; + text-align: ${({ $align = "left" }) => $align}; + justify-content: ${({ $align }) => { + switch ($align) { + case "right": + return "flex-end"; + case "center": + return "center"; + case "left": + default: + return "flex-start"; + } + }}; +`; + +export const TableHeaderCellContent = styled(TableCellContent)` + ${footnoteRegularTypography} +`; + +export const TableBodyRow = styled.tr` + ${subscriptRegularTypography} + color: ${({ theme }) => theme.colors.v3.text.primary}; + height: 28px; + + &:hover { + background: ${({ theme }) => theme.colors.v3.surface.primaryLight}; + cursor: pointer; + } +`; + +export const TableBodyCell = styled.td` + padding: 0 2px; + + &:first-child { + padding-left: 0; + border-radius: 4px 0 0 4px; + } + + &:last-child { + padding-right: 0; + border-radius: 0 4px 4px 0; + } +`; diff --git a/src/components/Dashboard/NewReport/MetricsTable/types.ts b/src/components/Dashboard/NewReport/MetricsTable/types.ts new file mode 100644 index 000000000..d1e4423d4 --- /dev/null +++ b/src/components/Dashboard/NewReport/MetricsTable/types.ts @@ -0,0 +1,16 @@ +import { ServiceData } from "../types"; + +export interface MetricsTableProps { + data: ServiceData[]; +} + +export type ContentAlignment = "left" | "center" | "right"; + +export interface ColumnMeta { + contentAlign?: ContentAlignment; + info?: string; +} + +export interface TableCellContentProps { + $align?: ContentAlignment; +} diff --git a/src/components/Dashboard/NewReport/NewReport.stories.tsx b/src/components/Dashboard/NewReport/NewReport.stories.tsx index 968aadb50..5da162e7e 100644 --- a/src/components/Dashboard/NewReport/NewReport.stories.tsx +++ b/src/components/Dashboard/NewReport/NewReport.stories.tsx @@ -1,8 +1,8 @@ import { Meta, StoryObj } from "@storybook/react"; import { NewReport } from "."; -import { actions as globalActions } from "../../../actions"; import { actions } from "../actions"; +import { mockedReport } from "./MetricsTable/mockData"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta: Meta = { @@ -24,54 +24,8 @@ export const Default: Story = { window.setTimeout(() => { window.postMessage({ type: "digma", - action: actions.SET_REPORT_ASSETS_STATS, - payload: { - totalCount: 10, - data: [ - { - name: "Endpoint", - count: 1 - }, - { - name: "DatabaseQueries", - count: 2 - }, - { - name: "Consumer", - count: 3 - }, - { - name: "EndpointClient", - count: 4 - }, - { - name: "CodeLocation", - count: 5 - }, - { - name: "Cache", - count: 6 - }, - { - name: "Other", - count: 7 - } - ] - } - }); - }, 500); - - window.setTimeout(() => { - window.postMessage({ - type: "digma", - action: actions.SET_REPORT_ISSUES_STATS, - payload: { - totalCount: 10, - fixedCount: 9, - activeCount: 8, - regressionCount: 7, - criticalCount: 6 - } + action: actions.SET_METRICS_REPORT_DATA, + payload: { ...mockedReport } }); }, 500); @@ -82,24 +36,5 @@ export const Default: Story = { payload: ["service 1", "service 2", "service 3", "service 4"] }); }, 500); - - window.setTimeout(() => { - window.postMessage({ - type: "digma", - action: globalActions.SET_ENVIRONMENTS, - payload: [ - { - id: "test1", - name: "test1", - type: "Public" - }, - { - id: "test2", - name: "test2", - type: "Public" - } - ] - }); - }, 500); } }; diff --git a/src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx b/src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx index cb5b357b3..abba7c2d5 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/ReportHeader.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; import { ReportHeader } from "."; import { actions } from "../../actions"; @@ -19,6 +20,10 @@ type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args export const Default: Story = { + args: { + onFilterChanged: fn(), + onViewModeChanged: fn() + }, play: () => { window.setTimeout(() => { window.postMessage({ diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 72489ea5c..5b309c7c4 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -9,9 +9,11 @@ import { WrenchIcon } from "../../../common/icons/12px/WrenchIcon"; import { actions } from "../../actions"; +import { isNumber } from "highcharts"; +import { usePrevious } from "../../../../hooks/usePrevious"; +import { formatUnit } from "../../../../utils/formatUnit"; import { TableIcon } from "../../../common/icons/16px/TableIcon"; import { TreemapIcon } from "../../../common/icons/16px/TreemapIcon"; -import { TimeModeToggle, ViewModeToggle } from "../styles"; import { GetServicesPayload } from "../types"; import * as s from "./styles"; import { ReportHeaderProps, ReportTimeMode, ReportViewMode } from "./types"; @@ -27,11 +29,13 @@ const dataFetcherFiltersConfiguration: DataFetcherConfiguration = { ...baseFetchConfig }; +const DEFAULT_PERIOD = 1; + export const ReportHeader = ({ onFilterChanged, - onViewModeChanged, - onTimeModeChanged + onViewModeChanged }: ReportHeaderProps) => { + const [periodInDays, setPeriodInDays] = useState(DEFAULT_PERIOD); const [viewMode, setVieMode] = useState("table"); const [timeMode, setTimeMode] = useState("baseline"); const [selectedServices, setSelectedServices] = useState([]); @@ -39,6 +43,10 @@ export const ReportHeader = ({ null ); const { environments } = useConfigSelector(); + const previousServices = usePrevious(selectedServices); + const previousEnvironment = usePrevious(selectedEnvironment); + const previousTimeMode = usePrevious(timeMode); + const previousPeriod = usePrevious(periodInDays); const getServicesPayload = useMemo( () => ({ environment: selectedEnvironment }), @@ -54,6 +62,31 @@ export const ReportHeader = ({ getData(); }, []); + useEffect(() => { + if ( + previousEnvironment !== selectedEnvironment || + previousServices !== previousServices || + previousTimeMode !== timeMode || + previousPeriod !== periodInDays + ) { + onFilterChanged({ + lastDays: timeMode === "baseline" ? null : periodInDays, + services: selectedServices, + environmentId: selectedEnvironment + }); + } + }, [ + periodInDays, + timeMode, + selectedServices, + selectedEnvironment, + onFilterChanged, + previousEnvironment, + previousServices, + previousTimeMode, + previousPeriod + ]); + const handleSelectedEnvironmentChanged = (option: string | string[]) => { const newItem = option === selectedEnvironment @@ -68,7 +101,18 @@ export const ReportHeader = ({ const handleSelectedServicesChanged = (option: string | string[]) => { const newItem = Array.isArray(option) ? option : [option]; setSelectedServices(newItem); - onFilterChanged({ environmentId: selectedEnvironment, services: newItem }); + }; + + const handlePeriodChanged = (option: string | string[]) => { + const newItem = Array.isArray(option) ? option : [option]; + if (newItem.length === 0) { + setPeriodInDays(DEFAULT_PERIOD); + return; + } + + const value = newItem[0]; + const newValue = isNumber(value) ? value : DEFAULT_PERIOD; + setPeriodInDays(newValue as number); }; const handleViewModeChanged = (value: string) => { @@ -80,14 +124,13 @@ export const ReportHeader = ({ const handleTimeModeChanged = (value: string) => { const newMode = value as ReportTimeMode; setTimeMode(newMode); - onTimeModeChanged(newMode); }; return ( Services with critical issues - - a.name.localeCompare(b.name)) @@ -109,6 +152,7 @@ export const ReportHeader = ({ selected: x.id === selectedEnvironment })) ?? [] } + showSelectedState={true} icon={GlobeIcon} onChange={handleSelectedEnvironmentChanged} placeholder={ @@ -117,6 +161,20 @@ export const ReportHeader = ({ } /> + {timeMode === "changes" && ( + ({ + value: x.toString(), + label: formatUnit(x, "Day"), + selected: x === periodInDays + }))} + showSelectedState={true} + icon={WrenchIcon} + onChange={handlePeriodChanged} + placeholder={`Period: ${formatUnit(periodInDays, "day")}`} + /> + )} + ({ @@ -126,6 +184,7 @@ export const ReportHeader = ({ selected: selectedServices.includes(service) })) ?? [] } + showSelectedState={true} multiselect={true} icon={WrenchIcon} onChange={handleSelectedServicesChanged} @@ -134,7 +193,7 @@ export const ReportHeader = ({ } /> - theme.colors.v3.stroke.primaryLight}; +`; + +export const ViewModeToggle = styled(StyledToggle)` + padding: 6px; + + ${OptionButton} { + padding: 6px; + } +`; + +export const TimeModeToggle = styled(StyledToggle)` + padding: 0; + + ${OptionButton} { + padding: 10px 16px; + + &:first-child { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + + &:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + } +`; diff --git a/src/components/Dashboard/NewReport/ReportHeader/types.ts b/src/components/Dashboard/NewReport/ReportHeader/types.ts index a87d09fd0..9a128d144 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/types.ts +++ b/src/components/Dashboard/NewReport/ReportHeader/types.ts @@ -9,5 +9,4 @@ export type ReportTimeMode = "baseline" | "changes"; export interface ReportHeaderProps { onFilterChanged: (query: ReportFilterQuery) => void; onViewModeChanged: (viewMode: ReportViewMode) => void; - onTimeModeChanged: (viewMode: ReportTimeMode) => void; } diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index d6caf0672..8e63cf5bc 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -1,22 +1,28 @@ -import { useLayoutEffect } from "react"; +import { useLayoutEffect, useState } from "react"; import { actions } from "../actions"; import { Chart } from "./Chart"; +import { MetricsTable } from "./MetricsTable"; import { ReportHeader } from "./ReportHeader"; +import { ReportViewMode } from "./ReportHeader/types"; import * as s from "./styles"; +import { ReportFilterQuery } from "./types"; +import { useReportsData } from "./useReportsData"; -// const DefaultQuery: ReportFilterQuery = { -// environmentId: "", -// services: [] -// }; +const DefaultQuery: ReportFilterQuery = { + environmentId: "", + services: [], + lastDays: null +}; export const NewReport = ({ type }: { type: "squarified" | "stripes" | "strip" | "sliceAndDice"; }) => { - // const [query, setQuery] = useState(DefaultQuery); - // const { data } = useReportsData(query); + const [query, setQuery] = useState(DefaultQuery); + const { data } = useReportsData(query); + const [viewMode, setViewMode] = useState("table"); useLayoutEffect(() => { window.sendMessageToDigma({ @@ -24,16 +30,22 @@ export const NewReport = ({ }); }, []); - const handleFilterChanged = () => - // query: ReportFilterQuery - { - // setQuery(query); - }; + const handleFilterChanged = (query: ReportFilterQuery) => { + setQuery(query); + }; + + const handleViewModeChange = (value: ReportViewMode) => { + setViewMode(value); + }; return ( - - + + {viewMode === "table" && } + {viewMode === "treemap" && } ); }; diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts index 3eb2decc4..438eb3bb5 100644 --- a/src/components/Dashboard/NewReport/styles.ts +++ b/src/components/Dashboard/NewReport/styles.ts @@ -1,6 +1,4 @@ import styled from "styled-components"; -import { Toggle } from "../../common/v3/Toggle"; -import { OptionButton } from "../../common/v3/Toggle/styles"; export const Container = styled.div` display: flex; @@ -27,37 +25,3 @@ export const Column = styled.div` border-radius: 12px; width: 100%; `; - -export const ViewModeToggle = styled(Toggle)` - border-radius: 8px; - padding: 6px; - align-items: center; - background-color: transparent; - border-color: ${({ theme }) => theme.colors.v3.stroke.dark}; - - ${OptionButton} { - padding: 6px; - } -`; - -export const TimeModeToggle = styled(Toggle)` - border-radius: 8px; - padding: 0; - align-items: center; - background-color: transparent; - border-color: ${({ theme }) => theme.colors.v3.stroke.dark}; - - ${OptionButton} { - padding: 10px 16px; - - &:first-child { - border-bottom-right-radius: 0; - border-top-right-radius: 0; - } - - &:last-child { - border-bottom-left-radius: 0; - border-top-left-radius: 0; - } - } -`; diff --git a/src/components/Dashboard/NewReport/types.ts b/src/components/Dashboard/NewReport/types.ts index ab5638233..fe1126341 100644 --- a/src/components/Dashboard/NewReport/types.ts +++ b/src/components/Dashboard/NewReport/types.ts @@ -4,6 +4,14 @@ export interface ReportFilterQuery { scope?: string; } +export interface ReportQuery { + keys: { + environment: string | null; + service: string | null; + lastDays: number | null; + }[]; +} + export interface GetServicesPayload { environment: string | null; } @@ -11,15 +19,19 @@ export interface GetServicesPayload { export interface ReportFilterQuery { environmentId: string | null; services: string[]; + lastDays: number | null; } export interface ServiceData { - name: string; - criticalIssuesCount: number; - impactScore: string; - totalIssuesCount: number; + key: { + environment: string; + service: string; + lastDays: number | null; + }; + issues: number; + impact: number; } -export interface ServiceDashboardsData { - data: ServiceData[]; +export interface ServiceMetricsReport { + reports: ServiceData[]; } diff --git a/src/components/Dashboard/NewReport/useReportsData.ts b/src/components/Dashboard/NewReport/useReportsData.ts index ae1467313..004a99545 100644 --- a/src/components/Dashboard/NewReport/useReportsData.ts +++ b/src/components/Dashboard/NewReport/useReportsData.ts @@ -4,7 +4,7 @@ import { useFetchData } from "../../../hooks/useFetchData"; import { actions } from "../actions"; -import { ReportFilterQuery, ServiceDashboardsData } from "./types"; +import { ReportFilterQuery, ReportQuery, ServiceMetricsReport } from "./types"; const baseFetchConfig = { refreshWithInterval: false, @@ -12,18 +12,44 @@ const baseFetchConfig = { }; const dataFetcherIssuesStatsConfiguration: DataFetcherConfiguration = { - requestAction: actions.GET_NEW_REPORT_DATA, - responseAction: actions.SET_NEW_REPORT_DATA, + requestAction: actions.GET_METRICS_REPORT_DATA, + responseAction: actions.SET_METRICS_REPORT_DATA, ...baseFetchConfig }; export const useReportsData = (query: ReportFilterQuery) => { - const payload = useMemo(() => query, [query]); + const payload = useMemo(() => { + if (!query.environmentId && !(query.services?.length > 0)) { + return { + keys: [] + }; + } - const { data, getData } = useFetchData< - ReportFilterQuery, - ServiceDashboardsData - >(dataFetcherIssuesStatsConfiguration, payload); + if (!(query.services?.length > 0)) { + return { + keys: [ + { + environment: query.environmentId, + service: null, + lastDays: query.lastDays + } + ] + }; + } + + return { + keys: query.services.map((x) => ({ + environment: query.environmentId, + service: x, + lastDays: query.lastDays + })) + }; + }, [query]); + + const { data, getData } = useFetchData( + dataFetcherIssuesStatsConfiguration, + payload + ); useEffect(() => { getData(); diff --git a/src/components/Dashboard/actions.ts b/src/components/Dashboard/actions.ts index 49e519483..e9b95ee7f 100644 --- a/src/components/Dashboard/actions.ts +++ b/src/components/Dashboard/actions.ts @@ -14,6 +14,6 @@ export const actions = addPrefix(ACTION_PREFIX, { SET_REPORT_ISSUES_STATS: "SET_REPORT_ISSUES_STATS", GET_REPORT_ASSETS_STATS: "GET_REPORT_ASSETS_STATS", SET_REPORT_ASSETS_STATS: "SET_REPORT_ASSETS_STATS", - GET_NEW_REPORT_DATA: "GET_NEW_REPORT_DATA", - SET_NEW_REPORT_DATA: "SET_NEW_REPORT_DATA" + GET_METRICS_REPORT_DATA: "GET_METRICS_REPORT_DATA", + SET_METRICS_REPORT_DATA: "SET_METRICS_REPORT_DATA" }); diff --git a/src/components/common/v3/Select/styles.ts b/src/components/common/v3/Select/styles.ts index 1a7c0c609..38500d238 100644 --- a/src/components/common/v3/Select/styles.ts +++ b/src/components/common/v3/Select/styles.ts @@ -19,9 +19,7 @@ export const Button = styled.button` ? theme.colors.v3.stroke.brandPrimary : theme.colors.v3.stroke.primaryLight}; background: ${({ theme, $isActive }) => - $isActive - ? theme.colors.v3.surface.brandDark - : theme.colors.v3.surface.sidePanelHeader}; + $isActive ? theme.colors.v3.surface.brandDark : "transparent"}; border-radius: 4px; padding: 4px 8px; display: flex; From 7bcf3651fba557d828b4c3d6a5ebfbff184a6828 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 15:36:46 +0300 Subject: [PATCH 04/26] Added ranks --- .../NewReport/MetricsTable/index.tsx | 36 +++++++---- .../NewReport/MetricsTable/mockData.ts | 8 +-- .../NewReport/MetricsTable/styles.ts | 59 +++++++++---------- .../Dashboard/NewReport/MetricsTable/types.ts | 5 ++ .../NewReport/ReportHeader/index.tsx | 15 ++--- src/components/Dashboard/NewReport/index.tsx | 10 ++-- src/components/Dashboard/NewReport/utils.ts | 18 ++++++ src/containers/Dashboard/index.tsx | 4 +- 8 files changed, 92 insertions(+), 63 deletions(-) create mode 100644 src/components/Dashboard/NewReport/utils.ts diff --git a/src/components/Dashboard/NewReport/MetricsTable/index.tsx b/src/components/Dashboard/NewReport/MetricsTable/index.tsx index e67f6f652..8f876a419 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/index.tsx +++ b/src/components/Dashboard/NewReport/MetricsTable/index.tsx @@ -2,36 +2,45 @@ import { createColumnHelper, flexRender, getCoreRowModel, + sortingFns, useReactTable } from "@tanstack/react-table"; import { ServiceData } from "../types"; +import { getRank } from "../utils"; import * as s from "./styles"; -import { ColumnMeta, MetricsTableProps } from "./types"; +import { ColumnMeta, MetricsTableProps, Severity } from "./types"; export const MetricsTable = ({ data }: MetricsTableProps) => { const columnHelper = createColumnHelper(); + const maxImpact = Math.max(...data.map((x) => x.impact)); + const columns = [ columnHelper.accessor((row) => row.key.service, { header: "Service", cell: (info) => info.getValue() }), - columnHelper.accessor((row) => row.key.environment, { - header: "Environment", - cell: (info) => info.getValue(), + columnHelper.accessor((row) => row.issues, { + header: "Critical issues", + cell: (info) => (info.getValue() === 0 ? 0 : `+${info.getValue()}`), + sortingFn: sortingFns.alphanumeric, meta: { contentAlign: "center" } }), - columnHelper.accessor((row) => row.issues, { - header: "Issues", - cell: (info) => info.getValue(), + columnHelper.accessor((row) => row.impact, { + header: "Impact", + cell: (info) => info.getValue() * 100, + sortingFn: sortingFns.alphanumeric, meta: { contentAlign: "center" } }), - columnHelper.accessor((row) => row.impact, { - header: "Impact", - cell: (info) => info.getValue(), + columnHelper.accessor((row) => getRank(maxImpact, row.impact), { + header: "Rank", + cell: (info) => { + return info.getValue(); + }, + sortingFn: sortingFns.alphanumeric, meta: { contentAlign: "center" } @@ -75,9 +84,12 @@ export const MetricsTable = ({ data }: MetricsTableProps) => { {row.getVisibleCells().map((cell) => { const meta = cell.column.columnDef.meta as ColumnMeta | undefined; - + const severity = + cell.column.columnDef.header === "Rank" + ? (cell.getValue() as Severity) + : null; return ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/src/components/Dashboard/NewReport/MetricsTable/mockData.ts b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts index 11304e61b..d190949eb 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/mockData.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts @@ -12,7 +12,7 @@ export const mockedReport: ServiceMetricsReport = { issues: 10 }, { - impact: 10, + impact: 50, key: { environment: "TEST", service: "API", @@ -21,7 +21,7 @@ export const mockedReport: ServiceMetricsReport = { issues: 10 }, { - impact: 101, + impact: 1, key: { environment: "TEST", service: "Orders", @@ -48,13 +48,13 @@ export const mockedReport: ServiceMetricsReport = { issues: 110 }, { - impact: 120, + impact: 70, key: { environment: "TEST", service: "Users", lastDays: null }, - issues: 110 + issues: 70 }, { impact: 120, diff --git a/src/components/Dashboard/NewReport/MetricsTable/styles.ts b/src/components/Dashboard/NewReport/MetricsTable/styles.ts index 78298fbac..52297bdb7 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/styles.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/styles.ts @@ -1,14 +1,13 @@ import styled from "styled-components"; import { - footnoteRegularTypography, - subscriptRegularTypography + subheadingBoldTypography, + subheadingSemiboldTypography } from "../../../common/App/typographies"; -import { TableCellContentProps } from "./types"; +import { TableBodyCellCellProps, TableCellContentProps } from "./types"; export const Table = styled.table` width: 100%; - table-layout: fixed; - border-spacing: 0 4px; + border-spacing: 0; `; export const TableHead = styled.thead` @@ -17,23 +16,12 @@ export const TableHead = styled.thead` `; export const TableHeaderCell = styled.th` - ${footnoteRegularTypography} - - padding: 0 2px; vertical-align: top; - - &:first-child { - padding-left: 0; - } - - &:last-child { - padding-right: 0; - } `; export const TableCellContent = styled.div` display: flex; - gap: 4px; + padding: 16px; align-items: center; text-align: ${({ $align = "left" }) => $align}; justify-content: ${({ $align }) => { @@ -50,13 +38,16 @@ export const TableCellContent = styled.div` `; export const TableHeaderCellContent = styled(TableCellContent)` - ${footnoteRegularTypography} + ${subheadingSemiboldTypography} + font-weight: 400; + color: ${({ theme }) => theme.colors.v3.text.tertiary}; `; export const TableBodyRow = styled.tr` - ${subscriptRegularTypography} + ${subheadingBoldTypography} color: ${({ theme }) => theme.colors.v3.text.primary}; - height: 28px; + height: 38px; + border-spacing: 0; &:hover { background: ${({ theme }) => theme.colors.v3.surface.primaryLight}; @@ -64,16 +55,20 @@ export const TableBodyRow = styled.tr` } `; -export const TableBodyCell = styled.td` - padding: 0 2px; - - &:first-child { - padding-left: 0; - border-radius: 4px 0 0 4px; - } - - &:last-child { - padding-right: 0; - border-radius: 0 4px 4px 0; - } +export const TableBodyCell = styled.td` + border: 1px solid ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; + background: ${({ $severity }) => { + switch ($severity) { + case "Critical": + return "radial-gradient(1166.07% 138.62% at 0% 0%, #B92B2B 0%, #B95E2B 100%)"; + case "High": + return "radial-gradient(129.2% 111.8% at 0% 0%, #B95E2B 0%, #B9A22B 100%)"; + case "Medium": + return "radial-gradient(408.61% 111.8% at 0% 0%, #B9A22B 0%, #6AB92B 100%)"; + case "Low": + return "radial-gradient(408.61% 111.8% at 0% 0%, #6AB92B 0%, #2BB997 100%)"; + default: + return "transparent"; + } + }}; `; diff --git a/src/components/Dashboard/NewReport/MetricsTable/types.ts b/src/components/Dashboard/NewReport/MetricsTable/types.ts index d1e4423d4..dd388bc6e 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/types.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/types.ts @@ -5,6 +5,7 @@ export interface MetricsTableProps { } export type ContentAlignment = "left" | "center" | "right"; +export type Severity = "Critical" | "High" | "Medium" | "Low"; export interface ColumnMeta { contentAlign?: ContentAlignment; @@ -14,3 +15,7 @@ export interface ColumnMeta { export interface TableCellContentProps { $align?: ContentAlignment; } + +export interface TableBodyCellCellProps { + $severity: Severity | null; +} diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 5b309c7c4..47f2a5059 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -9,9 +9,7 @@ import { WrenchIcon } from "../../../common/icons/12px/WrenchIcon"; import { actions } from "../../actions"; -import { isNumber } from "highcharts"; import { usePrevious } from "../../../../hooks/usePrevious"; -import { formatUnit } from "../../../../utils/formatUnit"; import { TableIcon } from "../../../common/icons/16px/TableIcon"; import { TreemapIcon } from "../../../common/icons/16px/TreemapIcon"; import { GetServicesPayload } from "../types"; @@ -29,6 +27,9 @@ const dataFetcherFiltersConfiguration: DataFetcherConfiguration = { ...baseFetchConfig }; +export const formatUnit = (value: number, unit: string) => + value === 1 ? `${value} ${unit}` : `${value} ${unit}s`; + const DEFAULT_PERIOD = 1; export const ReportHeader = ({ @@ -65,7 +66,7 @@ export const ReportHeader = ({ useEffect(() => { if ( previousEnvironment !== selectedEnvironment || - previousServices !== previousServices || + previousServices !== selectedServices || previousTimeMode !== timeMode || previousPeriod !== periodInDays ) { @@ -111,8 +112,8 @@ export const ReportHeader = ({ } const value = newItem[0]; - const newValue = isNumber(value) ? value : DEFAULT_PERIOD; - setPeriodInDays(newValue as number); + const newValue = Number(value); + setPeriodInDays(newValue); }; const handleViewModeChanged = (value: string) => { @@ -166,9 +167,9 @@ export const ReportHeader = ({ items={[1, 7].map((x) => ({ value: x.toString(), label: formatUnit(x, "Day"), - selected: x === periodInDays + selected: x === periodInDays, + enabled: true }))} - showSelectedState={true} icon={WrenchIcon} onChange={handlePeriodChanged} placeholder={`Period: ${formatUnit(periodInDays, "day")}`} diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index 8e63cf5bc..d3a20b11b 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -15,11 +15,7 @@ const DefaultQuery: ReportFilterQuery = { lastDays: null }; -export const NewReport = ({ - type -}: { - type: "squarified" | "stripes" | "strip" | "sliceAndDice"; -}) => { +export const NewReport = () => { const [query, setQuery] = useState(DefaultQuery); const { data } = useReportsData(query); const [viewMode, setViewMode] = useState("table"); @@ -45,7 +41,9 @@ export const NewReport = ({ onViewModeChanged={handleViewModeChange} /> {viewMode === "table" && } - {viewMode === "treemap" && } + {viewMode === "treemap" && ( + + )} ); }; diff --git a/src/components/Dashboard/NewReport/utils.ts b/src/components/Dashboard/NewReport/utils.ts new file mode 100644 index 000000000..26947489d --- /dev/null +++ b/src/components/Dashboard/NewReport/utils.ts @@ -0,0 +1,18 @@ +export const getRank = (maxImpactScore: number, value: number) => { + const rangeStep = maxImpactScore / 4; + const rangeValue = value / rangeStep; + + if (rangeValue < 1) { + return "Low"; + } + + if (rangeValue < 2) { + return "Medium"; + } + + if (rangeValue < 3) { + return "High"; + } + + return "Critical"; +}; diff --git a/src/containers/Dashboard/index.tsx b/src/containers/Dashboard/index.tsx index c49f7e1f9..970ab8832 100644 --- a/src/containers/Dashboard/index.tsx +++ b/src/containers/Dashboard/index.tsx @@ -5,7 +5,7 @@ import { sendMessage } from "../../api"; import { Dashboard } from "../../components/Dashboard"; -import { Report } from "../../components/Dashboard/Report"; +import { NewReport } from "../../components/Dashboard/NewReport"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; import { isString } from "../../typeGuards/isString"; @@ -32,7 +32,7 @@ const initialPath = isString(window.initialRoutePath) const getView = () => { switch (initialPath) { case "report": - return ; + return ; default: return ; From 9a253ad02f46f2daa2875f8c379191f6a19aa897 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 15:47:36 +0300 Subject: [PATCH 05/26] Show negative values --- .../Dashboard/NewReport/MetricsTable/index.tsx | 13 ++++++++++--- .../Dashboard/NewReport/MetricsTable/mockData.ts | 4 ++-- .../Dashboard/NewReport/MetricsTable/types.ts | 1 + src/components/Dashboard/NewReport/index.tsx | 7 ++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/Dashboard/NewReport/MetricsTable/index.tsx b/src/components/Dashboard/NewReport/MetricsTable/index.tsx index 8f876a419..035bb8f04 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/index.tsx +++ b/src/components/Dashboard/NewReport/MetricsTable/index.tsx @@ -10,7 +10,7 @@ import { getRank } from "../utils"; import * as s from "./styles"; import { ColumnMeta, MetricsTableProps, Severity } from "./types"; -export const MetricsTable = ({ data }: MetricsTableProps) => { +export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { const columnHelper = createColumnHelper(); const maxImpact = Math.max(...data.map((x) => x.impact)); @@ -21,7 +21,14 @@ export const MetricsTable = ({ data }: MetricsTableProps) => { }), columnHelper.accessor((row) => row.issues, { header: "Critical issues", - cell: (info) => (info.getValue() === 0 ? 0 : `+${info.getValue()}`), + cell: (info) => { + const value = info.getValue(); + if (!showSign) { + return value; + } + + return `${value > 0 ? "+" : ""}${value}`; + }, sortingFn: sortingFns.alphanumeric, meta: { contentAlign: "center" @@ -29,7 +36,7 @@ export const MetricsTable = ({ data }: MetricsTableProps) => { }), columnHelper.accessor((row) => row.impact, { header: "Impact", - cell: (info) => info.getValue() * 100, + cell: (info) => Math.round(info.getValue() * 100), sortingFn: sortingFns.alphanumeric, meta: { contentAlign: "center" diff --git a/src/components/Dashboard/NewReport/MetricsTable/mockData.ts b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts index d190949eb..bc53ece22 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/mockData.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts @@ -3,7 +3,7 @@ import { ServiceMetricsReport } from "../types"; export const mockedReport: ServiceMetricsReport = { reports: [ { - impact: 100, + impact: 100.123123, key: { environment: "TEST", service: "Transactions", @@ -57,7 +57,7 @@ export const mockedReport: ServiceMetricsReport = { issues: 70 }, { - impact: 120, + impact: 99.1231, key: { environment: "TEST", service: "Users", diff --git a/src/components/Dashboard/NewReport/MetricsTable/types.ts b/src/components/Dashboard/NewReport/MetricsTable/types.ts index dd388bc6e..ac9b962f3 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/types.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/types.ts @@ -2,6 +2,7 @@ import { ServiceData } from "../types"; export interface MetricsTableProps { data: ServiceData[]; + showSign: boolean; } export type ContentAlignment = "left" | "center" | "right"; diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index d3a20b11b..2bd50ad90 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -40,7 +40,12 @@ export const NewReport = () => { onFilterChanged={handleFilterChanged} onViewModeChanged={handleViewModeChange} /> - {viewMode === "table" && } + {viewMode === "table" && ( + + )} {viewMode === "treemap" && ( )} From 5ec31eb0a5de99cd84e0e13cff9bba603e911946 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 17:07:26 +0300 Subject: [PATCH 06/26] Enable sorting --- .../NewReport/MetricsTable/index.tsx | 40 +++++++++++++++++-- .../NewReport/MetricsTable/styles.ts | 12 ++++++ .../NewReport/ReportHeader/index.tsx | 16 ++++++-- src/components/common/icons/16px/SortIcon.tsx | 27 +++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/components/common/icons/16px/SortIcon.tsx diff --git a/src/components/Dashboard/NewReport/MetricsTable/index.tsx b/src/components/Dashboard/NewReport/MetricsTable/index.tsx index 035bb8f04..8ea46f500 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/index.tsx +++ b/src/components/Dashboard/NewReport/MetricsTable/index.tsx @@ -2,9 +2,14 @@ import { createColumnHelper, flexRender, getCoreRowModel, + getSortedRowModel, sortingFns, useReactTable } from "@tanstack/react-table"; + +import { isUndefined } from "../../../../typeGuards/isUndefined"; +import { SortIcon } from "../../../common/icons/16px/SortIcon"; +import { SORTING_ORDER } from "../../../common/SortingSelector/types"; import { ServiceData } from "../types"; import { getRank } from "../utils"; import * as s from "./styles"; @@ -17,10 +22,12 @@ export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { const columns = [ columnHelper.accessor((row) => row.key.service, { header: "Service", + enableSorting: false, cell: (info) => info.getValue() }), columnHelper.accessor((row) => row.issues, { header: "Critical issues", + id: "issues", cell: (info) => { const value = info.getValue(); if (!showSign) { @@ -32,18 +39,23 @@ export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { sortingFn: sortingFns.alphanumeric, meta: { contentAlign: "center" - } + }, + enableSorting: true }), columnHelper.accessor((row) => row.impact, { + id: "impact", header: "Impact", cell: (info) => Math.round(info.getValue() * 100), sortingFn: sortingFns.alphanumeric, + enableSorting: true, meta: { contentAlign: "center" } }), columnHelper.accessor((row) => getRank(maxImpact, row.impact), { header: "Rank", + id: "rank", + enableSorting: true, cell: (info) => { return info.getValue(); }, @@ -57,7 +69,8 @@ export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { const table = useReactTable({ data, columns, - getCoreRowModel: getCoreRowModel() + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel() }); return ( @@ -72,13 +85,34 @@ export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { return ( - + {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} + {isUndefined(header.column.columnDef.enableSorting) || + (header.column.columnDef.enableSorting && + { + asc: ( + + + + ), + desc: ( + + + + ) + }[(header.column.getIsSorted() as string) || "asc"])} ); diff --git a/src/components/Dashboard/NewReport/MetricsTable/styles.ts b/src/components/Dashboard/NewReport/MetricsTable/styles.ts index 52297bdb7..38794a9f4 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/styles.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/styles.ts @@ -3,6 +3,10 @@ import { subheadingBoldTypography, subheadingSemiboldTypography } from "../../../common/App/typographies"; +import { + SORTING_ORDER, + SortingOrderIconContainerProps +} from "../../../common/SortingSelector/types"; import { TableBodyCellCellProps, TableCellContentProps } from "./types"; export const Table = styled.table` @@ -40,6 +44,7 @@ export const TableCellContent = styled.div` export const TableHeaderCellContent = styled(TableCellContent)` ${subheadingSemiboldTypography} font-weight: 400; + gap: 4px; color: ${({ theme }) => theme.colors.v3.text.tertiary}; `; @@ -72,3 +77,10 @@ export const TableBodyCell = styled.td` } }}; `; + +export const SortingOrderIconContainer = styled.div` + display: flex; + transform: scaleY( + ${({ $sortingOrder }) => ($sortingOrder === SORTING_ORDER.DESC ? -1 : 1)} + ); +`; diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 47f2a5059..f789e68c1 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -36,6 +36,7 @@ export const ReportHeader = ({ onFilterChanged, onViewModeChanged }: ReportHeaderProps) => { + const { environments } = useConfigSelector(); const [periodInDays, setPeriodInDays] = useState(DEFAULT_PERIOD); const [viewMode, setVieMode] = useState("table"); const [timeMode, setTimeMode] = useState("baseline"); @@ -43,7 +44,7 @@ export const ReportHeader = ({ const [selectedEnvironment, setSelectedEnvironment] = useState( null ); - const { environments } = useConfigSelector(); + const previousServices = usePrevious(selectedServices); const previousEnvironment = usePrevious(selectedEnvironment); const previousTimeMode = usePrevious(timeMode); @@ -63,6 +64,13 @@ export const ReportHeader = ({ getData(); }, []); + useEffect(() => { + setSelectedEnvironment( + environments?.length && environments?.length > 0 + ? environments[0].id + : null + ); + }, [environments]); useEffect(() => { if ( previousEnvironment !== selectedEnvironment || @@ -72,7 +80,8 @@ export const ReportHeader = ({ ) { onFilterChanged({ lastDays: timeMode === "baseline" ? null : periodInDays, - services: selectedServices, + services: + selectedServices.length > 0 ? selectedServices : services ?? [], environmentId: selectedEnvironment }); } @@ -85,7 +94,8 @@ export const ReportHeader = ({ previousEnvironment, previousServices, previousTimeMode, - previousPeriod + previousPeriod, + services ]); const handleSelectedEnvironmentChanged = (option: string | string[]) => { diff --git a/src/components/common/icons/16px/SortIcon.tsx b/src/components/common/icons/16px/SortIcon.tsx new file mode 100644 index 000000000..a86dc66d2 --- /dev/null +++ b/src/components/common/icons/16px/SortIcon.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const SortIconComponent = (props: IconProps) => { + const { color, size } = useIconProps(props); + + return ( + + + + + + + + + + + ); +}; + +export const SortIcon = React.memo(SortIconComponent); From 153ec155e491eff2c8decdc820ba6b010088b6f0 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 17:30:46 +0300 Subject: [PATCH 07/26] Added footer --- src/components/Dashboard/NewReport/index.tsx | 35 ++++++++++++-------- src/components/Dashboard/NewReport/styles.ts | 14 +++++++- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index 2bd50ad90..ed700ba33 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -1,6 +1,7 @@ import { useLayoutEffect, useState } from "react"; import { actions } from "../actions"; +import { DigmaLogoIcon } from "../../common/icons/16px/DigmaLogoIcon"; import { Chart } from "./Chart"; import { MetricsTable } from "./MetricsTable"; import { ReportHeader } from "./ReportHeader"; @@ -35,20 +36,26 @@ export const NewReport = () => { }; return ( - - - {viewMode === "table" && ( - + + - )} - {viewMode === "treemap" && ( - - )} - + {viewMode === "table" && ( + + )} + {viewMode === "treemap" && ( + + )} + + + © 2024 digma.ai + + + ); }; diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts index 438eb3bb5..c3fba6004 100644 --- a/src/components/Dashboard/NewReport/styles.ts +++ b/src/components/Dashboard/NewReport/styles.ts @@ -1,11 +1,12 @@ import styled from "styled-components"; +import { bodyRegularTypography } from "../../common/App/typographies"; export const Container = styled.div` display: flex; flex-direction: column; min-height: 100%; min-width: fit-content; - background: ${({ theme }) => theme.colors.v3.surface.primary}; + background: ${({ theme }) => theme.colors.v3.surface.secondary}; padding: 24px; gap: 24px; `; @@ -25,3 +26,14 @@ export const Column = styled.div` border-radius: 12px; width: 100%; `; + +export const Footer = styled.div` + padding: 0 16px; + align-items: center; + display: flex; + justify-content: start; + gap: 8px; + margin-top: auto; + color: ${({ theme }) => theme.colors.v3.text.disabled}; + ${bodyRegularTypography} +`; From 0a73fd1d3cb4b667200b4e3ef9b0cc97ab9a681a Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 17:35:32 +0300 Subject: [PATCH 08/26] Fixed styles --- src/components/Dashboard/NewReport/index.tsx | 12 ++++----- src/components/Dashboard/NewReport/styles.ts | 27 +++++++------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index ed700ba33..0da02d544 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -36,7 +36,7 @@ export const NewReport = () => { }; return ( - <> + { {viewMode === "treemap" && ( )} - - - © 2024 digma.ai - - + + + © 2024 digma.ai + + ); }; diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts index c3fba6004..e9c0c1850 100644 --- a/src/components/Dashboard/NewReport/styles.ts +++ b/src/components/Dashboard/NewReport/styles.ts @@ -4,31 +4,15 @@ import { bodyRegularTypography } from "../../common/App/typographies"; export const Container = styled.div` display: flex; flex-direction: column; - min-height: 100%; + flex: 1; min-width: fit-content; background: ${({ theme }) => theme.colors.v3.surface.secondary}; padding: 24px; gap: 24px; `; -export const Content = styled.div` - padding: 32px; - display: flex; - gap: 16px; -`; - -export const Column = styled.div` - display: flex; - flex-direction: column; - padding: 16px; - align-self: stretch; - background: ${({ theme }) => theme.colors.v3.surface.secondary}; - border-radius: 12px; - width: 100%; -`; - export const Footer = styled.div` - padding: 0 16px; + padding: 24px 32px 16px; align-items: center; display: flex; justify-content: start; @@ -37,3 +21,10 @@ export const Footer = styled.div` color: ${({ theme }) => theme.colors.v3.text.disabled}; ${bodyRegularTypography} `; + +export const Section = styled.div` + display: flex; + height: 100%; + flex-direction: column; + background: ${({ theme }) => theme.colors.v3.surface.secondary}; +`; From cc25d3a215dd4d0022425a0bda80bc2e222b4e37 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 18:13:52 +0300 Subject: [PATCH 09/26] Added background --- .../NewReport/ReportHeader/index.tsx | 1 + src/components/Dashboard/NewReport/index.tsx | 2 ++ src/components/Dashboard/NewReport/styles.ts | 29 ++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index f789e68c1..8c322e23b 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -70,6 +70,7 @@ export const ReportHeader = ({ ? environments[0].id : null ); + setSelectedServices([]); }, [environments]); useEffect(() => { if ( diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index 0da02d544..498b6e486 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -37,6 +37,8 @@ export const NewReport = () => { return ( + + theme.colors.v3.surface.secondary}; padding: 24px; gap: 24px; `; @@ -26,5 +25,33 @@ export const Section = styled.div` display: flex; height: 100%; flex-direction: column; + position: relative; + overflow: hidden; +`; + +export const ContainerBackgroundGradient = styled.div` + z-index: -1; + position: absolute; + margin: auto; + right: -24%; + bottom: -117%; + height: 160%; + width: 146%; + border-radius: 100%; + opacity: 0.7; + background: radial-gradient( + 50% 50% at 50% 50%, + rgb(79 93 163 / 60%) 0%, + rgb(79 93 163 / 0%) 100% + ); + filter: blur(5px); +`; + +export const SectionBackground = styled.div` + z-index: -1; + position: absolute; + inset: 0; + height: 100%; + width: 100%; background: ${({ theme }) => theme.colors.v3.surface.secondary}; `; From c74c54b337f29118ec03edf9beac9a59710f8caf Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 17 Sep 2024 17:40:07 +0200 Subject: [PATCH 10/26] Add Chart view to the Report --- package-lock.json | 40 ++--- package.json | 3 +- .../ServiceTile/TooltipKeyValue/index.tsx | 9 ++ .../ServiceTile/TooltipKeyValue/styles.ts | 14 ++ .../ServiceTile/TooltipKeyValue/types.ts | 6 + .../NewReport/Chart/ServiceTile/index.tsx | 44 ++++++ .../NewReport/Chart/ServiceTile/styles.ts | 9 ++ .../NewReport/Chart/ServiceTile/types.ts | 10 ++ .../Dashboard/NewReport/Chart/index.tsx | 137 ++++++++---------- .../Dashboard/NewReport/Chart/styles.ts | 3 +- .../Dashboard/NewReport/Chart/types.ts | 10 +- .../NewReport/MetricsTable/mockData.ts | 6 +- src/components/Dashboard/NewReport/index.tsx | 14 +- src/components/Dashboard/NewReport/styles.ts | 5 +- src/components/Dashboard/NewReport/utils.ts | 4 +- src/components/common/TreeMap/Tile/index.tsx | 38 +++++ src/components/common/TreeMap/Tile/styles.ts | 62 ++++++++ src/components/common/TreeMap/Tile/types.ts | 21 +++ .../common/TreeMap/TreeMap.stories.tsx | 90 ++++++++++++ src/components/common/TreeMap/index.tsx | 67 +++++++++ src/components/common/TreeMap/types.ts | 14 ++ src/components/common/v3/Tooltip/index.tsx | 15 +- src/components/common/v3/Tooltip/types.ts | 1 + 23 files changed, 483 insertions(+), 139 deletions(-) create mode 100644 src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/index.tsx create mode 100644 src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/styles.ts create mode 100644 src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/types.ts create mode 100644 src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx create mode 100644 src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts create mode 100644 src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts create mode 100644 src/components/common/TreeMap/Tile/index.tsx create mode 100644 src/components/common/TreeMap/Tile/styles.ts create mode 100644 src/components/common/TreeMap/Tile/types.ts create mode 100644 src/components/common/TreeMap/TreeMap.stories.tsx create mode 100644 src/components/common/TreeMap/index.tsx create mode 100644 src/components/common/TreeMap/types.ts diff --git a/package-lock.json b/package-lock.json index 99e6bf6b1..672d8c02a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,6 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^2.29.3", "free-email-domains": "^1.2.5", - "highcharts": "^11.4.8", - "highcharts-react-official": "^3.2.1", "react": "^18.2.0", "react-cool-dimensions": "^3.0.1", "react-dom": "^18.2.0", @@ -30,6 +28,7 @@ "react-transition-group": "^4.4.5", "recharts": "^2.6.2", "semver": "^7.5.4", + "squarify": "^1.1.0", "styled-components": "^6.1.0", "uuid": "^9.0.1", "zustand": "^4.5.5", @@ -9403,22 +9402,6 @@ "he": "bin/he" } }, - "node_modules/highcharts": { - "version": "11.4.8", - "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.4.8.tgz", - "integrity": "sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==", - "license": "https://www.highcharts.com/license" - }, - "node_modules/highcharts-react-official": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.1.tgz", - "integrity": "sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA==", - "license": "MIT", - "peerDependencies": { - "highcharts": ">=6.0.0", - "react": ">=16.8.0" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -13659,6 +13642,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/squarify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/squarify/-/squarify-1.1.0.tgz", + "integrity": "sha512-0nD8UD4FPOfWHdaVYACbr1SmBF5XQeTbDcRfVs8NHVtueRC0OPo4DN/TJVjAJZ0fLwrgDhEI3XrhUicglD9npw==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -22539,17 +22527,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "highcharts": { - "version": "11.4.8", - "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.4.8.tgz", - "integrity": "sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==" - }, - "highcharts-react-official": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.1.tgz", - "integrity": "sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA==", - "requires": {} - }, "highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -25523,6 +25500,11 @@ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==" }, + "squarify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/squarify/-/squarify-1.1.0.tgz", + "integrity": "sha512-0nD8UD4FPOfWHdaVYACbr1SmBF5XQeTbDcRfVs8NHVtueRC0OPo4DN/TJVjAJZ0fLwrgDhEI3XrhUicglD9npw==" + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 49a5a184c..00995577b 100644 --- a/package.json +++ b/package.json @@ -108,8 +108,6 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^2.29.3", "free-email-domains": "^1.2.5", - "highcharts": "^11.4.8", - "highcharts-react-official": "^3.2.1", "react": "^18.2.0", "react-cool-dimensions": "^3.0.1", "react-dom": "^18.2.0", @@ -122,6 +120,7 @@ "react-transition-group": "^4.4.5", "recharts": "^2.6.2", "semver": "^7.5.4", + "squarify": "^1.1.0", "styled-components": "^6.1.0", "uuid": "^9.0.1", "zustand": "^4.5.5", diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/index.tsx b/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/index.tsx new file mode 100644 index 000000000..7c0602a08 --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/index.tsx @@ -0,0 +1,9 @@ +import * as s from "./styles"; +import { TooltipKeyValueProps } from "./types"; + +export const TooltipKeyValue = ({ label, children }: TooltipKeyValueProps) => ( + + {label}: + {children} + +); diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/styles.ts b/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/styles.ts new file mode 100644 index 000000000..9fc3c73c4 --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/styles.ts @@ -0,0 +1,14 @@ +import styled from "styled-components"; +import { footnoteRegularTypography } from "../../../../../common/App/typographies"; + +export const Container = styled.div` + ${footnoteRegularTypography} + + display: flex; + gap: 4px; + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const Label = styled.span` + color: ${({ theme }) => theme.colors.v3.text.secondary}; +`; diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/types.ts b/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/types.ts new file mode 100644 index 000000000..e0ae47cf7 --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/TooltipKeyValue/types.ts @@ -0,0 +1,6 @@ +import { ReactNode } from "react"; + +export interface TooltipKeyValueProps { + label: string; + children: ReactNode; +} diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx new file mode 100644 index 000000000..313451071 --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx @@ -0,0 +1,44 @@ +import { Tile } from "../../../../common/TreeMap/Tile"; +import * as s from "./styles"; +import { TooltipKeyValue } from "./TooltipKeyValue"; +import { ServiceTileProps } from "./types"; + +const getNumberSign = (value: number) => { + if (value > 0) { + return "+"; + } + + if (value < 0) { + return "-"; + } + + return ""; +}; + +export const ServiceTile = ({ + name, + criticalIssuesCount, + impactScore, + severity, + viewMode +}: ServiceTileProps) => ( + + {name} + + {viewMode === "changes" && getNumberSign(impactScore)} + {criticalIssuesCount} + + {impactScore} + + } + > + + {criticalIssuesCount} + | {impactScore} + + +); diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts b/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts new file mode 100644 index 000000000..3c69d2c67 --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts @@ -0,0 +1,9 @@ +import styled from "styled-components"; + +export const StatsMainNumber = styled.span` + color: ${(props) => props.theme.colors.v3.text.primary}; +`; + +export const TooltipContent = styled.div` + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts b/src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts new file mode 100644 index 000000000..8e0635322 --- /dev/null +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts @@ -0,0 +1,10 @@ +import { Severity } from "../../MetricsTable/types"; +import { ReportTimeMode } from "../../ReportHeader/types"; + +export interface ServiceTileProps { + name: string; + criticalIssuesCount: number; + impactScore: number; + severity: Severity; + viewMode: ReportTimeMode; +} diff --git a/src/components/Dashboard/NewReport/Chart/index.tsx b/src/components/Dashboard/NewReport/Chart/index.tsx index fddf9d153..5261f660d 100644 --- a/src/components/Dashboard/NewReport/Chart/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/index.tsx @@ -1,90 +1,67 @@ -import Highcharts from "highcharts"; -import HighchartsReact from "highcharts-react-official"; -import addHeatmap from "highcharts/modules/heatmap"; -import addTreemapModule from "highcharts/modules/treemap"; +import useDimensions from "react-cool-dimensions"; +import { Input } from "squarify"; +import { isNumber } from "../../../../typeGuards/isNumber"; +import { TreeMap } from "../../../common/TreeMap"; +import { TileData } from "../../../common/TreeMap/types"; +import { Severity } from "../MetricsTable/types"; +import { ReportTimeMode } from "../ReportHeader/types"; +import { getRank } from "../utils"; +import { ServiceTile } from "./ServiceTile"; import * as s from "./styles"; import { ChartProps } from "./types"; -addTreemapModule(Highcharts); -addHeatmap(Highcharts); -const baseOptions = ( - type: "squarified" | "stripes" | "strip" | "sliceAndDice" -) => ({ - colorAxis: { - maxColor: "#B92B2B", - minColor: "#2BA0B9" - }, - legend: { - enabled: false - }, - series: [ - { - type: "treemap", - layoutStartingDirection: "horizontal", - layoutAlgorithm: type ?? "sliceAndDice", - alternateStartingDirection: true, - borderRadius: 12, - clip: false, - levels: [ - { - level: 1, - borderWidth: 12, - borderColor: "#1A1B1E", // todo color depends on theme - dataLabels: { - enabled: true, - align: "center", - verticalAlign: "middle", - padding: 24, +const getChangesSeverity = (impactScore: number): Severity => { + if (impactScore < 0) { + return "Low"; + } - style: { - fontSize: 32, - color: "#FFFFFF", - fontWeight: "400" - } - } - } - ], - data: [ - { - name: "Payment Service
12 / 1500", - value: 6, - colorValue: 6 - }, - { - name: "Transaction Service
15 / 710", - value: 5, - colorValue: 5 - }, - { - name: "Share Service
5 / 530", - value: 3, - colorValue: 3 - }, - { - name: "Metadata Service
2 / 100", - value: 1, - colorValue: 1 - } - ] - } - ], - chart: { - backgroundColor: "transparent", - height: "100%", // 16:9 ratio,Transaction Service - margin: 0 + if (impactScore > 0) { + return "Critical"; } -}); -export const Chart = ({ type }: ChartProps) => { + return "High"; +}; + +export const Chart = ({ data }: ChartProps) => { + const { width, height, observe } = useDimensions(); + + const viewMode: ReportTimeMode = data.some((service) => + isNumber(service.key.lastDays) + ) + ? "changes" + : "baseline"; + + const transformedData = data.map((service) => ({ + ...service, + impact: Math.trunc(service.impact * 100) + })); + + const maxImpactScore = Math.max(...transformedData.map((x) => x.impact)); + + const chartData: Input[] = transformedData.map((service) => { + const severity = + viewMode === "baseline" + ? getRank(maxImpactScore, service.impact) + : getChangesSeverity(service.impact); + + return { + id: service.key.service, + value: service.impact, + content: ( + + ) + }; + }); + return ( - - + + ); }; diff --git a/src/components/Dashboard/NewReport/Chart/styles.ts b/src/components/Dashboard/NewReport/Chart/styles.ts index 459103c5b..cb5735fd4 100644 --- a/src/components/Dashboard/NewReport/Chart/styles.ts +++ b/src/components/Dashboard/NewReport/Chart/styles.ts @@ -1,7 +1,6 @@ import styled from "styled-components"; export const Container = styled.div` - display: flex; - flex-direction: column; + width: 100%; height: 100%; `; diff --git a/src/components/Dashboard/NewReport/Chart/types.ts b/src/components/Dashboard/NewReport/Chart/types.ts index f97d8383d..9a64f40c2 100644 --- a/src/components/Dashboard/NewReport/Chart/types.ts +++ b/src/components/Dashboard/NewReport/Chart/types.ts @@ -1,11 +1,5 @@ -export interface ChartDataItem { - id: string; - name: string; - value: string; -} +import { ServiceData } from "../types"; export interface ChartProps { - labelFormat?: string; - data: ChartDataItem[]; - type: "squarified" | "stripes" | "strip" | "sliceAndDice"; + data: ServiceData[]; } diff --git a/src/components/Dashboard/NewReport/MetricsTable/mockData.ts b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts index bc53ece22..7b100fdcb 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/mockData.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/mockData.ts @@ -42,7 +42,7 @@ export const mockedReport: ServiceMetricsReport = { impact: 120, key: { environment: "TEST", - service: "Users", + service: "Users1", lastDays: null }, issues: 110 @@ -51,7 +51,7 @@ export const mockedReport: ServiceMetricsReport = { impact: 70, key: { environment: "TEST", - service: "Users", + service: "Users2", lastDays: null }, issues: 70 @@ -60,7 +60,7 @@ export const mockedReport: ServiceMetricsReport = { impact: 99.1231, key: { environment: "TEST", - service: "Users", + service: "Users3", lastDays: null }, issues: 110 diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index 498b6e486..c31eb5335 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -1,7 +1,6 @@ import { useLayoutEffect, useState } from "react"; -import { actions } from "../actions"; - import { DigmaLogoIcon } from "../../common/icons/16px/DigmaLogoIcon"; +import { actions } from "../actions"; import { Chart } from "./Chart"; import { MetricsTable } from "./MetricsTable"; import { ReportHeader } from "./ReportHeader"; @@ -35,6 +34,8 @@ export const NewReport = () => { setViewMode(value); }; + const serviceData = data?.reports ?? []; + return ( @@ -45,14 +46,9 @@ export const NewReport = () => { onViewModeChanged={handleViewModeChange} /> {viewMode === "table" && ( - - )} - {viewMode === "treemap" && ( - + )} + {viewMode === "treemap" && } diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts index fed9cd909..e6079ba6a 100644 --- a/src/components/Dashboard/NewReport/styles.ts +++ b/src/components/Dashboard/NewReport/styles.ts @@ -4,10 +4,11 @@ import { bodyRegularTypography } from "../../common/App/typographies"; export const Container = styled.div` display: flex; flex-direction: column; - flex: 1; - min-width: fit-content; + height: 100%; + width: 100%; padding: 24px; gap: 24px; + box-sizing: border-box; `; export const Footer = styled.div` diff --git a/src/components/Dashboard/NewReport/utils.ts b/src/components/Dashboard/NewReport/utils.ts index 26947489d..0c542af5d 100644 --- a/src/components/Dashboard/NewReport/utils.ts +++ b/src/components/Dashboard/NewReport/utils.ts @@ -1,4 +1,6 @@ -export const getRank = (maxImpactScore: number, value: number) => { +import { Severity } from "./MetricsTable/types"; + +export const getRank = (maxImpactScore: number, value: number): Severity => { const rangeStep = maxImpactScore / 4; const rangeValue = value / rangeStep; diff --git a/src/components/common/TreeMap/Tile/index.tsx b/src/components/common/TreeMap/Tile/index.tsx new file mode 100644 index 000000000..8820c6bb3 --- /dev/null +++ b/src/components/common/TreeMap/Tile/index.tsx @@ -0,0 +1,38 @@ +import useDimensions from "react-cool-dimensions"; +import { Tooltip } from "../../v3/Tooltip"; +import * as s from "./styles"; +import { TileProps } from "./types"; + +const MIN_HEIGHT = 86; // in pixels +const MIN_WIDTH = 92; // in pixels +const MIN_HEIGHT_TO_SHOW_CHILDREN = MIN_HEIGHT + 40; // in pixels + +export const Tile = ({ title, children, severity, tooltip }: TileProps) => { + const { observe: observeContainer, entry: containerEntry } = useDimensions(); + + const isContentVisible = + containerEntry?.target && + containerEntry.target.clientHeight >= MIN_HEIGHT && + containerEntry.target.clientWidth >= MIN_WIDTH; + + const isChildrenVisible = + isContentVisible && + containerEntry.target.clientHeight >= MIN_HEIGHT_TO_SHOW_CHILDREN; + + return ( + + + + {isContentVisible && ( + + {title} + {isChildrenVisible && ( + {children} + )} + + )} + + + + ); +}; diff --git a/src/components/common/TreeMap/Tile/styles.ts b/src/components/common/TreeMap/Tile/styles.ts new file mode 100644 index 000000000..07eeed8d3 --- /dev/null +++ b/src/components/common/TreeMap/Tile/styles.ts @@ -0,0 +1,62 @@ +import styled from "styled-components"; +import { hexToRgb } from "../../../../utils/hexToRgb"; +import { + ChildrenContainerProps, + TileContainerProps, + TitleProps +} from "./types"; + +export const Container = styled.div` + width: 100%; + height: 100%; +`; + +export const TileContainer = styled.div` + width: 100%; + height: 100%; + border-radius: 12px; + background: ${({ $severity }) => { + switch ($severity) { + case "Critical": + return "radial-gradient(1166.07% 138.62% at 0% 0%, #B92B2B 0%, #B95E2B 100%)"; + case "High": + return "radial-gradient(129.2% 111.8% at 0% 0%, #B95E2B 0%, #B9A22B 100%)"; + case "Medium": + return "radial-gradient(408.61% 111.8% at 0% 0%, #B9A22B 0%, #6AB92B 100%)"; + case "Low": + default: + return "radial-gradient(408.61% 111.8% at 0% 0%, #6AB92B 0%, #2BB997 100%)"; + } + }}; +`; + +export const ContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + padding: 24px; + box-sizing: border-box; +`; + +export const Title = styled.span` + color: ${(props) => props.theme.colors.v3.text.primary}; + font-size: 32px; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const ChildrenContainer = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: ${(props) => { + const rgb = hexToRgb(props.theme.colors.v3.text.primary); + return rgb + ? `rgba(${rgb.r} ${rgb.g} ${rgb.b} / 70%)` + : props.theme.colors.v3.text.primary; + }}; + font-size: 32px; + font-weight: 700; +`; diff --git a/src/components/common/TreeMap/Tile/types.ts b/src/components/common/TreeMap/Tile/types.ts new file mode 100644 index 000000000..eafe989a6 --- /dev/null +++ b/src/components/common/TreeMap/Tile/types.ts @@ -0,0 +1,21 @@ +import { ReactNode } from "react"; +import { Severity } from "../../../Dashboard/NewReport/MetricsTable/types"; + +export interface TileProps { + title: string; + children?: ReactNode; + severity?: Severity; + tooltip?: ReactNode; +} + +export interface TileContainerProps { + $severity?: Severity; +} + +export interface TitleProps { + $isVisible?: boolean; +} + +export interface ChildrenContainerProps { + $isVisible?: boolean; +} diff --git a/src/components/common/TreeMap/TreeMap.stories.tsx b/src/components/common/TreeMap/TreeMap.stories.tsx new file mode 100644 index 000000000..68ee49140 --- /dev/null +++ b/src/components/common/TreeMap/TreeMap.stories.tsx @@ -0,0 +1,90 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { TreeMap } from "."; +import { ServiceTile } from "../../Dashboard/NewReport/Chart/ServiceTile"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "common/TreeMap", + component: TreeMap, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + padding: 40, + data: [ + { + id: "payment", + value: 1500, + content: ( + + ) + }, + { + id: "transaction", + value: 710, + content: ( + + ) + }, + { + id: "share", + value: 530, + content: ( + + ) + }, + { + id: "metadata", + value: 100, + content: ( + + ) + }, + { + id: "monitoring", + value: 3, + content: ( + + ) + } + ] + } +}; diff --git a/src/components/common/TreeMap/index.tsx b/src/components/common/TreeMap/index.tsx new file mode 100644 index 000000000..541934fbf --- /dev/null +++ b/src/components/common/TreeMap/index.tsx @@ -0,0 +1,67 @@ +import squarify from "squarify"; +import { isNull } from "../../../typeGuards/isNull"; +import { TreeMapProps } from "./types"; + +export const TreeMap = ({ padding = 0, data, width, height }: TreeMapProps) => { + const container = { x0: 0, y0: 0, x1: width, y1: height }; + + const dataMax = Math.max(...data.map((item) => item.value)); + const minNormalizedValue = dataMax > 0 ? dataMax * 0.05 : 1; + const normalizedData = data.map((item, index) => { + return { + id: index, + value: item.value < minNormalizedValue ? minNormalizedValue : item.value, + content: item.content + }; + }); + + const tiles = squarify(normalizedData, container); + + // Transform coordinates to add paddings between tiles + const transformedTiles = padding + ? tiles.map((tile) => { + const isLeftEdge = tile.x0 === 0; + const isTopEdge = tile.y0 === 0; + const isRightEdge = tile.x1 - width < 1; + const isBottomEdge = tile.y1 - height < 1; + + return { + ...tile, + x0: isLeftEdge ? tile.x0 : tile.x0 + padding / 2, + y0: isTopEdge ? tile.y0 : tile.y0 + padding / 2, + x1: isRightEdge ? tile.x1 : tile.x1 - padding / 2, + y1: isBottomEdge ? tile.y1 : tile.y1 - padding / 2 + }; + }) + : tiles; + + return ( +
+ {[width, height].some(isNull) + ? null + : transformedTiles.map((tile) => { + return ( +
+ {tile.content} +
+ ); + })} +
+ ); +}; diff --git a/src/components/common/TreeMap/types.ts b/src/components/common/TreeMap/types.ts new file mode 100644 index 000000000..407e77b40 --- /dev/null +++ b/src/components/common/TreeMap/types.ts @@ -0,0 +1,14 @@ +import { ReactNode } from "react"; +import { Input } from "squarify"; + +export interface TileData { + id: string; + content: ReactNode; +} + +export interface TreeMapProps { + padding?: number; + data: Input[]; + width: number; + height: number; +} diff --git a/src/components/common/v3/Tooltip/index.tsx b/src/components/common/v3/Tooltip/index.tsx index c769a5bb4..fe38905c7 100644 --- a/src/components/common/v3/Tooltip/index.tsx +++ b/src/components/common/v3/Tooltip/index.tsx @@ -8,6 +8,7 @@ import { hide, offset, shift, + useClientPoint, useFloating, useHover, useInteractions, @@ -76,7 +77,8 @@ export const Tooltip = ({ isDisabled, fullWidth, title, - boundary + boundary, + followCursor }: TooltipProps) => { const [isOpen, setIsOpen] = useState(false); const arrowRef = useRef(null); @@ -113,10 +115,17 @@ export const Tooltip = ({ const hover = useHover(context, { delay: { open: 1000, close: 0 }, - enabled: !isBoolean(forcedIsOpen) + enabled: !isBoolean(forcedIsOpen) || !followCursor }); - const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + const clientPoint = useClientPoint(context, { + enabled: followCursor + }); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + hover, + clientPoint + ]); const renderArrow = (withShadow: boolean) => ( Date: Tue, 17 Sep 2024 18:35:35 +0300 Subject: [PATCH 11/26] added icons --- .../NewReport/ReportHeader/index.tsx | 41 +++++++++++-------- .../icons/12px/DurationBreakdownIcon.tsx | 37 +++++++++++++++++ 2 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 src/components/common/icons/12px/DurationBreakdownIcon.tsx diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 8c322e23b..0c51487a8 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -4,12 +4,16 @@ import { useFetchData } from "../../../../hooks/useFetchData"; import { useConfigSelector } from "../../../../store/config/useConfigSelector"; -import { GlobeIcon } from "../../../common/icons/12px/GlobeIcon"; import { WrenchIcon } from "../../../common/icons/12px/WrenchIcon"; import { actions } from "../../actions"; import { usePrevious } from "../../../../hooks/usePrevious"; +import { Environment } from "../../../common/App/types"; + +import { CodeIcon } from "../../../common/icons/12px/CodeIcon"; +import { DurationBreakdownIcon } from "../../../common/icons/12px/DurationBreakdownIcon"; +import { InfinityIcon } from "../../../common/icons/16px/InfinityIcon"; import { TableIcon } from "../../../common/icons/16px/TableIcon"; import { TreemapIcon } from "../../../common/icons/16px/TreemapIcon"; import { GetServicesPayload } from "../types"; @@ -41,9 +45,8 @@ export const ReportHeader = ({ const [viewMode, setVieMode] = useState("table"); const [timeMode, setTimeMode] = useState("baseline"); const [selectedServices, setSelectedServices] = useState([]); - const [selectedEnvironment, setSelectedEnvironment] = useState( - null - ); + const [selectedEnvironment, setSelectedEnvironment] = + useState(null); const previousServices = usePrevious(selectedServices); const previousEnvironment = usePrevious(selectedEnvironment); @@ -51,7 +54,7 @@ export const ReportHeader = ({ const previousPeriod = usePrevious(periodInDays); const getServicesPayload = useMemo( - () => ({ environment: selectedEnvironment }), + () => ({ environment: selectedEnvironment?.id ?? null }), [selectedEnvironment] ); @@ -66,9 +69,7 @@ export const ReportHeader = ({ useEffect(() => { setSelectedEnvironment( - environments?.length && environments?.length > 0 - ? environments[0].id - : null + environments?.length && environments?.length > 0 ? environments[0] : null ); setSelectedServices([]); }, [environments]); @@ -83,7 +84,7 @@ export const ReportHeader = ({ lastDays: timeMode === "baseline" ? null : periodInDays, services: selectedServices.length > 0 ? selectedServices : services ?? [], - environmentId: selectedEnvironment + environmentId: selectedEnvironment?.id ?? null }); } }, [ @@ -101,13 +102,14 @@ export const ReportHeader = ({ const handleSelectedEnvironmentChanged = (option: string | string[]) => { const newItem = - option === selectedEnvironment + option === selectedEnvironment?.id ? [""] : Array.isArray(option) ? option : [option]; - setSelectedEnvironment(newItem[0]); + const newItemEnv = environments?.find((x) => x.id === newItem[0]) ?? null; + setSelectedEnvironment(newItemEnv); }; const handleSelectedServicesChanged = (option: string | string[]) => { @@ -161,16 +163,19 @@ export const ReportHeader = ({ label: x.name, value: x.id, enabled: true, - selected: x.id === selectedEnvironment + selected: x.id === selectedEnvironment?.id })) ?? [] } showSelectedState={true} - icon={GlobeIcon} - onChange={handleSelectedEnvironmentChanged} - placeholder={ - environments?.find((x) => x.id === selectedEnvironment)?.name ?? - "Select Environments" + icon={(props) => + selectedEnvironment?.type === "Public" ? ( + + ) : ( + + ) } + onChange={handleSelectedEnvironmentChanged} + placeholder={selectedEnvironment?.name ?? "Select Environments"} /> {timeMode === "changes" && ( @@ -181,7 +186,7 @@ export const ReportHeader = ({ selected: x === periodInDays, enabled: true }))} - icon={WrenchIcon} + icon={DurationBreakdownIcon} onChange={handlePeriodChanged} placeholder={`Period: ${formatUnit(periodInDays, "day")}`} /> diff --git a/src/components/common/icons/12px/DurationBreakdownIcon.tsx b/src/components/common/icons/12px/DurationBreakdownIcon.tsx new file mode 100644 index 000000000..ef37ce34c --- /dev/null +++ b/src/components/common/icons/12px/DurationBreakdownIcon.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const DurationBreakdownIconComponent = (props: IconProps) => { + const { size } = useIconProps(props); + + return ( + + + + + + + + + + + + + ); +}; + +export const DurationBreakdownIcon = React.memo(DurationBreakdownIconComponent); From 089eac974e1aaaccec9ee68438c3d7aaa6b776aa Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 18:48:25 +0300 Subject: [PATCH 12/26] update styles --- src/components/Dashboard/NewReport/ReportHeader/index.tsx | 1 + src/components/Dashboard/NewReport/styles.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 0c51487a8..4a3f7bbd3 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -110,6 +110,7 @@ export const ReportHeader = ({ const newItemEnv = environments?.find((x) => x.id === newItem[0]) ?? null; setSelectedEnvironment(newItemEnv); + setSelectedServices([]); }; const handleSelectedServicesChanged = (option: string | string[]) => { diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts index e6079ba6a..04c73f685 100644 --- a/src/components/Dashboard/NewReport/styles.ts +++ b/src/components/Dashboard/NewReport/styles.ts @@ -9,6 +9,7 @@ export const Container = styled.div` padding: 24px; gap: 24px; box-sizing: border-box; + overflow: auto; `; export const Footer = styled.div` From 9d1608b473e900f4c5174b2a4887afcd3a9645b7 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 17 Sep 2024 17:56:56 +0200 Subject: [PATCH 13/26] Add HEX to RGB conversion utility --- src/utils/hexToRgb.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/utils/hexToRgb.ts diff --git a/src/utils/hexToRgb.ts b/src/utils/hexToRgb.ts new file mode 100644 index 000000000..0a691f375 --- /dev/null +++ b/src/utils/hexToRgb.ts @@ -0,0 +1,19 @@ +// Source: https://stackoverflow.com/a/5624139 + +export const hexToRgb = (hex: string) => { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace( + shorthandRegex, + (_, r: string, g: string, b: string) => r + r + g + g + b + b + ); + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } + : null; +}; From d9d2a6809efd9921ed5a83711dbc936adaccac2d Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 17 Sep 2024 18:22:12 +0200 Subject: [PATCH 14/26] Add feature toggle --- src/components/Navigation/KebabMenu/index.tsx | 20 ++++++++++++------- src/featureFlags.ts | 3 ++- src/types.ts | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/Navigation/KebabMenu/index.tsx b/src/components/Navigation/KebabMenu/index.tsx index 199edc35f..954338372 100644 --- a/src/components/Navigation/KebabMenu/index.tsx +++ b/src/components/Navigation/KebabMenu/index.tsx @@ -1,7 +1,8 @@ import { actions as globalActions } from "../../../actions"; import { DIGMA_DOCUMENTATION } from "../../../constants"; +import { getFeatureFlagValue } from "../../../featureFlags"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; -import { OpenInstallationWizardPayload } from "../../../types"; +import { FeatureFlag, OpenInstallationWizardPayload } from "../../../types"; import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { isDigmaEngineRunning } from "../../../utils/isDigmaEngineRunning"; @@ -21,6 +22,9 @@ import { KebabMenuProps } from "./types"; export const KebabMenu = ({ onClose }: KebabMenuProps) => { const { backendInfo, digmaStatus, environment } = useConfigSelector(); + const isDigmaMetricsEnabled = + backendInfo?.centralize && + getFeatureFlagValue(backendInfo, FeatureFlag.IS_METRICS_REPORT_ENABLED); const handleOnboardingClick = () => { sendUserActionTrackingEvent(trackingEvents.ONBOARDING_LINK_CLICKED); @@ -124,12 +128,14 @@ export const KebabMenu = ({ onClose }: KebabMenuProps) => { }); } - items.push({ - id: "metrics", - label: "Digma Metrics", - icon: , - onClick: handleReportClick - }); + if (isDigmaMetricsEnabled) { + items.push({ + id: "metrics", + label: "Digma Metrics", + icon: , + onClick: handleReportClick + }); + } items.push({ id: "digma_docs", diff --git a/src/featureFlags.ts b/src/featureFlags.ts index 0f49c7212..00b016556 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -14,7 +14,8 @@ export const featureFlagMinBackendVersions: Record = { [FeatureFlag.IS_REPORT_ENABLED]: "0.3.95", [FeatureFlag.ARE_ISSUES_SERVICES_FILTERS_ENABLED]: "0.3.103", [FeatureFlag.ARE_EXTENDED_ASSETS_FILTERS_ENABLED]: "0.3.107", - [FeatureFlag.IS_NEW_IMPACT_SCORE_CALCULATION_ENABLED]: "0.3.107" + [FeatureFlag.IS_NEW_IMPACT_SCORE_CALCULATION_ENABLED]: "0.3.107", + [FeatureFlag.IS_METRICS_REPORT_ENABLED]: "0.3.120" }; export const getFeatureFlagValue = ( diff --git a/src/types.ts b/src/types.ts index c2cabb0cf..2120ae17a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,8 @@ export enum FeatureFlag { IS_REPORT_ENABLED, ARE_ISSUES_SERVICES_FILTERS_ENABLED, ARE_EXTENDED_ASSETS_FILTERS_ENABLED, - IS_NEW_IMPACT_SCORE_CALCULATION_ENABLED + IS_NEW_IMPACT_SCORE_CALCULATION_ENABLED, + IS_METRICS_REPORT_ENABLED } export enum InsightType { From dbbc872ffe44c0507e935b85da6ad1c96c2e1398 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 17 Sep 2024 18:38:31 +0200 Subject: [PATCH 15/26] Fix number format --- .../NewReport/Chart/ServiceTile/index.tsx | 60 ++++++++++--------- .../NewReport/Chart/ServiceTile/styles.ts | 2 +- src/components/common/TreeMap/Tile/styles.ts | 8 +-- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx index 313451071..40d4416f6 100644 --- a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx @@ -1,17 +1,14 @@ import { Tile } from "../../../../common/TreeMap/Tile"; +import { ReportTimeMode } from "../../ReportHeader/types"; import * as s from "./styles"; import { TooltipKeyValue } from "./TooltipKeyValue"; import { ServiceTileProps } from "./types"; -const getNumberSign = (value: number) => { - if (value > 0) { +const getFormattedNumber = (viewMode: ReportTimeMode, value: number) => { + if (viewMode === "changes" && value > 0) { return "+"; } - if (value < 0) { - return "-"; - } - return ""; }; @@ -21,24 +18,33 @@ export const ServiceTile = ({ impactScore, severity, viewMode -}: ServiceTileProps) => ( - - {name} - - {viewMode === "changes" && getNumberSign(impactScore)} - {criticalIssuesCount} - - {impactScore} - - } - > - - {criticalIssuesCount} - | {impactScore} - - -); +}: ServiceTileProps) => { + const formattedCriticalIssuesCount = getFormattedNumber( + viewMode, + criticalIssuesCount + ); + const formattedImpactScore = getFormattedNumber(viewMode, impactScore); + + return ( + + {name} + + {formattedCriticalIssuesCount} + + + {formattedImpactScore} + + + } + > + + {formattedCriticalIssuesCount} + | {formattedImpactScore} + + + ); +}; diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts b/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts index 3c69d2c67..c354ed764 100644 --- a/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts @@ -1,7 +1,7 @@ import styled from "styled-components"; export const StatsMainNumber = styled.span` - color: ${(props) => props.theme.colors.v3.text.primary}; + color: ${({ theme }) => theme.colors.v3.text.primary}; `; export const TooltipContent = styled.div` diff --git a/src/components/common/TreeMap/Tile/styles.ts b/src/components/common/TreeMap/Tile/styles.ts index 07eeed8d3..9190a92da 100644 --- a/src/components/common/TreeMap/Tile/styles.ts +++ b/src/components/common/TreeMap/Tile/styles.ts @@ -39,7 +39,7 @@ export const ContentContainer = styled.div` `; export const Title = styled.span` - color: ${(props) => props.theme.colors.v3.text.primary}; + color: ${({ theme }) => theme.colors.v3.text.primary}; font-size: 32px; font-weight: 400; overflow: hidden; @@ -51,11 +51,11 @@ export const ChildrenContainer = styled.div` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: ${(props) => { - const rgb = hexToRgb(props.theme.colors.v3.text.primary); + color: ${({ theme }) => { + const rgb = hexToRgb(theme.colors.v3.text.primary); return rgb ? `rgba(${rgb.r} ${rgb.g} ${rgb.b} / 70%)` - : props.theme.colors.v3.text.primary; + : theme.colors.v3.text.primary; }}; font-size: 32px; font-weight: 700; From 6c6e8c36b47493a8a350432fb7d56dae5ba50db3 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 20:06:31 +0300 Subject: [PATCH 16/26] update styles --- .../Dashboard/NewReport/Chart/index.tsx | 15 +--------- .../NewReport/MetricsTable/index.tsx | 30 +++++++++++-------- .../NewReport/ReportHeader/index.tsx | 19 ++++++++---- src/components/Dashboard/NewReport/utils.ts | 12 ++++++++ 4 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/components/Dashboard/NewReport/Chart/index.tsx b/src/components/Dashboard/NewReport/Chart/index.tsx index 5261f660d..ea29627a5 100644 --- a/src/components/Dashboard/NewReport/Chart/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/index.tsx @@ -3,25 +3,12 @@ import { Input } from "squarify"; import { isNumber } from "../../../../typeGuards/isNumber"; import { TreeMap } from "../../../common/TreeMap"; import { TileData } from "../../../common/TreeMap/types"; -import { Severity } from "../MetricsTable/types"; import { ReportTimeMode } from "../ReportHeader/types"; -import { getRank } from "../utils"; +import { getChangesSeverity, getRank } from "../utils"; import { ServiceTile } from "./ServiceTile"; import * as s from "./styles"; import { ChartProps } from "./types"; -const getChangesSeverity = (impactScore: number): Severity => { - if (impactScore < 0) { - return "Low"; - } - - if (impactScore > 0) { - return "Critical"; - } - - return "High"; -}; - export const Chart = ({ data }: ChartProps) => { const { width, height, observe } = useDimensions(); diff --git a/src/components/Dashboard/NewReport/MetricsTable/index.tsx b/src/components/Dashboard/NewReport/MetricsTable/index.tsx index 8ea46f500..8babb3262 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/index.tsx +++ b/src/components/Dashboard/NewReport/MetricsTable/index.tsx @@ -11,7 +11,7 @@ import { isUndefined } from "../../../../typeGuards/isUndefined"; import { SortIcon } from "../../../common/icons/16px/SortIcon"; import { SORTING_ORDER } from "../../../common/SortingSelector/types"; import { ServiceData } from "../types"; -import { getRank } from "../utils"; +import { getChangesSeverity, getRank } from "../utils"; import * as s from "./styles"; import { ColumnMeta, MetricsTableProps, Severity } from "./types"; @@ -52,18 +52,24 @@ export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { contentAlign: "center" } }), - columnHelper.accessor((row) => getRank(maxImpact, row.impact), { - header: "Rank", - id: "rank", - enableSorting: true, - cell: (info) => { - return info.getValue(); - }, - sortingFn: sortingFns.alphanumeric, - meta: { - contentAlign: "center" + columnHelper.accessor( + (row) => + row.key.lastDays + ? getChangesSeverity(row.impact) + : getRank(maxImpact, row.impact), + { + header: "Rank", + id: "rank", + enableSorting: true, + cell: (info) => { + return info.getValue(); + }, + sortingFn: sortingFns.alphanumeric, + meta: { + contentAlign: "center" + } } - }) + ) ]; const table = useReactTable({ diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 4a3f7bbd3..79bd607be 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -11,6 +11,8 @@ import { actions } from "../../actions"; import { usePrevious } from "../../../../hooks/usePrevious"; import { Environment } from "../../../common/App/types"; +import { isEnvironment } from "../../../../typeGuards/isEnvironment"; +import { isNumber } from "../../../../typeGuards/isNumber"; import { CodeIcon } from "../../../common/icons/12px/CodeIcon"; import { DurationBreakdownIcon } from "../../../common/icons/12px/DurationBreakdownIcon"; import { InfinityIcon } from "../../../common/icons/16px/InfinityIcon"; @@ -48,7 +50,7 @@ export const ReportHeader = ({ const [selectedEnvironment, setSelectedEnvironment] = useState(null); - const previousServices = usePrevious(selectedServices); + const previousSelectedServices = usePrevious(selectedServices); const previousEnvironment = usePrevious(selectedEnvironment); const previousTimeMode = usePrevious(timeMode); const previousPeriod = usePrevious(periodInDays); @@ -62,6 +64,7 @@ export const ReportHeader = ({ GetServicesPayload, string[] >(dataFetcherFiltersConfiguration, getServicesPayload); + const previousServices = usePrevious(services); useEffect(() => { getData(); @@ -73,12 +76,15 @@ export const ReportHeader = ({ ); setSelectedServices([]); }, [environments]); + useEffect(() => { if ( - previousEnvironment !== selectedEnvironment || - previousServices !== selectedServices || previousTimeMode !== timeMode || - previousPeriod !== periodInDays + (isEnvironment(selectedEnvironment) && + previousEnvironment !== selectedEnvironment) || + previousSelectedServices !== selectedServices || + services !== previousServices || + (isNumber(periodInDays) && previousPeriod !== periodInDays) ) { onFilterChanged({ lastDays: timeMode === "baseline" ? null : periodInDays, @@ -94,10 +100,11 @@ export const ReportHeader = ({ selectedEnvironment, onFilterChanged, previousEnvironment, - previousServices, previousTimeMode, previousPeriod, - services + services, + previousSelectedServices, + previousServices ]); const handleSelectedEnvironmentChanged = (option: string | string[]) => { diff --git a/src/components/Dashboard/NewReport/utils.ts b/src/components/Dashboard/NewReport/utils.ts index 0c542af5d..25b0931cf 100644 --- a/src/components/Dashboard/NewReport/utils.ts +++ b/src/components/Dashboard/NewReport/utils.ts @@ -18,3 +18,15 @@ export const getRank = (maxImpactScore: number, value: number): Severity => { return "Critical"; }; + +export const getChangesSeverity = (impactScore: number): Severity => { + if (impactScore < 0) { + return "Low"; + } + + if (impactScore > 0) { + return "Critical"; + } + + return "Medium"; +}; From c7128fd055186f83b8956beec6886b46a532bcb2 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 17 Sep 2024 20:48:11 +0200 Subject: [PATCH 17/26] Update coloring logic and styles --- .../NewReport/Chart/ServiceTile/index.tsx | 9 ++--- .../Dashboard/NewReport/Chart/index.tsx | 14 ++++---- .../NewReport/MetricsTable/index.tsx | 8 ++--- .../NewReport/MetricsTable/styles.ts | 2 +- .../Dashboard/NewReport/MetricsTable/types.ts | 2 +- src/components/Dashboard/NewReport/index.tsx | 8 ++--- src/components/Dashboard/NewReport/utils.ts | 34 ++++++++----------- src/components/common/TreeMap/Tile/styles.ts | 2 +- .../common/TreeMap/TreeMap.stories.tsx | 2 +- src/components/common/TreeMap/index.tsx | 12 +++---- 10 files changed, 41 insertions(+), 52 deletions(-) diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx index 40d4416f6..5c6dc9117 100644 --- a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx @@ -4,13 +4,8 @@ import * as s from "./styles"; import { TooltipKeyValue } from "./TooltipKeyValue"; import { ServiceTileProps } from "./types"; -const getFormattedNumber = (viewMode: ReportTimeMode, value: number) => { - if (viewMode === "changes" && value > 0) { - return "+"; - } - - return ""; -}; +const getFormattedNumber = (viewMode: ReportTimeMode, value: number) => + `${viewMode === "changes" && value > 0 ? "+" : ""}${value}`; export const ServiceTile = ({ name, diff --git a/src/components/Dashboard/NewReport/Chart/index.tsx b/src/components/Dashboard/NewReport/Chart/index.tsx index ea29627a5..c03ae9225 100644 --- a/src/components/Dashboard/NewReport/Chart/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/index.tsx @@ -4,7 +4,7 @@ import { isNumber } from "../../../../typeGuards/isNumber"; import { TreeMap } from "../../../common/TreeMap"; import { TileData } from "../../../common/TreeMap/types"; import { ReportTimeMode } from "../ReportHeader/types"; -import { getChangesSeverity, getRank } from "../utils"; +import { getSeverity } from "../utils"; import { ServiceTile } from "./ServiceTile"; import * as s from "./styles"; import { ChartProps } from "./types"; @@ -20,16 +20,18 @@ export const Chart = ({ data }: ChartProps) => { const transformedData = data.map((service) => ({ ...service, - impact: Math.trunc(service.impact * 100) + impact: Math.round(service.impact * 100) })); + const minImpactScore = Math.min(...transformedData.map((x) => x.impact)); const maxImpactScore = Math.max(...transformedData.map((x) => x.impact)); const chartData: Input[] = transformedData.map((service) => { - const severity = - viewMode === "baseline" - ? getRank(maxImpactScore, service.impact) - : getChangesSeverity(service.impact); + const severity = getSeverity( + minImpactScore, + maxImpactScore, + service.impact + ); return { id: service.key.service, diff --git a/src/components/Dashboard/NewReport/MetricsTable/index.tsx b/src/components/Dashboard/NewReport/MetricsTable/index.tsx index 8babb3262..7f4437933 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/index.tsx +++ b/src/components/Dashboard/NewReport/MetricsTable/index.tsx @@ -11,12 +11,13 @@ import { isUndefined } from "../../../../typeGuards/isUndefined"; import { SortIcon } from "../../../common/icons/16px/SortIcon"; import { SORTING_ORDER } from "../../../common/SortingSelector/types"; import { ServiceData } from "../types"; -import { getChangesSeverity, getRank } from "../utils"; +import { getSeverity } from "../utils"; import * as s from "./styles"; import { ColumnMeta, MetricsTableProps, Severity } from "./types"; export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { const columnHelper = createColumnHelper(); + const minImpact = Math.min(...data.map((x) => x.impact)); const maxImpact = Math.max(...data.map((x) => x.impact)); const columns = [ @@ -53,10 +54,7 @@ export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { } }), columnHelper.accessor( - (row) => - row.key.lastDays - ? getChangesSeverity(row.impact) - : getRank(maxImpact, row.impact), + (row) => getSeverity(minImpact, maxImpact, row.impact), { header: "Rank", id: "rank", diff --git a/src/components/Dashboard/NewReport/MetricsTable/styles.ts b/src/components/Dashboard/NewReport/MetricsTable/styles.ts index 38794a9f4..041f1c81f 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/styles.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/styles.ts @@ -64,7 +64,7 @@ export const TableBodyCell = styled.td` border: 1px solid ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; background: ${({ $severity }) => { switch ($severity) { - case "Critical": + case "Top": return "radial-gradient(1166.07% 138.62% at 0% 0%, #B92B2B 0%, #B95E2B 100%)"; case "High": return "radial-gradient(129.2% 111.8% at 0% 0%, #B95E2B 0%, #B9A22B 100%)"; diff --git a/src/components/Dashboard/NewReport/MetricsTable/types.ts b/src/components/Dashboard/NewReport/MetricsTable/types.ts index ac9b962f3..6167592fa 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/types.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/types.ts @@ -6,7 +6,7 @@ export interface MetricsTableProps { } export type ContentAlignment = "left" | "center" | "right"; -export type Severity = "Critical" | "High" | "Medium" | "Low"; +export type Severity = "Top" | "High" | "Medium" | "Low"; export interface ColumnMeta { contentAlign?: ContentAlignment; diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index c31eb5335..288af6f7d 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -49,11 +49,11 @@ export const NewReport = () => { )} {viewMode === "treemap" && } + + + © 2024 digma.ai +
- - - © 2024 digma.ai -
); }; diff --git a/src/components/Dashboard/NewReport/utils.ts b/src/components/Dashboard/NewReport/utils.ts index 25b0931cf..651097a59 100644 --- a/src/components/Dashboard/NewReport/utils.ts +++ b/src/components/Dashboard/NewReport/utils.ts @@ -1,32 +1,26 @@ import { Severity } from "./MetricsTable/types"; -export const getRank = (maxImpactScore: number, value: number): Severity => { - const rangeStep = maxImpactScore / 4; - const rangeValue = value / rangeStep; - - if (rangeValue < 1) { +export const getSeverity = ( + min: number, + max: number, + value: number +): Severity => { + const range = max - min; + const lowThreshold = min + 0.15 * range; + const mediumThreshold = min + 0.5 * range; + const highThreshold = min + 0.85 * range; + + if (value <= lowThreshold) { return "Low"; } - if (rangeValue < 2) { + if (value <= mediumThreshold) { return "Medium"; } - if (rangeValue < 3) { + if (value <= highThreshold) { return "High"; } - return "Critical"; -}; - -export const getChangesSeverity = (impactScore: number): Severity => { - if (impactScore < 0) { - return "Low"; - } - - if (impactScore > 0) { - return "Critical"; - } - - return "Medium"; + return "Top"; }; diff --git a/src/components/common/TreeMap/Tile/styles.ts b/src/components/common/TreeMap/Tile/styles.ts index 9190a92da..138891956 100644 --- a/src/components/common/TreeMap/Tile/styles.ts +++ b/src/components/common/TreeMap/Tile/styles.ts @@ -17,7 +17,7 @@ export const TileContainer = styled.div` border-radius: 12px; background: ${({ $severity }) => { switch ($severity) { - case "Critical": + case "Top": return "radial-gradient(1166.07% 138.62% at 0% 0%, #B92B2B 0%, #B95E2B 100%)"; case "High": return "radial-gradient(129.2% 111.8% at 0% 0%, #B95E2B 0%, #B9A22B 100%)"; diff --git a/src/components/common/TreeMap/TreeMap.stories.tsx b/src/components/common/TreeMap/TreeMap.stories.tsx index 68ee49140..91b4ffc04 100644 --- a/src/components/common/TreeMap/TreeMap.stories.tsx +++ b/src/components/common/TreeMap/TreeMap.stories.tsx @@ -28,7 +28,7 @@ export const Default: Story = { name={"Payment Service"} criticalIssuesCount={12} impactScore={1500} - severity={"Critical"} + severity={"Top"} viewMode={"baseline"} /> ) diff --git a/src/components/common/TreeMap/index.tsx b/src/components/common/TreeMap/index.tsx index 541934fbf..fe4bbad35 100644 --- a/src/components/common/TreeMap/index.tsx +++ b/src/components/common/TreeMap/index.tsx @@ -14,8 +14,8 @@ export const TreeMap = ({ padding = 0, data, width, height }: TreeMapProps) => { content: item.content }; }); - - const tiles = squarify(normalizedData, container); + const sortedData = [...normalizedData].sort((a, b) => b.value - a.value); + const tiles = squarify(sortedData, container); // Transform coordinates to add paddings between tiles const transformedTiles = padding @@ -27,10 +27,10 @@ export const TreeMap = ({ padding = 0, data, width, height }: TreeMapProps) => { return { ...tile, - x0: isLeftEdge ? tile.x0 : tile.x0 + padding / 2, - y0: isTopEdge ? tile.y0 : tile.y0 + padding / 2, - x1: isRightEdge ? tile.x1 : tile.x1 - padding / 2, - y1: isBottomEdge ? tile.y1 : tile.y1 - padding / 2 + x0: isLeftEdge ? tile.x0 : tile.x0 + padding, + y0: isTopEdge ? tile.y0 : tile.y0 + padding, + x1: isRightEdge ? tile.x1 : tile.x1 - padding, + y1: isBottomEdge ? tile.y1 : tile.y1 - padding }; }) : tiles; From 93ed4fa3af117fbc0f8a357d84a28ce40d2a5600 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 22:11:11 +0300 Subject: [PATCH 18/26] Update conditions for service filter --- .../NewReport/ReportHeader/index.tsx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 79bd607be..fb117f1ab 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -47,6 +47,7 @@ export const ReportHeader = ({ const [viewMode, setVieMode] = useState("table"); const [timeMode, setTimeMode] = useState("baseline"); const [selectedServices, setSelectedServices] = useState([]); + const [servicesFromStore, setServicesFromStore] = useState([]); const [selectedEnvironment, setSelectedEnvironment] = useState(null); @@ -66,6 +67,10 @@ export const ReportHeader = ({ >(dataFetcherFiltersConfiguration, getServicesPayload); const previousServices = usePrevious(services); + useEffect(() => { + setServicesFromStore(services ?? []); + }, [services, setServicesFromStore]); + useEffect(() => { getData(); }, []); @@ -74,22 +79,43 @@ export const ReportHeader = ({ setSelectedEnvironment( environments?.length && environments?.length > 0 ? environments[0] : null ); - setSelectedServices([]); + setServicesFromStore([]); }, [environments]); useEffect(() => { + if (servicesFromStore.length > 0) { + onFilterChanged({ + lastDays: timeMode === "baseline" ? null : periodInDays, + services: + selectedServices.length > 0 + ? selectedServices + : servicesFromStore ?? [], + environmentId: selectedEnvironment?.id ?? null + }); + } + }, [servicesFromStore]); + + useEffect(() => { + if ( + getServicesPayload.environment !== selectedEnvironment?.id || + servicesFromStore.length === 0 + ) { + return; + } + if ( previousTimeMode !== timeMode || (isEnvironment(selectedEnvironment) && previousEnvironment !== selectedEnvironment) || previousSelectedServices !== selectedServices || - services !== previousServices || (isNumber(periodInDays) && previousPeriod !== periodInDays) ) { onFilterChanged({ lastDays: timeMode === "baseline" ? null : periodInDays, services: - selectedServices.length > 0 ? selectedServices : services ?? [], + selectedServices.length > 0 + ? selectedServices + : servicesFromStore ?? [], environmentId: selectedEnvironment?.id ?? null }); } @@ -102,9 +128,10 @@ export const ReportHeader = ({ previousEnvironment, previousTimeMode, previousPeriod, - services, previousSelectedServices, - previousServices + previousServices, + getServicesPayload, + servicesFromStore ]); const handleSelectedEnvironmentChanged = (option: string | string[]) => { @@ -118,6 +145,7 @@ export const ReportHeader = ({ const newItemEnv = environments?.find((x) => x.id === newItem[0]) ?? null; setSelectedEnvironment(newItemEnv); setSelectedServices([]); + setServicesFromStore([]); }; const handleSelectedServicesChanged = (option: string | string[]) => { From 4ba91c2759303052a81a78282cb413aea32bf50c Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 17 Sep 2024 21:31:16 +0200 Subject: [PATCH 19/26] Fix feature flag version --- src/featureFlags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/featureFlags.ts b/src/featureFlags.ts index 00b016556..87879db68 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -15,7 +15,7 @@ export const featureFlagMinBackendVersions: Record = { [FeatureFlag.ARE_ISSUES_SERVICES_FILTERS_ENABLED]: "0.3.103", [FeatureFlag.ARE_EXTENDED_ASSETS_FILTERS_ENABLED]: "0.3.107", [FeatureFlag.IS_NEW_IMPACT_SCORE_CALCULATION_ENABLED]: "0.3.107", - [FeatureFlag.IS_METRICS_REPORT_ENABLED]: "0.3.120" + [FeatureFlag.IS_METRICS_REPORT_ENABLED]: "0.3.120-alpha.15" }; export const getFeatureFlagValue = ( From f879a36b02a14d6baa58b5d2fc8d0cd71e146e7d Mon Sep 17 00:00:00 2001 From: opoliarush Date: Tue, 17 Sep 2024 22:35:09 +0300 Subject: [PATCH 20/26] Added empty state --- .../Dashboard/NewReport/ReportHeader/index.tsx | 18 ++++++++---------- src/components/Dashboard/NewReport/index.tsx | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index fb117f1ab..99779f6c6 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -83,16 +83,14 @@ export const ReportHeader = ({ }, [environments]); useEffect(() => { - if (servicesFromStore.length > 0) { - onFilterChanged({ - lastDays: timeMode === "baseline" ? null : periodInDays, - services: - selectedServices.length > 0 - ? selectedServices - : servicesFromStore ?? [], - environmentId: selectedEnvironment?.id ?? null - }); - } + onFilterChanged({ + lastDays: timeMode === "baseline" ? null : periodInDays, + services: + selectedServices.length > 0 + ? selectedServices + : servicesFromStore ?? [], + environmentId: selectedEnvironment?.id ?? null + }); }, [servicesFromStore]); useEffect(() => { diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index 288af6f7d..4ef29132d 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -34,7 +34,7 @@ export const NewReport = () => { setViewMode(value); }; - const serviceData = data?.reports ?? []; + const serviceData = (query?.services.length > 0 ? data?.reports : null) ?? []; return ( From 6554b2451ac9e9ddc74624acbf2f2f9012669580 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 17 Sep 2024 22:03:39 +0200 Subject: [PATCH 21/26] Update styles --- .../Dashboard/NewReport/MetricsTable/styles.ts | 4 ++-- src/components/Dashboard/NewReport/styles.ts | 3 +-- src/components/Dashboard/NewReport/utils.ts | 9 +++++---- src/components/common/v3/Select/index.tsx | 6 +++++- src/components/common/v3/Select/styles.ts | 4 ++++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/Dashboard/NewReport/MetricsTable/styles.ts b/src/components/Dashboard/NewReport/MetricsTable/styles.ts index 041f1c81f..6844e2bcc 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/styles.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/styles.ts @@ -20,7 +20,7 @@ export const TableHead = styled.thead` `; export const TableHeaderCell = styled.th` - vertical-align: top; + height: 68px; `; export const TableCellContent = styled.div` @@ -51,7 +51,7 @@ export const TableHeaderCellContent = styled(TableCellContent)` export const TableBodyRow = styled.tr` ${subheadingBoldTypography} color: ${({ theme }) => theme.colors.v3.text.primary}; - height: 38px; + height: 68px; border-spacing: 0; &:hover { diff --git a/src/components/Dashboard/NewReport/styles.ts b/src/components/Dashboard/NewReport/styles.ts index 04c73f685..a3a1c2896 100644 --- a/src/components/Dashboard/NewReport/styles.ts +++ b/src/components/Dashboard/NewReport/styles.ts @@ -6,14 +6,13 @@ export const Container = styled.div` flex-direction: column; height: 100%; width: 100%; - padding: 24px; + padding: 24px 24px 16px; gap: 24px; box-sizing: border-box; overflow: auto; `; export const Footer = styled.div` - padding: 24px 32px 16px; align-items: center; display: flex; justify-content: start; diff --git a/src/components/Dashboard/NewReport/utils.ts b/src/components/Dashboard/NewReport/utils.ts index 651097a59..8865a0afc 100644 --- a/src/components/Dashboard/NewReport/utils.ts +++ b/src/components/Dashboard/NewReport/utils.ts @@ -5,10 +5,11 @@ export const getSeverity = ( max: number, value: number ): Severity => { - const range = max - min; - const lowThreshold = min + 0.15 * range; - const mediumThreshold = min + 0.5 * range; - const highThreshold = min + 0.85 * range; + const normalizedMin = Math.max(min, 0); + const range = max - normalizedMin; + const lowThreshold = normalizedMin + 0.15 * range; + const mediumThreshold = normalizedMin + 0.5 * range; + const highThreshold = normalizedMin + 0.85 * range; if (value <= lowThreshold) { return "Low"; diff --git a/src/components/common/v3/Select/index.tsx b/src/components/common/v3/Select/index.tsx index 98aa55725..63ed55560 100644 --- a/src/components/common/v3/Select/index.tsx +++ b/src/components/common/v3/Select/index.tsx @@ -161,7 +161,11 @@ export const Select = ({ disabled={disabled} className={className} > - {Icon && } + {Icon && ( + + + + )} {isString(placeholder) && {placeholder}} {multiselect && isSelectedStateEnabled && selectedValues.length > 0 && ( {selectedValues.length} diff --git a/src/components/common/v3/Select/styles.ts b/src/components/common/v3/Select/styles.ts index 38500d238..556108bcf 100644 --- a/src/components/common/v3/Select/styles.ts +++ b/src/components/common/v3/Select/styles.ts @@ -49,6 +49,10 @@ export const Button = styled.button` } `; +export const ButtonIconContainer = styled.div` + display: flex; +`; + export const ButtonLabel = styled.span` ${subscriptRegularTypography} margin-right: auto; From c92f9b67150cdf8b8ab81d44385b0a76766206ca Mon Sep 17 00:00:00 2001 From: opoliarush Date: Wed, 18 Sep 2024 17:19:22 +0300 Subject: [PATCH 22/26] Update font fot tile --- src/components/Dashboard/NewReport/Chart/Chart.stories.tsx | 7 ++++++- src/components/common/TreeMap/Tile/styles.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Dashboard/NewReport/Chart/Chart.stories.tsx b/src/components/Dashboard/NewReport/Chart/Chart.stories.tsx index c5d33114b..8e516c27c 100644 --- a/src/components/Dashboard/NewReport/Chart/Chart.stories.tsx +++ b/src/components/Dashboard/NewReport/Chart/Chart.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { Chart } from "."; +import { mockedReport } from "../MetricsTable/mockData"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta: Meta = { @@ -17,4 +18,8 @@ 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: { + data: mockedReport.reports + } +}; diff --git a/src/components/common/TreeMap/Tile/styles.ts b/src/components/common/TreeMap/Tile/styles.ts index 138891956..d36741031 100644 --- a/src/components/common/TreeMap/Tile/styles.ts +++ b/src/components/common/TreeMap/Tile/styles.ts @@ -40,7 +40,7 @@ export const ContentContainer = styled.div` export const Title = styled.span` color: ${({ theme }) => theme.colors.v3.text.primary}; - font-size: 32px; + font-size: 24px; font-weight: 400; overflow: hidden; text-overflow: ellipsis; From 1200387b3fbd25f893e230a140cec72f419fdb25 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Wed, 18 Sep 2024 18:50:12 +0300 Subject: [PATCH 23/26] fix ui --- .../Dashboard/NewReport/ReportHeader/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Dashboard/NewReport/ReportHeader/index.tsx b/src/components/Dashboard/NewReport/ReportHeader/index.tsx index 99779f6c6..00b30e65d 100644 --- a/src/components/Dashboard/NewReport/ReportHeader/index.tsx +++ b/src/components/Dashboard/NewReport/ReportHeader/index.tsx @@ -83,6 +83,10 @@ export const ReportHeader = ({ }, [environments]); useEffect(() => { + if (!selectedEnvironment?.id) { + return; + } + onFilterChanged({ lastDays: timeMode === "baseline" ? null : periodInDays, services: @@ -91,11 +95,12 @@ export const ReportHeader = ({ : servicesFromStore ?? [], environmentId: selectedEnvironment?.id ?? null }); - }, [servicesFromStore]); + }, [servicesFromStore, selectedEnvironment]); useEffect(() => { if ( - getServicesPayload.environment !== selectedEnvironment?.id || + !selectedEnvironment?.id || + getServicesPayload.environment !== selectedEnvironment.id || servicesFromStore.length === 0 ) { return; From 8793ed44c7c34f1dd6704b18c15ec078c09ed722 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Thu, 19 Sep 2024 13:08:59 +0200 Subject: [PATCH 24/26] Fix story --- src/components/common/TreeMap/TreeMap.stories.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/common/TreeMap/TreeMap.stories.tsx b/src/components/common/TreeMap/TreeMap.stories.tsx index 91b4ffc04..8908e6cbd 100644 --- a/src/components/common/TreeMap/TreeMap.stories.tsx +++ b/src/components/common/TreeMap/TreeMap.stories.tsx @@ -18,6 +18,8 @@ type Story = StoryObj; export const Default: Story = { args: { + width: 800, + height: 800, padding: 40, data: [ { From ebd453cc75619453d559737313eafb464051854f Mon Sep 17 00:00:00 2001 From: opoliarush Date: Thu, 19 Sep 2024 14:32:12 +0300 Subject: [PATCH 25/26] Drill down from heatmap --- .../NewReport/Chart/ServiceTile/index.tsx | 12 +++++++----- .../NewReport/Chart/ServiceTile/styles.ts | 12 ++++++++++++ .../NewReport/Chart/ServiceTile/types.ts | 1 + .../Dashboard/NewReport/Chart/index.tsx | 3 ++- .../Dashboard/NewReport/Chart/types.ts | 1 + src/components/Dashboard/NewReport/index.tsx | 19 ++++++++++++++++++- src/components/Main/index.tsx | 6 ++++++ src/components/Main/types.ts | 3 ++- 8 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx index 5c6dc9117..650a05027 100644 --- a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx @@ -12,7 +12,8 @@ export const ServiceTile = ({ criticalIssuesCount, impactScore, severity, - viewMode + viewMode, + onIssuesClick }: ServiceTileProps) => { const formattedCriticalIssuesCount = getFormattedNumber( viewMode, @@ -36,10 +37,11 @@ export const ServiceTile = ({ } > - - {formattedCriticalIssuesCount} - | {formattedImpactScore} - + onIssuesClick && onIssuesClick(name)}> + + {formattedCriticalIssuesCount} | {formattedImpactScore} + + ); }; diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts b/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts index c354ed764..d0f208364 100644 --- a/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/styles.ts @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { Link } from "../../../../common/v3/Link"; export const StatsMainNumber = styled.span` color: ${({ theme }) => theme.colors.v3.text.primary}; @@ -7,3 +8,14 @@ export const StatsMainNumber = styled.span` export const TooltipContent = styled.div` color: ${({ theme }) => theme.colors.v3.text.primary}; `; + +export const StyledLink = styled(Link)` + color: ${({ theme }) => theme.colors.v3.text.primary}; + font-size: 32px; + font-weight: 700; + line-height: normal; + + :hover { + text-decoration: underline; + } +`; diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts b/src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts index 8e0635322..acdf1c42f 100644 --- a/src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/types.ts @@ -7,4 +7,5 @@ export interface ServiceTileProps { impactScore: number; severity: Severity; viewMode: ReportTimeMode; + onIssuesClick?: (service: string) => void; } diff --git a/src/components/Dashboard/NewReport/Chart/index.tsx b/src/components/Dashboard/NewReport/Chart/index.tsx index c03ae9225..9a8ac83da 100644 --- a/src/components/Dashboard/NewReport/Chart/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/index.tsx @@ -9,7 +9,7 @@ import { ServiceTile } from "./ServiceTile"; import * as s from "./styles"; import { ChartProps } from "./types"; -export const Chart = ({ data }: ChartProps) => { +export const Chart = ({ data, onServiceSelected }: ChartProps) => { const { width, height, observe } = useDimensions(); const viewMode: ReportTimeMode = data.some((service) => @@ -43,6 +43,7 @@ export const Chart = ({ data }: ChartProps) => { impactScore={service.impact} severity={severity} viewMode={viewMode} + onIssuesClick={onServiceSelected} /> ) }; diff --git a/src/components/Dashboard/NewReport/Chart/types.ts b/src/components/Dashboard/NewReport/Chart/types.ts index 9a64f40c2..ccef973e0 100644 --- a/src/components/Dashboard/NewReport/Chart/types.ts +++ b/src/components/Dashboard/NewReport/Chart/types.ts @@ -2,4 +2,5 @@ import { ServiceData } from "../types"; export interface ChartProps { data: ServiceData[]; + onServiceSelected: (name: string) => void; } diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index 4ef29132d..f3e107477 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -1,5 +1,7 @@ import { useLayoutEffect, useState } from "react"; +import { changeScope } from "../../../utils/actions/changeScope"; import { DigmaLogoIcon } from "../../common/icons/16px/DigmaLogoIcon"; +import { SCOPE_CHANGE_EVENTS } from "../../Main/types"; import { actions } from "../actions"; import { Chart } from "./Chart"; import { MetricsTable } from "./MetricsTable"; @@ -34,6 +36,19 @@ export const NewReport = () => { setViewMode(value); }; + const handleServiceSelected = (name: string) => { + changeScope({ + span: null, + environmentId: query.environmentId ?? undefined, + context: { + event: SCOPE_CHANGE_EVENTS.METRICS_SERVICE_SELECTED, + payload: { + service: name + } + } + }); + }; + const serviceData = (query?.services.length > 0 ? data?.reports : null) ?? []; return ( @@ -48,7 +63,9 @@ export const NewReport = () => { {viewMode === "table" && ( )} - {viewMode === "treemap" && } + {viewMode === "treemap" && ( + + )} © 2024 digma.ai diff --git a/src/components/Main/index.tsx b/src/components/Main/index.tsx index c60836c57..22b3b15c2 100644 --- a/src/components/Main/index.tsx +++ b/src/components/Main/index.tsx @@ -213,6 +213,12 @@ export const Main = () => { case SCOPE_CHANGE_EVENTS.ASSETS_EMPTY_CATEGORY_PARENT_LINK_CLICKED as string: goTo(`/${TAB_IDS.ASSETS}`, { state }); break; + case SCOPE_CHANGE_EVENTS.METRICS_SERVICE_SELECTED as string: { + const serviceToSelect = scope.context.payload?.service as string; + setSelectedServices(serviceToSelect ? [serviceToSelect] : []); + goTo(`/${TAB_IDS.ISSUES}`, { state }); + break; + } case SCOPE_CHANGE_EVENTS.IDE_CODE_LENS_CLICKED as string: { const url = getURLToNavigateOnCodeLensClick(scope); if (url) { diff --git a/src/components/Main/types.ts b/src/components/Main/types.ts index 41017adaf..b795aea50 100644 --- a/src/components/Main/types.ts +++ b/src/components/Main/types.ts @@ -56,7 +56,8 @@ export enum SCOPE_CHANGE_EVENTS { RECENT_ACTIVITY_SPAN_LINK_CLICKED = "RECENT_ACTIVITY_SPAN_LINK_CLICKED", IDE_CODE_LENS_CLICKED = "IDE/CODE_LENS_CLICKED", IDE_NOTIFICATION_LINK_CLICKED = "IDE/NOTIFICATION_LINK_CLICKED", - ASSETS_EMPTY_CATEGORY_PARENT_LINK_CLICKED = "ASSETS/EMPTY_CATEGORY_PARENT_LINK_CLICKED" + ASSETS_EMPTY_CATEGORY_PARENT_LINK_CLICKED = "ASSETS/EMPTY_CATEGORY_PARENT_LINK_CLICKED", + METRICS_SERVICE_SELECTED = "METRICS/SERVICE_SELECTED" } export interface ReactRouterLocationState { From 4bd79d37134657fcfeb395705745a0ddb791ff08 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Thu, 19 Sep 2024 15:44:04 +0300 Subject: [PATCH 26/26] Added tracking events --- .../NewReport/Chart/ServiceTile/index.tsx | 4 +- .../Dashboard/NewReport/Chart/index.tsx | 9 ++- .../NewReport/MetricsTable/index.tsx | 65 ++++++++++++++++--- .../NewReport/MetricsTable/styles.ts | 33 ++++++++++ .../Dashboard/NewReport/MetricsTable/types.ts | 1 + src/components/Dashboard/NewReport/index.tsx | 6 +- .../Dashboard/NewReport/tracking.ts | 4 +- 7 files changed, 107 insertions(+), 15 deletions(-) diff --git a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx index 650a05027..907ead5ae 100644 --- a/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/ServiceTile/index.tsx @@ -13,7 +13,7 @@ export const ServiceTile = ({ impactScore, severity, viewMode, - onIssuesClick + onIssuesClick: onSeeIssuesClick }: ServiceTileProps) => { const formattedCriticalIssuesCount = getFormattedNumber( viewMode, @@ -37,7 +37,7 @@ export const ServiceTile = ({ } > - onIssuesClick && onIssuesClick(name)}> + onSeeIssuesClick && onSeeIssuesClick(name)}> {formattedCriticalIssuesCount} | {formattedImpactScore} diff --git a/src/components/Dashboard/NewReport/Chart/index.tsx b/src/components/Dashboard/NewReport/Chart/index.tsx index 9a8ac83da..5ef58b8e7 100644 --- a/src/components/Dashboard/NewReport/Chart/index.tsx +++ b/src/components/Dashboard/NewReport/Chart/index.tsx @@ -1,9 +1,11 @@ import useDimensions from "react-cool-dimensions"; import { Input } from "squarify"; import { isNumber } from "../../../../typeGuards/isNumber"; +import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; import { TreeMap } from "../../../common/TreeMap"; import { TileData } from "../../../common/TreeMap/types"; import { ReportTimeMode } from "../ReportHeader/types"; +import { trackingEvents } from "../tracking"; import { getSeverity } from "../utils"; import { ServiceTile } from "./ServiceTile"; import * as s from "./styles"; @@ -23,6 +25,11 @@ export const Chart = ({ data, onServiceSelected }: ChartProps) => { impact: Math.round(service.impact * 100) })); + const handSeeIssuesClick = (service: string) => { + sendUserActionTrackingEvent(trackingEvents.HEATMAP_SEE_ISSUES_LINK_CLICKED); + onServiceSelected(service); + }; + const minImpactScore = Math.min(...transformedData.map((x) => x.impact)); const maxImpactScore = Math.max(...transformedData.map((x) => x.impact)); @@ -43,7 +50,7 @@ export const Chart = ({ data, onServiceSelected }: ChartProps) => { impactScore={service.impact} severity={severity} viewMode={viewMode} - onIssuesClick={onServiceSelected} + onIssuesClick={handSeeIssuesClick} /> ) }; diff --git a/src/components/Dashboard/NewReport/MetricsTable/index.tsx b/src/components/Dashboard/NewReport/MetricsTable/index.tsx index 7f4437933..886724e80 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/index.tsx +++ b/src/components/Dashboard/NewReport/MetricsTable/index.tsx @@ -7,35 +7,72 @@ import { useReactTable } from "@tanstack/react-table"; +import { ReactNode } from "react"; import { isUndefined } from "../../../../typeGuards/isUndefined"; +import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; import { SortIcon } from "../../../common/icons/16px/SortIcon"; +import { ChevronIcon } from "../../../common/icons/20px/ChevronIcon"; +import { Direction } from "../../../common/icons/types"; import { SORTING_ORDER } from "../../../common/SortingSelector/types"; +import { trackingEvents } from "../tracking"; import { ServiceData } from "../types"; import { getSeverity } from "../utils"; import * as s from "./styles"; import { ColumnMeta, MetricsTableProps, Severity } from "./types"; -export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { +const IssuesLink = ({ + children, + onClick +}: { + onClick: () => void; + children: ReactNode; +}) => { + return ( + + {children} + + + + + ); +}; + +export const MetricsTable = ({ + data, + showSign, + onServiceSelected +}: MetricsTableProps) => { const columnHelper = createColumnHelper(); const minImpact = Math.min(...data.map((x) => x.impact)); const maxImpact = Math.max(...data.map((x) => x.impact)); + const handleSeeIssuesLinkClick = (service: string, source: string) => { + onServiceSelected(service); + sendUserActionTrackingEvent(trackingEvents.TABLE_SEE_ISSUES_LINK_CLICKED, { + source + }); + }; + const columns = [ columnHelper.accessor((row) => row.key.service, { header: "Service", enableSorting: false, cell: (info) => info.getValue() }), - columnHelper.accessor((row) => row.issues, { + columnHelper.accessor((row) => row, { header: "Critical issues", id: "issues", cell: (info) => { - const value = info.getValue(); - if (!showSign) { - return value; - } - - return `${value > 0 ? "+" : ""}${value}`; + const value = info.getValue().issues; + return ( + + handleSeeIssuesLinkClick(info.getValue().key.service, "issues") + } + > + {!showSign ? value : `${value > 0 ? "+" : ""}${value}`} + + ); }, sortingFn: sortingFns.alphanumeric, meta: { @@ -43,10 +80,18 @@ export const MetricsTable = ({ data, showSign }: MetricsTableProps) => { }, enableSorting: true }), - columnHelper.accessor((row) => row.impact, { + columnHelper.accessor((row) => row, { id: "impact", header: "Impact", - cell: (info) => Math.round(info.getValue() * 100), + cell: (info) => ( + + handleSeeIssuesLinkClick(info.getValue().key.service, "impact") + } + > + {Math.round(info.getValue().impact * 100)} + + ), sortingFn: sortingFns.alphanumeric, enableSorting: true, meta: { diff --git a/src/components/Dashboard/NewReport/MetricsTable/styles.ts b/src/components/Dashboard/NewReport/MetricsTable/styles.ts index 6844e2bcc..e514b4148 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/styles.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/styles.ts @@ -7,6 +7,7 @@ import { SORTING_ORDER, SortingOrderIconContainerProps } from "../../../common/SortingSelector/types"; +import { Link } from "../../../common/v3/Link"; import { TableBodyCellCellProps, TableCellContentProps } from "./types"; export const Table = styled.table` @@ -84,3 +85,35 @@ export const SortingOrderIconContainer = styled.div ($sortingOrder === SORTING_ORDER.DESC ? -1 : 1)} ); `; + +export const LinkChevron = styled.div` + display: none; +`; + +export const SeeIssuesLink = styled(Link)` + ${subheadingBoldTypography} + color: ${({ theme }) => theme.colors.v3.text.primary}; + min-width: 103px; + width: 100%; + + &:hover { + align-items: center; + justify-content: space-between; + display: flex; + + span { + display: none; + } + + ${LinkChevron} { + display: flex; + margin-left: auto; + } + } + + &:hover::before { + content: "See Issues"; + margin-left: auto; + color: ${({ theme }) => theme.colors.v3.text.link}; + } +`; diff --git a/src/components/Dashboard/NewReport/MetricsTable/types.ts b/src/components/Dashboard/NewReport/MetricsTable/types.ts index 6167592fa..e09b054fe 100644 --- a/src/components/Dashboard/NewReport/MetricsTable/types.ts +++ b/src/components/Dashboard/NewReport/MetricsTable/types.ts @@ -3,6 +3,7 @@ import { ServiceData } from "../types"; export interface MetricsTableProps { data: ServiceData[]; showSign: boolean; + onServiceSelected: (name: string) => void; } export type ContentAlignment = "left" | "center" | "right"; diff --git a/src/components/Dashboard/NewReport/index.tsx b/src/components/Dashboard/NewReport/index.tsx index f3e107477..49e83868e 100644 --- a/src/components/Dashboard/NewReport/index.tsx +++ b/src/components/Dashboard/NewReport/index.tsx @@ -61,7 +61,11 @@ export const NewReport = () => { onViewModeChanged={handleViewModeChange} /> {viewMode === "table" && ( - + )} {viewMode === "treemap" && ( diff --git a/src/components/Dashboard/NewReport/tracking.ts b/src/components/Dashboard/NewReport/tracking.ts index 77899a324..a7a8291fc 100644 --- a/src/components/Dashboard/NewReport/tracking.ts +++ b/src/components/Dashboard/NewReport/tracking.ts @@ -8,7 +8,9 @@ export const trackingEvents = addPrefix( REFRESH_DATA_CLICKED: "refresh report data clicked", DOWNLOAD_REPORT_CLICKED: "download report data clicked", ENVIRONMENT_FILTER_SELECTED: "environment filter selected", - SERVICES_FILTER_SELECTED: "service filter selected" + SERVICES_FILTER_SELECTED: "service filter selected", + TABLE_SEE_ISSUES_LINK_CLICKED: "table see issues link clicked", + HEATMAP_SEE_ISSUES_LINK_CLICKED: "heatmap see issues link clicked" }, " " );