From 478fb77ad06878c08b60abea38c678b5ae332aa6 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 6 Aug 2024 12:31:49 +0200 Subject: [PATCH 1/5] Add carousel buttons --- .vscode/settings.json | 3 +- .../EnvironmentSelector.stories.tsx | 84 ++++++++++++++++ .../EnvironmentSelector/index.tsx | 96 +++++++++---------- .../EnvironmentSelector/styles.ts | 29 +++++- .../EnvironmentSelector/types.ts | 6 ++ .../Insights/Issues/IssuesFilter/index.tsx | 2 +- src/components/common/v3/NewButton/styles.ts | 34 +++---- src/components/common/v3/NewButton/types.ts | 6 +- 8 files changed, 190 insertions(+), 70 deletions(-) create mode 100644 src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 3c80beaa0..5386dcb55 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,8 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "stylelint.validate": ["typescript"], "cSpell.words": [ - "Checkmark", + "borderless", + "checkmark", "digma", "digmathon", "digmo", diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx new file mode 100644 index 000000000..4e7475c70 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx @@ -0,0 +1,84 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { EnvironmentSelector } from "."; +import { SelectorEnvironment } from "./types"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Insights/InsightsCatalog/EnvironmentSelector", + component: EnvironmentSelector, + 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; + +const mockedEnvironments: SelectorEnvironment[] = [ + { + environment: { + id: "1", + name: "Development", + type: "Private" + }, + issueCounts: { + highCriticality: 1, + mediumCriticality: 2, + lowCriticality: 3 + } + }, + { + environment: { + id: "2", + name: "Staging", + type: "Private" + }, + issueCounts: { + highCriticality: 0, + mediumCriticality: 2, + lowCriticality: 3 + } + }, + { + environment: { + id: "3", + name: "Production", + type: "Private" + }, + issueCounts: { + highCriticality: 0, + mediumCriticality: 0, + lowCriticality: 3 + } + } +]; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = { + args: { + environments: mockedEnvironments + } +}; + +export const WithCarousel: Story = { + args: { + environments: [ + ...mockedEnvironments, + { + environment: { + id: "4", + name: "Custom", + type: "Private" + }, + issueCounts: { + highCriticality: 0, + mediumCriticality: 0, + lowCriticality: 1 + } + } + ] + } +}; diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx b/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx index 6e25f73d7..50e8d8be0 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx @@ -1,17 +1,19 @@ -import { useState } from "react"; import useDimensions from "react-cool-dimensions"; import { useGlobalStore } from "../../../../containers/Main/stores/useGlobalStore"; +import { logger } from "../../../../logging"; import { changeScope } from "../../../../utils/actions/changeScope"; import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; -import { Environment } from "../../../common/App/types"; -import { NewPopover } from "../../../common/NewPopover"; -import { NewButton } from "../../../common/v3/NewButton"; -import { EnvironmentMenu } from "../../../Navigation/EnvironmentBar/EnvironmentMenu"; +import { ChevronIcon } from "../../../common/icons/12px/ChevronIcon"; +import { Direction } from "../../../common/icons/types"; import { trackingEvents } from "../../tracking"; import { EnvironmentChip } from "./EnvironmentChip"; import { getMostCriticalIssueCount } from "./getMostCriticalIssueCount"; import * as s from "./styles"; -import { EnvironmentSelectorProps, SelectorEnvironment } from "./types"; +import { + EnvironmentSelectorProps, + ScrollDirection, + SelectorEnvironment +} from "./types"; const ENVIRONMENT_CHIP_COUNT = 3; @@ -61,8 +63,7 @@ export const EnvironmentSelector = ({ }: EnvironmentSelectorProps) => { const scope = useGlobalStore.use.scope(); const environment = useGlobalStore.use.environment(); - const [isMenuOpen, setIsMenuOpen] = useState(false); - const { observe, width } = useDimensions(); + const { observe } = useDimensions(); const sortedEnvironments = environments.sort( sortEnvironmentsByCriticalIssues ); @@ -84,23 +85,31 @@ export const EnvironmentSelector = ({ }); }; - const handleMenuItemClick = (environment: Environment) => { - setIsMenuOpen(false); - changeEnvironment(environment.id); - }; - const handleEnvironmentChipClick = (environmentId: string) => { changeEnvironment(environmentId); }; - const handleEnvironmentMenuOpenChange = (isOpen: boolean) => { - setIsMenuOpen(isOpen); - }; - - const handleEnvironmentMenuButtonClick = () => { - sendUserActionTrackingEvent(trackingEvents.ENVIRONMENT_MENU_BUTTON_CLICKED); - - setIsMenuOpen(!isMenuOpen); + const handleCarouselButtonClick = (direction: ScrollDirection) => { + logger.debug(direction); + // const { entry: containerEntry, width: containerWidth } = + // environmentListContainerDimensions; + // const { entry } = environmentListDimensions; + // if (containerEntry && entry) { + // let delta = containerWidth; + // if (direction === "left") { + // delta *= -1; + // } + // let newScrollLeft = containerEntry.target.scrollLeft + delta; + // const maxScrollLeft = containerEntry.target.scrollWidth - containerWidth; + // if (newScrollLeft >= maxScrollLeft) { + // newScrollLeft = maxScrollLeft; + // } + // if (newScrollLeft < 0) { + // newScrollLeft = 0; + // } + // setScrollLeft(newScrollLeft); + // containerEntry.target.scrollLeft = newScrollLeft; + // } }; const environmentIndex = sortedEnvironments.findIndex( @@ -116,13 +125,7 @@ export const EnvironmentSelector = ({ ) : sortedEnvironments; - const renderEnvironmentMenuButton = () => ( - - ); + const isCarouselVisible = sortedEnvironments.length > ENVIRONMENT_CHIP_COUNT; return ( @@ -137,28 +140,23 @@ export const EnvironmentSelector = ({ /> ))} - {sortedEnvironments.length > ENVIRONMENT_CHIP_COUNT && ( - <> - {/* // TODO: refactor this to use only popover */} - {isMenuOpen ? ( - x.environment)} - onMenuItemClick={handleMenuItemClick} - /> - } - onOpenChange={handleEnvironmentMenuOpenChange} - isOpen={isMenuOpen} - placement={"bottom-end"} - width={width} - > - {renderEnvironmentMenuButton()} - - ) : ( - renderEnvironmentMenuButton() + {isCarouselVisible && ( + ( + + )} + onClick={() => handleCarouselButtonClick("left")} + /> + )} + {isCarouselVisible && ( + ( + )} - + onClick={() => handleCarouselButtonClick("right")} + /> )} ); diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts b/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts index 7bc77e4d7..edb31e94b 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts @@ -1,10 +1,13 @@ -import styled from "styled-components"; +import styled, { css } from "styled-components"; +import { NewIconButton } from "../../../common/v3/NewIconButton"; +import { CarouselButtonProps } from "./types"; export const Container = styled.div` display: flex; gap: 4px; overflow: hidden; flex-grow: 1; + position: relative; `; export const EnvironmentsContainer = styled.div` @@ -17,3 +20,27 @@ export const EnvironmentsContainer = styled.div` flex: 1 1 0; } `; + +export const CarouselButton = styled(NewIconButton)` + position: absolute; + ${({ direction }) => + direction === "left" + ? css` + padding-left: 22px; + left: 0; + background: linear-gradient( + 90deg, + #222326 35%, + rgba(34 35 38 / 0%) 100% + ); + ` + : css` + padding-right: 22px; + right: 0; + background: linear-gradient( + 90deg, + rgba(34 35 38 / 0%) 4.55%, + #222326 60.23% + ); + `} +`; diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts b/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts index 53a5eea5e..c87992118 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts @@ -8,3 +8,9 @@ export interface SelectorEnvironment { export interface EnvironmentSelectorProps { environments: SelectorEnvironment[]; } + +export type ScrollDirection = "left" | "right"; + +export interface CarouselButtonProps { + direction: ScrollDirection; +} diff --git a/src/components/Insights/Issues/IssuesFilter/index.tsx b/src/components/Insights/Issues/IssuesFilter/index.tsx index 2d86e91e0..2b71f8623 100644 --- a/src/components/Insights/Issues/IssuesFilter/index.tsx +++ b/src/components/Insights/Issues/IssuesFilter/index.tsx @@ -181,7 +181,7 @@ export const IssuesFilter = () => { /> ` width: fit-content; padding: ${({ $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "6px 8px"; case "secondary": case "primary": @@ -23,7 +23,7 @@ export const Button = styled.button` }}; color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": case "secondary": return theme.colors.v3.icon.primary; case "primary": @@ -33,7 +33,7 @@ export const Button = styled.button` }}; border: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return `1px solid ${theme.colors.v3.stroke.dark}`; @@ -44,7 +44,7 @@ export const Button = styled.button` }}; background: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return theme.colors.v3.surface.primary; @@ -57,7 +57,7 @@ export const Button = styled.button` span { color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": case "secondary": return theme.colors.v3.text.primary; case "primary": @@ -78,7 +78,7 @@ export const Button = styled.button` cursor: initial; color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": case "secondary": return theme.colors.v3.icon.disabled; case "primary": @@ -88,7 +88,7 @@ export const Button = styled.button` }}; border: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return `1px solid ${theme.colors.v3.stroke.tertiary}`; @@ -99,7 +99,7 @@ export const Button = styled.button` }}; background: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return theme.colors.v3.surface.primaryLight; @@ -112,7 +112,7 @@ export const Button = styled.button` span { color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": case "secondary": return theme.colors.v3.text.disabled; case "primary": @@ -126,7 +126,7 @@ export const Button = styled.button` &:hover:enabled { color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return theme.colors.v3.icon.brandTertiary; case "secondary": return theme.colors.v3.icon.primary; @@ -137,7 +137,7 @@ export const Button = styled.button` }}; border: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return `1px solid ${theme.colors.v3.stroke.primary}`; @@ -148,7 +148,7 @@ export const Button = styled.button` }}; background: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return theme.colors.v3.surface.brandDark; @@ -161,7 +161,7 @@ export const Button = styled.button` span { color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return theme.colors.v3.text.link; case "secondary": return theme.colors.v3.text.primary; @@ -177,7 +177,7 @@ export const Button = styled.button` &:active:enabled { color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return theme.colors.v3.icon.brandSecondary; case "secondary": return theme.colors.v3.icon.primary; @@ -188,7 +188,7 @@ export const Button = styled.button` }}; border: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return `1px solid ${theme.colors.v3.stroke.primary}`; @@ -199,7 +199,7 @@ export const Button = styled.button` }}; background: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return "none"; case "secondary": return theme.colors.v3.surface.primary; @@ -212,7 +212,7 @@ export const Button = styled.button` span { color: ${({ theme, $type }) => { switch ($type) { - case "tertiary": + case "borderlessPrimary": return theme.colors.v3.surface.brandSecondary; case "secondary": return theme.colors.v3.text.primary; diff --git a/src/components/common/v3/NewButton/types.ts b/src/components/common/v3/NewButton/types.ts index 7af665314..4f83a6e8a 100644 --- a/src/components/common/v3/NewButton/types.ts +++ b/src/components/common/v3/NewButton/types.ts @@ -1,7 +1,11 @@ import { ButtonHTMLAttributes } from "react"; import { IconProps } from "../../icons/types"; -export type ButtonType = "primary" | "secondary" | "tertiary"; +export type ButtonType = + | "primary" + | "secondary" + | "borderlessPrimary" + | "borderlessSecondary"; export interface BaseButtonProps { icon?: React.ComponentType; From 955f1318562a649d427e6fafef1edc3cb777516a Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Fri, 9 Aug 2024 12:59:55 +0200 Subject: [PATCH 2/5] Reimplement carousel --- package-lock.json | 27 ++++ package.json | 1 + .../EnvironmentSelector.stories.tsx | 12 ++ .../EnvironmentSelector/index.tsx | 117 +++++------------- .../EnvironmentSelector/styles.ts | 103 +++++++++++---- .../common/Carousel/Carousel.stories.tsx | 40 ++++++ src/components/common/Carousel/index.tsx | 60 +++++++++ src/components/common/Carousel/styles.ts | 75 +++++++++++ src/components/common/Carousel/types.ts | 13 ++ src/utils/hexToRgb.ts | 19 +++ 10 files changed, 358 insertions(+), 109 deletions(-) create mode 100644 src/components/common/Carousel/Carousel.stories.tsx create mode 100644 src/components/common/Carousel/index.tsx create mode 100644 src/components/common/Carousel/styles.ts create mode 100644 src/components/common/Carousel/types.ts create mode 100644 src/utils/hexToRgb.ts diff --git a/package-lock.json b/package-lock.json index 676f4bf01..1387989d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@floating-ui/react": "^0.25.1", + "@splidejs/react-splide": "^0.7.12", "@tanstack/react-table": "^8.7.8", "allotment": "^1.19.0", "axios": "^1.7.2", @@ -3585,6 +3586,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@splidejs/react-splide": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@splidejs/react-splide/-/react-splide-0.7.12.tgz", + "integrity": "sha512-UfXH+j47jsMc4x5HA/aOwuuHPqn6y9+ZTNYPWDRD8iLKvIVMZlzq2unjUEvyDAU+TTVPZOXkG2Ojeoz0P4AkZw==", + "dependencies": { + "@splidejs/splide": "^4.1.3" + } + }, + "node_modules/@splidejs/splide": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-4.1.4.tgz", + "integrity": "sha512-5I30evTJcAJQXt6vJ26g2xEkG+l1nXcpEw4xpKh0/FWQ8ozmAeTbtniVtVmz2sH1Es3vgfC4SS8B2X4o5JMptA==" + }, "node_modules/@storybook/addon-a11y": { "version": "8.1.6", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.1.6.tgz", @@ -20536,6 +20550,19 @@ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true }, + "@splidejs/react-splide": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@splidejs/react-splide/-/react-splide-0.7.12.tgz", + "integrity": "sha512-UfXH+j47jsMc4x5HA/aOwuuHPqn6y9+ZTNYPWDRD8iLKvIVMZlzq2unjUEvyDAU+TTVPZOXkG2Ojeoz0P4AkZw==", + "requires": { + "@splidejs/splide": "^4.1.3" + } + }, + "@splidejs/splide": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-4.1.4.tgz", + "integrity": "sha512-5I30evTJcAJQXt6vJ26g2xEkG+l1nXcpEw4xpKh0/FWQ8ozmAeTbtniVtVmz2sH1Es3vgfC4SS8B2X4o5JMptA==" + }, "@storybook/addon-a11y": { "version": "8.1.6", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.1.6.tgz", diff --git a/package.json b/package.json index 1e7f991a0..5ebec0c9a 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ }, "dependencies": { "@floating-ui/react": "^0.25.1", + "@splidejs/react-splide": "^0.7.12", "@tanstack/react-table": "^8.7.8", "allotment": "^1.19.0", "axios": "^1.7.2", diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx index 4e7475c70..c50a7f64c 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentSelector.stories.tsx @@ -78,6 +78,18 @@ export const WithCarousel: Story = { mediumCriticality: 0, lowCriticality: 1 } + }, + { + environment: { + id: "5", + name: "Custom2", + type: "Private" + }, + issueCounts: { + highCriticality: 0, + mediumCriticality: 0, + lowCriticality: 2 + } } ] } diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx b/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx index 50e8d8be0..66ceabe4f 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx @@ -1,34 +1,16 @@ import useDimensions from "react-cool-dimensions"; import { useGlobalStore } from "../../../../containers/Main/stores/useGlobalStore"; -import { logger } from "../../../../logging"; import { changeScope } from "../../../../utils/actions/changeScope"; import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; -import { ChevronIcon } from "../../../common/icons/12px/ChevronIcon"; -import { Direction } from "../../../common/icons/types"; +import { Carousel } from "../../../common/Carousel"; import { trackingEvents } from "../../tracking"; import { EnvironmentChip } from "./EnvironmentChip"; import { getMostCriticalIssueCount } from "./getMostCriticalIssueCount"; import * as s from "./styles"; -import { - EnvironmentSelectorProps, - ScrollDirection, - SelectorEnvironment -} from "./types"; +import { EnvironmentSelectorProps, SelectorEnvironment } from "./types"; const ENVIRONMENT_CHIP_COUNT = 3; -const getSlidingWindow = (arr: T[], start: number, length: number) => { - const result = []; - const arrLength = arr.length; - - for (let i = 0; i < length; i++) { - const currentIndex = (start + i + arrLength) % arrLength; - result.push(arr[currentIndex]); - } - - return result; -}; - const sortEnvironmentsByCriticalIssues = ( a: SelectorEnvironment, b: SelectorEnvironment @@ -89,74 +71,43 @@ export const EnvironmentSelector = ({ changeEnvironment(environmentId); }; - const handleCarouselButtonClick = (direction: ScrollDirection) => { - logger.debug(direction); - // const { entry: containerEntry, width: containerWidth } = - // environmentListContainerDimensions; - // const { entry } = environmentListDimensions; - // if (containerEntry && entry) { - // let delta = containerWidth; - // if (direction === "left") { - // delta *= -1; - // } - // let newScrollLeft = containerEntry.target.scrollLeft + delta; - // const maxScrollLeft = containerEntry.target.scrollWidth - containerWidth; - // if (newScrollLeft >= maxScrollLeft) { - // newScrollLeft = maxScrollLeft; - // } - // if (newScrollLeft < 0) { - // newScrollLeft = 0; - // } - // setScrollLeft(newScrollLeft); - // containerEntry.target.scrollLeft = newScrollLeft; - // } - }; - - const environmentIndex = sortedEnvironments.findIndex( - (x) => x.environment.id === environment?.id - ); - - const environmentsWithChips = - sortedEnvironments.length > ENVIRONMENT_CHIP_COUNT - ? getSlidingWindow( - sortedEnvironments, - environmentIndex - 1, - ENVIRONMENT_CHIP_COUNT - ) - : sortedEnvironments; - const isCarouselVisible = sortedEnvironments.length > ENVIRONMENT_CHIP_COUNT; return ( - - {environmentsWithChips.map((x) => ( - - ))} - - {isCarouselVisible && ( - ( - - )} - onClick={() => handleCarouselButtonClick("left")} - /> - )} - {isCarouselVisible && ( - ( - - )} - onClick={() => handleCarouselButtonClick("right")} + {isCarouselVisible ? ( + ( + + ))} /> + ) : ( + + {sortedEnvironments.map((x) => ( + + ))} + )} ); diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts b/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts index edb31e94b..431eb72ae 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts @@ -1,6 +1,4 @@ -import styled, { css } from "styled-components"; -import { NewIconButton } from "../../../common/v3/NewIconButton"; -import { CarouselButtonProps } from "./types"; +import styled from "styled-components"; export const Container = styled.div` display: flex; @@ -10,6 +8,18 @@ export const Container = styled.div` position: relative; `; +// export const CarouselContainer = styled.div` +// width: 100%; + +// & ul { +// gap: 4px; +// } + +// & ul > li > * { +// width: 100%; +// } +// `; + export const EnvironmentsContainer = styled.div` display: flex; gap: 4px; @@ -21,26 +31,67 @@ export const EnvironmentsContainer = styled.div` } `; -export const CarouselButton = styled(NewIconButton)` - position: absolute; - ${({ direction }) => - direction === "left" - ? css` - padding-left: 22px; - left: 0; - background: linear-gradient( - 90deg, - #222326 35%, - rgba(34 35 38 / 0%) 100% - ); - ` - : css` - padding-right: 22px; - right: 0; - background: linear-gradient( - 90deg, - rgba(34 35 38 / 0%) 4.55%, - #222326 60.23% - ); - `} -`; +// export const CarouselButton = styled(NewIconButton)` +// position: absolute; +// border-radius: 0; +// border: none; +// height: 100%; +// width: 44px; + +// ${({ direction, theme }) => { +// const hexColor = theme.colors.v3.surface.primary; +// const rgbColor = hexToRgb(hexColor); + +// return direction === "left" +// ? css` +// padding-left: 22px; +// left: 0; +// background: ${rgbColor +// ? `linear-gradient( +// 90deg, +// ${hexColor} 35%, +// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 100% +// )` +// : "transparent"}; +// ` +// : css` +// padding-right: 22px; +// right: 0; +// background: ${rgbColor +// ? `linear-gradient( +// 90deg, +// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 4.55%, +// ${hexColor} 60.23% +// )` +// : "transparent"}; +// `; +// }} + +// &:hover:enabled { +// border: none; +// ${({ direction, theme }) => { +// const hexColor = theme.colors.v3.surface.primary; +// const rgbColor = hexToRgb(hexColor); + +// return direction === "left" +// ? css` +// background: ${rgbColor +// ? `linear-gradient( +// 90deg, +// ${hexColor} 35%, +// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 100% +// )` +// : "transparent"}; +// ` +// : css` +// background: ${rgbColor +// ? `linear-gradient( +// 90deg, +// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 4.55%, +// ${hexColor} 60.23% +// )` +// : "transparent"}; +// `; +// }} +// } +// `; diff --git a/src/components/common/Carousel/Carousel.stories.tsx b/src/components/common/Carousel/Carousel.stories.tsx new file mode 100644 index 000000000..a9512d089 --- /dev/null +++ b/src/components/common/Carousel/Carousel.stories.tsx @@ -0,0 +1,40 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Carousel } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "common/Carousel", + component: Carousel, + 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: { + items: [ +
1
, +
2
, +
3
, +
4
, +
5
, +
6
, +
7
, +
8
+ ], + itemsPerSlide: 3, + breakpoints: { + 461: { + perPage: 4 + }, + 561: { + perPage: 5 + } + } + } +}; diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx new file mode 100644 index 000000000..b93e39791 --- /dev/null +++ b/src/components/common/Carousel/index.tsx @@ -0,0 +1,60 @@ +import { + Options, + Splide, + SplideSlide, + SplideTrack +} from "@splidejs/react-splide"; +import "@splidejs/react-splide/css/core"; +import { v4 as uuidv4 } from "uuid"; +import { ChevronIcon } from "../icons/12px/ChevronIcon"; +import { Direction } from "../icons/types"; +import * as s from "./styles"; +import { CarouselProps } from "./types"; + +export const Carousel = ({ + items, + itemsPerSlide = 1, + gap, + breakpoints +}: CarouselProps) => { + const options: Options = { + perPage: itemsPerSlide, + gap, + type: "loop", + perMove: 1, + arrows: true, + pagination: false, + isNavigation: true, + mediaQuery: "min", + breakpoints, + focus: "center" + }; + + return ( + + + + {items.map((item) => ( + {item} + ))} + +
+ ( + + )} + direction={"left"} + /> + ( + + )} + direction={"right"} + /> +
+
+
+ ); +}; diff --git a/src/components/common/Carousel/styles.ts b/src/components/common/Carousel/styles.ts new file mode 100644 index 000000000..27e0c0fb9 --- /dev/null +++ b/src/components/common/Carousel/styles.ts @@ -0,0 +1,75 @@ +import styled, { css } from "styled-components"; +import { hexToRgb } from "../../../utils/hexToRgb"; +import { NewIconButton } from "../v3/NewIconButton"; +import { CarouselButtonProps } from "./types"; + +export const Container = styled.div` + position: relative; +`; + +export const CarouselButton = styled(NewIconButton)` + top: 0; + position: absolute; + border-radius: 0; + border: none; + height: 100%; + min-height: 28px; + width: 44px; + + ${({ direction, theme }) => { + const hexColor = theme.colors.v3.surface.primary; + const rgbColor = hexToRgb(hexColor); + + return direction === "left" + ? css` + padding-left: 22px; + left: 0; + background: ${rgbColor + ? `linear-gradient( + 90deg, + ${hexColor} 35%, + rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 100% + )` + : "transparent"}; + ` + : css` + padding-right: 22px; + right: 0; + background: ${rgbColor + ? `linear-gradient( + 90deg, + rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 4.55%, + ${hexColor} 60.23% + )` + : "transparent"}; + `; + }} + + &:hover:enabled { + border: none; + ${({ direction, theme }) => { + const hexColor = theme.colors.v3.surface.primary; + const rgbColor = hexToRgb(hexColor); + + return direction === "left" + ? css` + background: ${rgbColor + ? `linear-gradient( + 90deg, + ${hexColor} 35%, + rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 100% + )` + : "transparent"}; + ` + : css` + background: ${rgbColor + ? `linear-gradient( + 90deg, + rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 4.55%, + ${hexColor} 60.23% + )` + : "transparent"}; + `; + }} + } +`; diff --git a/src/components/common/Carousel/types.ts b/src/components/common/Carousel/types.ts new file mode 100644 index 000000000..db3d35c6d --- /dev/null +++ b/src/components/common/Carousel/types.ts @@ -0,0 +1,13 @@ +import { Options } from "@splidejs/react-splide"; +import { ReactNode } from "react"; + +export interface CarouselProps { + items: ReactNode[]; + itemsPerSlide?: number; + breakpoints: Options["breakpoints"]; + gap?: number | string; +} + +export interface CarouselButtonProps { + direction: "left" | "right"; +} 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 e1fbcb266b2b979b79541cfe261c4cd29b7fda13 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 12 Aug 2024 08:23:25 +0200 Subject: [PATCH 3/5] Integrate Carousel into Insights --- .../EnvironmentChip/index.tsx | 9 +- .../EnvironmentChip/types.ts | 1 + .../EnvironmentSelector/index.tsx | 42 +++++++-- .../EnvironmentSelector/styles.ts | 88 +------------------ .../EnvironmentSelector/types.ts | 1 + .../Insights/InsightsCatalog/index.tsx | 3 +- .../Insights/InsightsCatalog/styles.ts | 9 ++ src/components/common/Carousel/index.tsx | 31 ++++++- src/components/common/Carousel/styles.ts | 4 +- src/components/common/Carousel/types.ts | 2 + 10 files changed, 90 insertions(+), 100 deletions(-) diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/index.tsx b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/index.tsx index 1f4fb4da1..88bc83b26 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/index.tsx +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/index.tsx @@ -8,7 +8,8 @@ export const EnvironmentChip = ({ environment, issueCounts, onClick, - isActive + isActive, + className }: EnvironmentChipProps) => { const handleClick = () => { if (!isActive) { @@ -21,7 +22,11 @@ export const EnvironmentChip = ({ return ( - + diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/types.ts b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/types.ts index 9b84265e8..287a93a41 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/types.ts +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/EnvironmentChip/types.ts @@ -14,6 +14,7 @@ export interface EnvironmentChipProps { onClick: (environment: string) => void; isActive: boolean; issueCounts?: EnvironmentIssueCounts; + className?: string; } export interface StyledChipProps { diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx b/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx index 66ceabe4f..0926a097b 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/index.tsx @@ -1,5 +1,7 @@ -import useDimensions from "react-cool-dimensions"; +import { useEffect, useMemo, useState } from "react"; import { useGlobalStore } from "../../../../containers/Main/stores/useGlobalStore"; +import { usePrevious } from "../../../../hooks/usePrevious"; +import { isNumber } from "../../../../typeGuards/isNumber"; import { changeScope } from "../../../../utils/actions/changeScope"; import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; import { Carousel } from "../../../common/Carousel"; @@ -41,19 +43,44 @@ const sortEnvironmentsByCriticalIssues = ( }; export const EnvironmentSelector = ({ - environments + environments, + className }: EnvironmentSelectorProps) => { const scope = useGlobalStore.use.scope(); const environment = useGlobalStore.use.environment(); - const { observe } = useDimensions(); const sortedEnvironments = environments.sort( sortEnvironmentsByCriticalIssues ); + const environmentIndex = useMemo( + () => + Math.max( + sortedEnvironments.findIndex( + (x) => x.environment.id === environment?.id + ), + 0 + ), + [sortedEnvironments, environment] + ); + const previousEnvironmentIndex = usePrevious(environmentIndex); + const [carouselIndex, setCarouselIndex] = useState(environmentIndex); + + useEffect(() => { + if ( + isNumber(previousEnvironmentIndex) && + environmentIndex !== previousEnvironmentIndex + ) { + setCarouselIndex(environmentIndex); + } + }, [environmentIndex, previousEnvironmentIndex]); if (sortedEnvironments.length < 2) { return null; } + const handleCarouselMove = (index: number) => { + setCarouselIndex(index); + }; + const changeEnvironment = (environmentId: string) => { sendUserActionTrackingEvent(trackingEvents.ENVIRONMENT_SELECTED); @@ -74,10 +101,11 @@ export const EnvironmentSelector = ({ const isCarouselVisible = sortedEnvironments.length > ENVIRONMENT_CHIP_COUNT; return ( - +
{isCarouselVisible ? ( ( - ))} + currentIndex={carouselIndex} + onMove={handleCarouselMove} /> ) : ( @@ -109,6 +139,6 @@ export const EnvironmentSelector = ({ ))} )} - +
); }; diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts b/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts index 431eb72ae..c58a03092 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/styles.ts @@ -1,97 +1,15 @@ import styled from "styled-components"; +import { EnvironmentChip } from "./EnvironmentChip"; -export const Container = styled.div` - display: flex; - gap: 4px; - overflow: hidden; - flex-grow: 1; - position: relative; +export const CarouselEnvironmentChip = styled(EnvironmentChip)` + width: 100%; `; -// export const CarouselContainer = styled.div` -// width: 100%; - -// & ul { -// gap: 4px; -// } - -// & ul > li > * { -// width: 100%; -// } -// `; - export const EnvironmentsContainer = styled.div` display: flex; gap: 4px; - overflow: hidden; - flex-grow: 1; & > * { flex: 1 1 0; } `; - -// export const CarouselButton = styled(NewIconButton)` -// position: absolute; -// border-radius: 0; -// border: none; -// height: 100%; -// width: 44px; - -// ${({ direction, theme }) => { -// const hexColor = theme.colors.v3.surface.primary; -// const rgbColor = hexToRgb(hexColor); - -// return direction === "left" -// ? css` -// padding-left: 22px; -// left: 0; -// background: ${rgbColor -// ? `linear-gradient( -// 90deg, -// ${hexColor} 35%, -// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 100% -// )` -// : "transparent"}; -// ` -// : css` -// padding-right: 22px; -// right: 0; -// background: ${rgbColor -// ? `linear-gradient( -// 90deg, -// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 4.55%, -// ${hexColor} 60.23% -// )` -// : "transparent"}; -// `; -// }} - -// &:hover:enabled { -// border: none; -// ${({ direction, theme }) => { -// const hexColor = theme.colors.v3.surface.primary; -// const rgbColor = hexToRgb(hexColor); - -// return direction === "left" -// ? css` -// background: ${rgbColor -// ? `linear-gradient( -// 90deg, -// ${hexColor} 35%, -// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 100% -// )` -// : "transparent"}; -// ` -// : css` -// background: ${rgbColor -// ? `linear-gradient( -// 90deg, -// rgba(${rgbColor.r} ${rgbColor.g} ${rgbColor.b} / 0%) 4.55%, -// ${hexColor} 60.23% -// )` -// : "transparent"}; -// `; -// }} -// } -// `; diff --git a/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts b/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts index c87992118..23974d6b6 100644 --- a/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts +++ b/src/components/Insights/InsightsCatalog/EnvironmentSelector/types.ts @@ -7,6 +7,7 @@ export interface SelectorEnvironment { export interface EnvironmentSelectorProps { environments: SelectorEnvironment[]; + className?: string; } export type ScrollDirection = "left" | "right"; diff --git a/src/components/Insights/InsightsCatalog/index.tsx b/src/components/Insights/InsightsCatalog/index.tsx index af25f3f32..75c72d6de 100644 --- a/src/components/Insights/InsightsCatalog/index.tsx +++ b/src/components/Insights/InsightsCatalog/index.tsx @@ -30,7 +30,6 @@ import { NewIconButton } from "../../common/v3/NewIconButton"; import { Tooltip } from "../../common/v3/Tooltip"; import { IssuesFilter } from "../Issues/IssuesFilter"; import { trackingEvents } from "../tracking"; -import { EnvironmentSelector } from "./EnvironmentSelector"; import { SelectorEnvironment } from "./EnvironmentSelector/types"; import { FilterButton } from "./FilterButton"; import { FilterPanel } from "./FilterPanel"; @@ -262,7 +261,7 @@ export const InsightsCatalog = ({ {isAtSpan && selectorEnvironments.length > 1 && ( - + )} {!isAtSpan && renderFilterPanel()} diff --git a/src/components/Insights/InsightsCatalog/styles.ts b/src/components/Insights/InsightsCatalog/styles.ts index ccdf32114..db3a2b9b1 100644 --- a/src/components/Insights/InsightsCatalog/styles.ts +++ b/src/components/Insights/InsightsCatalog/styles.ts @@ -5,6 +5,10 @@ import { subscriptRegularTypography } from "../../common/App/typographies"; import { Link } from "../../common/v3/Link"; +import { EnvironmentSelector } from "./EnvironmentSelector"; + +const TOOLBAR_BUTTONS_CONTAINER_WIDTH = 60; // in pixels +const TOOLBAR_GAP = 4; // in pixels export const Footer = styled.div` display: flex; @@ -109,7 +113,12 @@ export const InsightsViewModeToolbar = styled(Toolbar)` padding: 0 8px; `; +export const StyledEnvironmentSelector = styled(EnvironmentSelector)` + width: calc(100% - ${TOOLBAR_BUTTONS_CONTAINER_WIDTH + TOOLBAR_GAP}px); +`; + export const ToolbarButtonsContainer = styled.div` + width: ${TOOLBAR_BUTTONS_CONTAINER_WIDTH}px; display: flex; gap: 4px; align-items: center; diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx index b93e39791..b65114598 100644 --- a/src/components/common/Carousel/index.tsx +++ b/src/components/common/Carousel/index.tsx @@ -5,6 +5,8 @@ import { SplideTrack } from "@splidejs/react-splide"; import "@splidejs/react-splide/css/core"; +import { Splide as SplideInstance } from "@splidejs/splide"; +import { useEffect, useRef } from "react"; import { v4 as uuidv4 } from "uuid"; import { ChevronIcon } from "../icons/12px/ChevronIcon"; import { Direction } from "../icons/types"; @@ -15,8 +17,11 @@ export const Carousel = ({ items, itemsPerSlide = 1, gap, - breakpoints + breakpoints, + currentIndex = 0, + onMove }: CarouselProps) => { + const splideRef = useRef(null); const options: Options = { perPage: itemsPerSlide, gap, @@ -27,12 +32,32 @@ export const Carousel = ({ isNavigation: true, mediaQuery: "min", breakpoints, - focus: "center" + focus: "center", + width: "100%", + start: currentIndex + }; + + useEffect(() => { + const splideInstance = splideRef.current?.splide; + if (!splideInstance) { + return; + } + + splideInstance.go(currentIndex); + }, [currentIndex]); + + const handleMoved = (_: SplideInstance, newIndex: number) => { + onMove(newIndex); }; return ( - + {items.map((item) => ( {item} diff --git a/src/components/common/Carousel/styles.ts b/src/components/common/Carousel/styles.ts index 27e0c0fb9..44eefee87 100644 --- a/src/components/common/Carousel/styles.ts +++ b/src/components/common/Carousel/styles.ts @@ -22,7 +22,7 @@ export const CarouselButton = styled(NewIconButton)` return direction === "left" ? css` - padding-left: 22px; + padding-right: 22px; left: 0; background: ${rgbColor ? `linear-gradient( @@ -33,7 +33,7 @@ export const CarouselButton = styled(NewIconButton)` : "transparent"}; ` : css` - padding-right: 22px; + padding-left: 22px; right: 0; background: ${rgbColor ? `linear-gradient( diff --git a/src/components/common/Carousel/types.ts b/src/components/common/Carousel/types.ts index db3d35c6d..7e3138e36 100644 --- a/src/components/common/Carousel/types.ts +++ b/src/components/common/Carousel/types.ts @@ -6,6 +6,8 @@ export interface CarouselProps { itemsPerSlide?: number; breakpoints: Options["breakpoints"]; gap?: number | string; + currentIndex?: number; + onMove: (index: number) => void; } export interface CarouselButtonProps { From b930440b938e2bf70cd6db49f5e5a5c5c4eda8c6 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 12 Aug 2024 09:38:01 +0200 Subject: [PATCH 4/5] Add loading screen for changing the scope --- .../LoadingMessage/LoadingMessage.stories.tsx | 18 +++++++ .../InsightsCatalog/LoadingMessage/index.tsx | 14 +++++ .../InsightsCatalog/LoadingMessage/styles.ts | 42 +++++++++++++++ .../Insights/InsightsCatalog/index.tsx | 30 +++++++---- src/components/common/App/index.tsx | 54 ++++++++----------- src/containers/Main/stores/useGlobalStore.ts | 4 ++ .../Main/stores/useInsightsStore.ts | 2 +- src/utils/actions/changeScope.ts | 4 ++ 8 files changed, 126 insertions(+), 42 deletions(-) create mode 100644 src/components/Insights/InsightsCatalog/LoadingMessage/LoadingMessage.stories.tsx create mode 100644 src/components/Insights/InsightsCatalog/LoadingMessage/index.tsx create mode 100644 src/components/Insights/InsightsCatalog/LoadingMessage/styles.ts diff --git a/src/components/Insights/InsightsCatalog/LoadingMessage/LoadingMessage.stories.tsx b/src/components/Insights/InsightsCatalog/LoadingMessage/LoadingMessage.stories.tsx new file mode 100644 index 000000000..439dcafe8 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/LoadingMessage/LoadingMessage.stories.tsx @@ -0,0 +1,18 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { LoadingMessage } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Insights/InsightsCatalog/LoadingMessage", + component: LoadingMessage, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/Insights/InsightsCatalog/LoadingMessage/index.tsx b/src/components/Insights/InsightsCatalog/LoadingMessage/index.tsx new file mode 100644 index 000000000..2aae6e587 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/LoadingMessage/index.tsx @@ -0,0 +1,14 @@ +import { PetalsIcon } from "../../../common/icons/16px/PetalsIcon"; +import * as s from "./styles"; + +export const LoadingMessage = () => ( + + + + + + Fetching results + Updating the results list may take a few moments. + + +); diff --git a/src/components/Insights/InsightsCatalog/LoadingMessage/styles.ts b/src/components/Insights/InsightsCatalog/LoadingMessage/styles.ts new file mode 100644 index 000000000..aecb45939 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/LoadingMessage/styles.ts @@ -0,0 +1,42 @@ +import styled from "styled-components"; +import { + bodySemiboldTypography, + footnoteRegularTypography +} from "../../../common/App/typographies"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + height: 100%; +`; + +export const IconContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + border-radius: 50%; + background: ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; + color: ${({ theme }) => theme.colors.v3.surface.gray}; +`; + +export const Title = styled.span` + ${bodySemiboldTypography} + + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const TextContainer = styled.div` + ${footnoteRegularTypography} + + display: flex; + flex-direction: column; + gap: 4px; + text-align: center; + color: ${({ theme }) => theme.colors.v3.text.tertiary}; + max-width: 210px; +`; diff --git a/src/components/Insights/InsightsCatalog/index.tsx b/src/components/Insights/InsightsCatalog/index.tsx index 75c72d6de..fa094a2b3 100644 --- a/src/components/Insights/InsightsCatalog/index.tsx +++ b/src/components/Insights/InsightsCatalog/index.tsx @@ -34,6 +34,7 @@ import { SelectorEnvironment } from "./EnvironmentSelector/types"; import { FilterButton } from "./FilterButton"; import { FilterPanel } from "./FilterPanel"; import { InsightsPage } from "./InsightsPage"; +import { LoadingMessage } from "./LoadingMessage"; import { PromotionCard } from "./PromotionCard"; import * as s from "./styles"; import { @@ -111,6 +112,10 @@ export const InsightsCatalog = ({ PROMOTION_COMPLETED_PERSISTENCE_KEY, "application" ); + const areInsightsLoading = useInsightsStore.use.isDataLoading(); + const isScopeLoading = useGlobalStore.use.isScopeLoading(); + const isInitialLoading = !data && areInsightsLoading; + const isLoading = isInitialLoading || isScopeLoading; const appliedFilterCount = filters.length + (filteredInsightTypes.length > 0 ? 1 : 0); @@ -363,16 +368,21 @@ export const InsightsCatalog = ({ )} - + {isLoading ? ( + + ) : ( + + )} {totalCount > 0 && ( <> diff --git a/src/components/common/App/index.tsx b/src/components/common/App/index.tsx index dbf09f4de..7a4701076 100644 --- a/src/components/common/App/index.tsx +++ b/src/components/common/App/index.tsx @@ -62,37 +62,28 @@ export const App = ({ theme, children, id }: AppProps) => { const [mainFont, setMainFont] = useState(defaultMainFont); const [codeFont, setCodeFont] = useState(defaultCodeFont); const [config, setConfig] = useState(useContext(ConfigContext)); - const setJaegerURL = useGlobalStore((state) => state.setJaegerURL); - const setIsJaegerEnabled = useGlobalStore( - (state) => state.setIsJaegerEnabled - ); - const setIsDigmaEngineInstalled = useGlobalStore( - (state) => state.setIsDigmaEngineInstalled - ); - const setIsDigmaEngineRunning = useGlobalStore( - (state) => state.setIsDigmaEngineRunning - ); - const setDigmaStatus = useGlobalStore((state) => state.setDigmaStatus); - const setIsDockerInstalled = useGlobalStore( - (state) => state.setIsDockerInstalled - ); - const setIsDockerComposeInstalled = useGlobalStore( - (state) => state.setIsDockerComposeInstalled - ); - const setDigmaApiUrl = useGlobalStore((state) => state.setDigmaApiUrl); - const setUserRegistrationEmail = useGlobalStore( - (state) => state.setUserRegistrationEmail - ); - const setIsObservabilityEnabled = useGlobalStore( - (state) => state.setIsObservabilityEnabled - ); - const setBackendInfo = useGlobalStore((state) => state.setBackendInfo); - const setEnvironments = useGlobalStore((state) => state.setEnvironments); - const setEnvironment = useGlobalStore((state) => state.setEnvironment); - const setScope = useGlobalStore((state) => state.setScope); - const setUserInfo = useGlobalStore((state) => state.setUserInfo); - const setInsightStats = useGlobalStore((state) => state.setInsightStats); - const setRunConfiguration = useGlobalStore((state) => state.setRunConfig); + const setJaegerURL = useGlobalStore.use.setJaegerURL(); + const setIsJaegerEnabled = useGlobalStore.use.setIsJaegerEnabled(); + const setIsDigmaEngineInstalled = + useGlobalStore.use.setIsDigmaEngineInstalled(); + const setIsDigmaEngineRunning = useGlobalStore.use.setIsDigmaEngineRunning(); + const setDigmaStatus = useGlobalStore.use.setDigmaStatus(); + const setIsDockerInstalled = useGlobalStore.use.setIsDockerInstalled(); + const setIsDockerComposeInstalled = + useGlobalStore.use.setIsDockerComposeInstalled(); + const setDigmaApiUrl = useGlobalStore.use.setDigmaApiUrl(); + const setUserRegistrationEmail = + useGlobalStore.use.setUserRegistrationEmail(); + const setIsObservabilityEnabled = + useGlobalStore.use.setIsObservabilityEnabled(); + const setBackendInfo = useGlobalStore.use.setBackendInfo(); + const setEnvironments = useGlobalStore.use.setEnvironments(); + const setEnvironment = useGlobalStore.use.setEnvironment(); + const setScope = useGlobalStore.use.setScope(); + const setIsScopeLoading = useGlobalStore.use.setIsScopeLoading(); + const setUserInfo = useGlobalStore.use.setUserInfo(); + const setInsightStats = useGlobalStore.use.setInsightStats(); + const setRunConfiguration = useGlobalStore.use.setRunConfig(); const setIsMicrometerProject = useGlobalStore( (state) => state.setIsMicrometerProject ); @@ -280,6 +271,7 @@ export const App = ({ theme, children, id }: AppProps) => { : config.environment; setScope(scope); + setIsScopeLoading(false); setEnvironment(environment ?? null); return { diff --git a/src/containers/Main/stores/useGlobalStore.ts b/src/containers/Main/stores/useGlobalStore.ts index a1d390031..396c59a66 100644 --- a/src/containers/Main/stores/useGlobalStore.ts +++ b/src/containers/Main/stores/useGlobalStore.ts @@ -34,6 +34,7 @@ export interface GlobalState { environment: Environment | null; environments: Environment[] | null; scope: Scope | null; + isScopeLoading: boolean; insightStats: InsightStats | null; userId: string | null; userInfo: UserInfo | null; @@ -82,6 +83,7 @@ export const initialState: GlobalState = { environment: isEnvironment(window.environment) ? window.environment : null, environments: null, scope: null, + isScopeLoading: false, insightStats: null, userId: isString(window.userId) ? window.userId : null, userInfo: null, @@ -112,6 +114,7 @@ export interface GlobalActions { setEnvironment: (environment: Environment | null) => void; setEnvironments: (environments: Environment[]) => void; setScope: (scope: Scope) => void; + setIsScopeLoading: (isLoading: boolean) => void; setInsightStats: (stats: InsightStats) => void; setUserId: (userId: string) => void; setUserInfo: (userInfo: UserInfo) => void; @@ -151,6 +154,7 @@ export const useGlobalStore = createSelectors( setEnvironment: (environment) => set({ environment }), setEnvironments: (environments) => set({ environments }), setScope: (scope) => set({ scope }), + setIsScopeLoading: (isLoading) => set({ isScopeLoading: isLoading }), setInsightStats: (stats) => set({ insightStats: stats }), setUserId: (userId) => set({ userId }), setUserInfo: (userInfo) => set({ userInfo }), diff --git a/src/containers/Main/stores/useInsightsStore.ts b/src/containers/Main/stores/useInsightsStore.ts index 63cb210e5..1768550fe 100644 --- a/src/containers/Main/stores/useInsightsStore.ts +++ b/src/containers/Main/stores/useInsightsStore.ts @@ -47,7 +47,7 @@ export const initialState: InsightsState = { }; export interface InsightsActions { - setData: (data: InsightsData) => void; + setData: (data: InsightsData | null) => void; setIsDataLoading: (isDataLoading: boolean) => void; setSearch: (search: string) => void; setPage: (page: number) => void; diff --git a/src/utils/actions/changeScope.ts b/src/utils/actions/changeScope.ts index 81e2bfa09..ea082bd41 100644 --- a/src/utils/actions/changeScope.ts +++ b/src/utils/actions/changeScope.ts @@ -1,4 +1,6 @@ import { actions } from "../../actions"; +import { useGlobalStore } from "../../containers/Main/stores/useGlobalStore"; +import { useInsightsStore } from "../../containers/Main/stores/useInsightsStore"; interface ChangeScopePayload { span: { @@ -16,4 +18,6 @@ export const changeScope = (payload: ChangeScopePayload) => { action: actions.CHANGE_SCOPE, payload }); + useGlobalStore.getState().setIsScopeLoading(true); + useInsightsStore.getState().setData(null); }; From b82e71cb37d6dfe17fff1834a59e2a697943b007 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 12 Aug 2024 10:02:11 +0200 Subject: [PATCH 5/5] Improve loading screen --- .../Insights/InsightsCatalog/index.tsx | 2 +- .../LoadingMessage/LoadingMessage.stories.tsx | 0 .../LoadingMessage/index.tsx | 2 +- .../LoadingMessage/styles.ts | 2 +- src/components/Insights/index.tsx | 18 ++++++------------ 5 files changed, 9 insertions(+), 15 deletions(-) rename src/components/Insights/{InsightsCatalog => }/LoadingMessage/LoadingMessage.stories.tsx (100%) rename src/components/Insights/{InsightsCatalog => }/LoadingMessage/index.tsx (84%) rename src/components/Insights/{InsightsCatalog => }/LoadingMessage/styles.ts (95%) diff --git a/src/components/Insights/InsightsCatalog/index.tsx b/src/components/Insights/InsightsCatalog/index.tsx index fa094a2b3..e405dd937 100644 --- a/src/components/Insights/InsightsCatalog/index.tsx +++ b/src/components/Insights/InsightsCatalog/index.tsx @@ -29,12 +29,12 @@ import { Button } from "../../common/v3/Button"; import { NewIconButton } from "../../common/v3/NewIconButton"; import { Tooltip } from "../../common/v3/Tooltip"; import { IssuesFilter } from "../Issues/IssuesFilter"; +import { LoadingMessage } from "../LoadingMessage"; import { trackingEvents } from "../tracking"; import { SelectorEnvironment } from "./EnvironmentSelector/types"; import { FilterButton } from "./FilterButton"; import { FilterPanel } from "./FilterPanel"; import { InsightsPage } from "./InsightsPage"; -import { LoadingMessage } from "./LoadingMessage"; import { PromotionCard } from "./PromotionCard"; import * as s from "./styles"; import { diff --git a/src/components/Insights/InsightsCatalog/LoadingMessage/LoadingMessage.stories.tsx b/src/components/Insights/LoadingMessage/LoadingMessage.stories.tsx similarity index 100% rename from src/components/Insights/InsightsCatalog/LoadingMessage/LoadingMessage.stories.tsx rename to src/components/Insights/LoadingMessage/LoadingMessage.stories.tsx diff --git a/src/components/Insights/InsightsCatalog/LoadingMessage/index.tsx b/src/components/Insights/LoadingMessage/index.tsx similarity index 84% rename from src/components/Insights/InsightsCatalog/LoadingMessage/index.tsx rename to src/components/Insights/LoadingMessage/index.tsx index 2aae6e587..133e1b90d 100644 --- a/src/components/Insights/InsightsCatalog/LoadingMessage/index.tsx +++ b/src/components/Insights/LoadingMessage/index.tsx @@ -1,4 +1,4 @@ -import { PetalsIcon } from "../../../common/icons/16px/PetalsIcon"; +import { PetalsIcon } from "../../common/icons/16px/PetalsIcon"; import * as s from "./styles"; export const LoadingMessage = () => ( diff --git a/src/components/Insights/InsightsCatalog/LoadingMessage/styles.ts b/src/components/Insights/LoadingMessage/styles.ts similarity index 95% rename from src/components/Insights/InsightsCatalog/LoadingMessage/styles.ts rename to src/components/Insights/LoadingMessage/styles.ts index aecb45939..9d88d06bd 100644 --- a/src/components/Insights/InsightsCatalog/LoadingMessage/styles.ts +++ b/src/components/Insights/LoadingMessage/styles.ts @@ -2,7 +2,7 @@ import styled from "styled-components"; import { bodySemiboldTypography, footnoteRegularTypography -} from "../../../common/App/typographies"; +} from "../../common/App/typographies"; export const Container = styled.div` display: flex; diff --git a/src/components/Insights/index.tsx b/src/components/Insights/index.tsx index ba73a2475..dfac37e61 100644 --- a/src/components/Insights/index.tsx +++ b/src/components/Insights/index.tsx @@ -221,7 +221,7 @@ export const Insights = ({ insightViewType }: InsightsProps) => { "project" ); const previousPersistedFilters = usePrevious(persistedFilters); - const { data, isLoading, refresh } = useInsightsData({ + const { data, refresh } = useInsightsData({ areFiltersRehydrated }); const reset = useInsightsStore.use.reset(); @@ -374,16 +374,10 @@ export const Insights = ({ insightViewType }: InsightsProps) => { } }; - const renderContent = ( - data: InsightsData | null, - isLoading: boolean - ): JSX.Element => { - const isInitialLoading = - (!data && isLoading) || - !backendInfo || - !storedInsightViewType || - !areFiltersRehydrated; - if (isInitialLoading) { + const renderContent = (data: InsightsData | null): JSX.Element => { + const isInitializationInProgress = + !backendInfo || !storedInsightViewType || !areFiltersRehydrated; + if (isInitializationInProgress) { return } />; } @@ -459,7 +453,7 @@ export const Insights = ({ insightViewType }: InsightsProps) => { return ( - {renderContent(data, isLoading)} + {renderContent(data)} {infoToOpenJiraTicket && (