From edc7979d95688e2ee36d3afaf3f24c874b5de5c8 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 28 Nov 2023 12:45:34 +0100 Subject: [PATCH 1/3] Add filter by services to Assets page --- .../Assets/AssetList/AssetList.stories.tsx | 1 + src/components/Assets/AssetList/index.tsx | 20 +- src/components/Assets/AssetList/types.ts | 1 + ....stories.tsx => AssetTypeList.stories.tsx} | 1 + src/components/Assets/AssetTypeList/index.tsx | 52 +++-- src/components/Assets/AssetTypeList/styles.ts | 2 +- src/components/Assets/AssetTypeList/types.ts | 1 + src/components/Assets/Assets.stories.tsx | 34 +++ .../Assets/FilterMenu/FilterMenu.stories.tsx | 94 +++++++++ src/components/Assets/FilterMenu/index.tsx | 80 +++++++ src/components/Assets/FilterMenu/styles.ts | 195 ++++++++++++++++++ src/components/Assets/FilterMenu/types.ts | 12 ++ src/components/Assets/actions.ts | 4 +- src/components/Assets/index.tsx | 132 +++++++++++- src/components/Assets/styles.ts | 86 ++++++++ src/components/Assets/types.ts | 7 + src/components/InstallationWizard/index.tsx | 1 - .../common/Checkbox/Checkbox.stories.tsx | 16 +- src/components/common/Checkbox/index.tsx | 30 ++- src/components/common/Checkbox/styles.ts | 149 ++++++++++++- src/components/common/Checkbox/types.ts | 5 +- src/components/common/icons/CheckmarkIcon.tsx | 30 +++ src/components/common/icons/FilterIcon.tsx | 26 +++ 23 files changed, 931 insertions(+), 48 deletions(-) rename src/components/Assets/AssetTypeList/{AssetList.stories.tsx => AssetTypeList.stories.tsx} (98%) create mode 100644 src/components/Assets/Assets.stories.tsx create mode 100644 src/components/Assets/FilterMenu/FilterMenu.stories.tsx create mode 100644 src/components/Assets/FilterMenu/index.tsx create mode 100644 src/components/Assets/FilterMenu/styles.ts create mode 100644 src/components/Assets/FilterMenu/types.ts create mode 100644 src/components/Assets/types.ts create mode 100644 src/components/common/icons/CheckmarkIcon.tsx create mode 100644 src/components/common/icons/FilterIcon.tsx diff --git a/src/components/Assets/AssetList/AssetList.stories.tsx b/src/components/Assets/AssetList/AssetList.stories.tsx index e3ae503ff..ffef353a3 100644 --- a/src/components/Assets/AssetList/AssetList.stories.tsx +++ b/src/components/Assets/AssetList/AssetList.stories.tsx @@ -20,6 +20,7 @@ type Story = StoryObj; export const Default: Story = { args: { assetTypeId: "Endpoint", + services: [], data: { data: [ { diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index 9c0e8b322..af85a16fc 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -159,6 +159,7 @@ export const AssetList = (props: AssetListProps) => { const refreshTimerId = useRef(); const previousEnvironment = usePrevious(config.environment); const previousAssetTypeId = usePrevious(props.assetTypeId); + const previousServices = usePrevious(props.services); const entries = data?.data || []; @@ -176,7 +177,8 @@ export const AssetList = (props: AssetListProps) => { sortOrder: sorting.order, ...(debouncedSearchInputValue.length > 0 ? { displayName: debouncedSearchInputValue } - : {}) + : {}), + services: props.services } } }); @@ -204,7 +206,8 @@ export const AssetList = (props: AssetListProps) => { (isString(previousDebouncedSearchInputValue) && previousDebouncedSearchInputValue !== debouncedSearchInputValue) || (isString(previousAssetTypeId) && - previousAssetTypeId !== props.assetTypeId) + previousAssetTypeId !== props.assetTypeId) || + previousServices !== props.services ) { window.sendMessageToDigma({ action: actions.GET_DATA, @@ -217,7 +220,8 @@ export const AssetList = (props: AssetListProps) => { sortOrder: sorting.order, ...(debouncedSearchInputValue.length > 0 ? { displayName: debouncedSearchInputValue } - : {}) + : {}), + services: props.services } } }); @@ -232,7 +236,9 @@ export const AssetList = (props: AssetListProps) => { previousPage, page, previousEnvironment, - config.environment + config.environment, + props.services, + previousServices ]); useEffect(() => { @@ -250,7 +256,8 @@ export const AssetList = (props: AssetListProps) => { sortOrder: sorting.order, ...(debouncedSearchInputValue.length > 0 ? { displayName: debouncedSearchInputValue } - : {}) + : {}), + services: props.services } } }); @@ -263,7 +270,8 @@ export const AssetList = (props: AssetListProps) => { page, sorting, debouncedSearchInputValue, - config.environment + config.environment, + props.services ]); useEffect(() => { diff --git a/src/components/Assets/AssetList/types.ts b/src/components/Assets/AssetList/types.ts index 62442df34..0d1ca8dc9 100644 --- a/src/components/Assets/AssetList/types.ts +++ b/src/components/Assets/AssetList/types.ts @@ -4,6 +4,7 @@ export interface AssetListProps { data?: AssetsData; onBackButtonClick: () => void; assetTypeId: string; + services: string[]; } export enum SORTING_CRITERION { diff --git a/src/components/Assets/AssetTypeList/AssetList.stories.tsx b/src/components/Assets/AssetTypeList/AssetTypeList.stories.tsx similarity index 98% rename from src/components/Assets/AssetTypeList/AssetList.stories.tsx rename to src/components/Assets/AssetTypeList/AssetTypeList.stories.tsx index 0953f9d2b..e22d7fa0c 100644 --- a/src/components/Assets/AssetTypeList/AssetList.stories.tsx +++ b/src/components/Assets/AssetTypeList/AssetTypeList.stories.tsx @@ -19,6 +19,7 @@ type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args export const Default: Story = { args: { + services: [], data: { assetCategories: [ { diff --git a/src/components/Assets/AssetTypeList/index.tsx b/src/components/Assets/AssetTypeList/index.tsx index 299d07bc3..b818a1d0a 100644 --- a/src/components/Assets/AssetTypeList/index.tsx +++ b/src/components/Assets/AssetTypeList/index.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { actions as globalActions } from "../../../actions"; import { dispatcher } from "../../../dispatcher"; import { usePrevious } from "../../../hooks/usePrevious"; @@ -33,13 +33,19 @@ export const AssetTypeList = (props: AssetTypeListProps) => { const [data, setData] = useState(); const previousData = usePrevious(data); const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); const [isInitialLoading, setIsInitialLoading] = useState(false); const config = useContext(ConfigContext); const previousEnvironment = usePrevious(config.environment); + const refreshTimerId = useRef(); + const previousServices = usePrevious(props.services); useEffect(() => { window.sendMessageToDigma({ - action: actions.GET_CATEGORIES_DATA + action: actions.GET_CATEGORIES_DATA, + payload: { + services: props.services + } }); setIsInitialLoading(true); @@ -58,31 +64,47 @@ export const AssetTypeList = (props: AssetTypeListProps) => { actions.SET_CATEGORIES_DATA, handleCategoriesData ); + window.clearTimeout(refreshTimerId.current); }; }, []); useEffect(() => { if ( - isString(previousEnvironment) && - previousEnvironment !== config.environment + (isString(previousEnvironment) && + previousEnvironment !== config.environment) || + previousServices !== props.services ) { window.sendMessageToDigma({ - action: actions.GET_CATEGORIES_DATA + action: actions.GET_CATEGORIES_DATA, + payload: { + services: props.services + } }); } - }, [previousEnvironment, config.environment]); + }, [ + previousEnvironment, + config.environment, + previousServices, + props.services + ]); useEffect(() => { - const timerId = window.setTimeout(() => { - window.sendMessageToDigma({ - action: actions.GET_CATEGORIES_DATA - }); - }, REFRESH_INTERVAL); + if (previousLastSetDataTimeStamp !== lastSetDataTimeStamp) { + window.clearTimeout(refreshTimerId.current); + refreshTimerId.current = window.setTimeout(() => { + window.sendMessageToDigma({ + action: actions.GET_CATEGORIES_DATA, + payload: { + services: props.services + } + }); + }, REFRESH_INTERVAL); - return () => { - window.clearTimeout(timerId); - }; - }, [lastSetDataTimeStamp]); + return () => { + window.clearTimeout(refreshTimerId.current); + }; + } + }, [props.services, previousLastSetDataTimeStamp, lastSetDataTimeStamp]); useEffect(() => { if (props.data) { diff --git a/src/components/Assets/AssetTypeList/styles.ts b/src/components/Assets/AssetTypeList/styles.ts index ef236f733..5d6debe38 100644 --- a/src/components/Assets/AssetTypeList/styles.ts +++ b/src/components/Assets/AssetTypeList/styles.ts @@ -19,7 +19,7 @@ export const List = styled.ul` `; export const NoDataContainer = styled.div` - min-height: 100vh; + flex-grow: 1; display: flex; flex-direction: column; align-items: center; diff --git a/src/components/Assets/AssetTypeList/types.ts b/src/components/Assets/AssetTypeList/types.ts index 80b830464..5ad35604f 100644 --- a/src/components/Assets/AssetTypeList/types.ts +++ b/src/components/Assets/AssetTypeList/types.ts @@ -1,6 +1,7 @@ export interface AssetTypeListProps { data?: AssetCategoriesData; onAssetTypeSelect: (assetTypeId: string) => void; + services: string[]; } export interface AssetCategoriesData { diff --git a/src/components/Assets/Assets.stories.tsx b/src/components/Assets/Assets.stories.tsx new file mode 100644 index 000000000..d3356d864 --- /dev/null +++ b/src/components/Assets/Assets.stories.tsx @@ -0,0 +1,34 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { Assets } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Assets/Assets", + component: Assets, + 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: { + services: [ + "service_1", + "service_2", + "service_3", + "service_4", + "service_5", + "service_6", + "service_7", + "service_8", + "service_9" + ] + } +}; diff --git a/src/components/Assets/FilterMenu/FilterMenu.stories.tsx b/src/components/Assets/FilterMenu/FilterMenu.stories.tsx new file mode 100644 index 000000000..a4b83f312 --- /dev/null +++ b/src/components/Assets/FilterMenu/FilterMenu.stories.tsx @@ -0,0 +1,94 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { useState } from "react"; +import { FilterMenu } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Assets/FilterMenu", + component: FilterMenu, + 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 = { + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [selectedItems, setSelectedItems] = useState([]); + + const handleItemClick = (value: string) => { + const itemIndex = selectedItems.findIndex((x) => x === value); + + if (itemIndex < 0) { + setSelectedItems([...selectedItems, value]); + } else { + setSelectedItems([ + ...selectedItems.slice(0, itemIndex), + ...selectedItems.slice(itemIndex + 1) + ]); + } + }; + + const items = args.items.map((x) => ({ + ...x, + selected: selectedItems.includes(x.value) + })); + + return ; + }, + args: { + title: "Filter by services", + items: [ + { + label: "very_long_long_long_long_name", + value: "item_1" + }, + { + label: "Item 2", + value: "item_2" + }, + { + label: "Item 3", + value: "item_3" + }, + { + label: "Item 4", + value: "item_4" + }, + { + label: "Item 5", + value: "item_5" + }, + { + label: "Item 6", + value: "item_6" + }, + { + label: "Item 7", + value: "item_7" + }, + { + label: "Item 8", + value: "item_8" + }, + { + label: "Item 9", + value: "item_9" + } + ] + } +}; + +export const NoItems: Story = { + args: { + title: "Filter by services", + items: [] + } +}; diff --git a/src/components/Assets/FilterMenu/index.tsx b/src/components/Assets/FilterMenu/index.tsx new file mode 100644 index 000000000..aac59eebd --- /dev/null +++ b/src/components/Assets/FilterMenu/index.tsx @@ -0,0 +1,80 @@ +import { ChangeEvent, useState } from "react"; +import { Checkbox } from "../../common/Checkbox"; +import { Tooltip } from "../../common/Tooltip"; +import { CrossIcon } from "../../common/icons/CrossIcon"; +import { MagnifierIcon } from "../../common/icons/MagnifierIcon"; +import * as s from "./styles"; +import { FilterMenuProps } from "./types"; + +export const FilterMenu = (props: FilterMenuProps) => { + const [searchInputValue, setSearchInputValue] = useState(""); + + const handleSearchInputChange = (e: ChangeEvent) => { + setSearchInputValue(e.target.value); + }; + + const handleCloseButtonClick = () => { + props.onClose(); + }; + + const handleMenuItemClick = (value: string) => { + props.onItemClick(value); + }; + + const filteredItems = props.items.filter((x) => + x.label.toLocaleLowerCase().includes(searchInputValue.toLowerCase()) + ); + + const selectedItems = props.items.filter((x) => x.selected); + + return ( + + + {props.title} + + + + + + + + + + + + {selectedItems.length > 0 && ( + + {selectedItems.map((x) => ( + + + {x.label} + handleMenuItemClick(x.value)} + > + + + + + ))} + + )} + + {filteredItems.map((item) => ( + + { + handleMenuItemClick(item.value); + }} + /> + + ))} + + + + ); +}; diff --git a/src/components/Assets/FilterMenu/styles.ts b/src/components/Assets/FilterMenu/styles.ts new file mode 100644 index 000000000..0cbfa920e --- /dev/null +++ b/src/components/Assets/FilterMenu/styles.ts @@ -0,0 +1,195 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; + box-shadow: 0 2px 16px rgb(0 0 0 / 36%); + border-radius: 4px; + width: 217px; + height: 177px; + background: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#fbfdff"; + case "dark": + case "dark-jetbrains": + return "#2b2d30"; + } + }}; +`; + +export const Header = styled.div` + font-size: 14px; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: center; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; + +export const CloseButton = styled.button` + padding: 0; + cursor: pointer; + background: none; + border: none; + height: 14px; + color: inherit; +`; + +export const DeleteTagButton = CloseButton; + +export const SearchInputContainer = styled.div` + display: flex; + position: relative; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#4d668a"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } + }}; + + &:focus, + &:hover { + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#7891d0"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; + } +`; + +export const SearchInputIconContainer = styled.div` + display: flex; + align-items: center; + margin: auto; + position: absolute; + top: 0; + bottom: 0; + left: 8px; +`; + +export const SearchInput = styled.input` + font-size: 14px; + padding: 4px 8px 4px 24px; + border-radius: 4px; + outline: none; + width: 100%; + background: transparent; + color: inherit; + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#d0d6eb"; + case "dark": + case "dark-jetbrains": + return "#4e5157"; + } + }}; + + &:focus, + &:hover { + color: inherit; + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#7891d0"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; + } + + &::placeholder { + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#4d668a"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } + }}; + } + + &:hover::placeholder { + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#7891d0"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; + } + + &:focus::placeholder { + color: transparent; + } +`; + +export const ContentContainer = styled.div` + display: flex; + flex-direction: column; + overflow: auto; + gap: 8px; +`; + +export const TagsContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 4px; +`; + +export const Tag = styled.div` + display: flex; + border-radius: 2px; + align-items: center; + gap: 4px; + background: rgba(53 56 205 / 50%); + font-size: 14px; + padding: 4px; + color: #dfe1e5; + max-width: 100%; + box-sizing: border-box; +`; + +export const TagText = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const List = styled.ul` + display: flex; + flex-direction: column; + margin: 0; + padding: 0; +`; + +export const ListItem = styled.li` + flex-direction: row; + width: 100%; + box-sizing: border-box; + list-style-type: none; +`; diff --git a/src/components/Assets/FilterMenu/types.ts b/src/components/Assets/FilterMenu/types.ts new file mode 100644 index 000000000..e50778c73 --- /dev/null +++ b/src/components/Assets/FilterMenu/types.ts @@ -0,0 +1,12 @@ +export interface MenuItem { + label: string; + value: string; + selected?: boolean; +} + +export interface FilterMenuProps { + title: string; + items: MenuItem[]; + onItemClick: (value: string) => void; + onClose: () => void; +} diff --git a/src/components/Assets/actions.ts b/src/components/Assets/actions.ts index 1c97d004d..5bab91285 100644 --- a/src/components/Assets/actions.ts +++ b/src/components/Assets/actions.ts @@ -8,5 +8,7 @@ export const actions = addPrefix(ACTION_PREFIX, { SET_DATA: "SET_DATA", GO_TO_ASSET: "GO_TO_ASSET", GET_CATEGORIES_DATA: "GET_CATEGORIES_DATA", - SET_CATEGORIES_DATA: "SET_CATEGORIES_DATA" + SET_CATEGORIES_DATA: "SET_CATEGORIES_DATA", + GET_SERVICES: "GET_SERVICES", + SET_SERVICES: "SET_SERVICES" }); diff --git a/src/components/Assets/index.tsx b/src/components/Assets/index.tsx index 6b57d76b4..83bdc9f64 100644 --- a/src/components/Assets/index.tsx +++ b/src/components/Assets/index.tsx @@ -1,12 +1,87 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { dispatcher } from "../../dispatcher"; +import { isNumber } from "../../typeGuards/isNumber"; +import { NewPopover } from "../common/NewPopover"; +import { ChevronIcon } from "../common/icons/ChevronIcon"; +import { FilterIcon } from "../common/icons/FilterIcon"; +import { Direction } from "../common/icons/types"; import { AssetList } from "./AssetList"; import { AssetTypeList } from "./AssetTypeList"; +import { FilterMenu } from "./FilterMenu"; +import { actions } from "./actions"; import * as s from "./styles"; +import { AssetsProps, ServiceData } from "./types"; -export const Assets = () => { +const REFRESH_INTERVAL = isNumber(window.assetsRefreshInterval) + ? window.assetsRefreshInterval + : 10 * 1000; // in milliseconds + +export const Assets = (props: AssetsProps) => { const [selectedAssetTypeId, setSelectedAssetTypeId] = useState( null ); + const [services, setServices] = useState([]); + const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const [selectedServices, setSelectedServices] = useState([]); + const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false); + + useEffect(() => { + window.sendMessageToDigma({ + action: actions.GET_SERVICES + }); + + const handleServicesData = (data: unknown, timeStamp: number) => { + setServices((data as ServiceData).services); + setLastSetDataTimeStamp(timeStamp); + }; + + dispatcher.addActionListener(actions.SET_SERVICES, handleServicesData); + + return () => { + dispatcher.removeActionListener(actions.SET_SERVICES, handleServicesData); + }; + }, []); + + useEffect(() => { + const timerId = window.setTimeout(() => { + window.sendMessageToDigma({ + action: actions.GET_SERVICES + }); + }, REFRESH_INTERVAL); + + return () => { + window.clearTimeout(timerId); + }; + }, [lastSetDataTimeStamp]); + + useEffect(() => { + if (props.services) { + setServices(props.services); + } + }, [props.services]); + + const handleServiceMenuClose = () => { + setIsServiceMenuOpen(false); + }; + + const handleServiceMenuItemClick = (service: string) => { + const serviceIndex = selectedServices.findIndex((x) => x === service); + + if (serviceIndex < 0) { + setSelectedServices([...selectedServices, service]); + } else { + setSelectedServices([ + ...selectedServices.slice(0, serviceIndex), + ...selectedServices.slice(serviceIndex + 1) + ]); + } + }; + + const filterMenuItems = services.map((x) => ({ + label: x, + value: x, + selected: selectedServices.includes(x) + })); const handleBackButtonClick = () => { setSelectedAssetTypeId(null); @@ -18,16 +93,63 @@ export const Assets = () => { const renderContent = useMemo((): JSX.Element => { if (!selectedAssetTypeId) { - return ; + return ( + + ); } return ( ); - }, [selectedAssetTypeId]); + }, [selectedAssetTypeId, selectedServices]); - return {renderContent}; + return ( + + + Assets + + } + onOpenChange={setIsServiceMenuOpen} + isOpen={isServiceMenuOpen} + placement={"bottom-start"} + > + + + + Services + {selectedServices.length > 0 ? ( + {selectedServices.length} + ) : ( + + All + + )} + + + + + + + + {renderContent} + + ); }; diff --git a/src/components/Assets/styles.ts b/src/components/Assets/styles.ts index 34a9dc207..8b07bfe38 100644 --- a/src/components/Assets/styles.ts +++ b/src/components/Assets/styles.ts @@ -2,6 +2,8 @@ import styled from "styled-components"; export const Container = styled.div` min-height: 100vh; + display: flex; + flex-direction: column; background: ${({ theme }) => { switch (theme.mode) { case "light": @@ -12,3 +14,87 @@ export const Container = styled.div` } }}; `; + +export const Header = styled.div` + padding: 8px; + display: flex; + gap: 8px; + align-items: center; + font-size: 16px; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; + +export const ServiceMenuButton = styled.button` + border: 1px solid #4e5157; + background: transparent; + border-radius: 4px; + padding: 8px; + display: flex; + gap: 10px; + align-items: center; +`; + +export const ServiceMenuButtonLabel = styled.span` + display: flex; + gap: 4px; + align-items: center; + font-size: 14px; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#818594"; + case "dark": + case "dark-jetbrains": + return "#b4b8bf"; + } + }}; +`; + +export const SelectedServiceNumberPlaceholder = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; + user-select: none; +`; + +export const Number = styled.span` + min-width: 18px; + height: 18px; + flex-shrink: 0; + font-size: 14px; + line-height: 100%; + font-weight: 500; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + background: #5053d4; + padding: 4px; +`; + +export const ServiceMenuChevronIconContainer = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; diff --git a/src/components/Assets/types.ts b/src/components/Assets/types.ts new file mode 100644 index 000000000..f03b422fa --- /dev/null +++ b/src/components/Assets/types.ts @@ -0,0 +1,7 @@ +export interface ServiceData { + services: string[]; +} + +export type AssetsProps = { + services?: string[]; +}; diff --git a/src/components/InstallationWizard/index.tsx b/src/components/InstallationWizard/index.tsx index 8f2506fba..4ffa3aafd 100644 --- a/src/components/InstallationWizard/index.tsx +++ b/src/components/InstallationWizard/index.tsx @@ -404,7 +404,6 @@ export const InstallationWizard = () => { ) : ( <> ; export const Default: Story = { args: { - id: "id", label: "Click me", value: false } }; + +export const Checked: Story = { + args: { + label: "Click me", + value: true + } +}; + +export const Disabled: Story = { + args: { + label: "Click me", + value: true, + disabled: true + } +}; diff --git a/src/components/common/Checkbox/index.tsx b/src/components/common/Checkbox/index.tsx index eea104f4e..af108b876 100644 --- a/src/components/common/Checkbox/index.tsx +++ b/src/components/common/Checkbox/index.tsx @@ -1,4 +1,6 @@ import { ChangeEvent } from "react"; +import { Tooltip } from "../Tooltip"; +import { CheckmarkIcon } from "../icons/CheckmarkIcon"; import * as s from "./styles"; import { CheckboxProps } from "./types"; @@ -8,15 +10,23 @@ export const Checkbox = (props: CheckboxProps) => { }; return ( - - - {props.label} - + + + + + {props.value && ( + + + + )} + + {props.label} + + ); }; diff --git a/src/components/common/Checkbox/styles.ts b/src/components/common/Checkbox/styles.ts index 3faf031eb..29ef5b2e0 100644 --- a/src/components/common/Checkbox/styles.ts +++ b/src/components/common/Checkbox/styles.ts @@ -1,6 +1,131 @@ import styled from "styled-components"; +import { LabelProps } from "./types"; -export const Container = styled.div` +export const CheckboxContainer = styled.div` + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +`; + +export const CheckContainer = styled.div` + position: absolute; + display: flex; + align-items: center; + justify-content: center; +`; + +export const Checkbox = styled.input` + appearance: none; + margin: 1px; + cursor: pointer; + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#b9c0d4"; + case "dark": + case "dark-jetbrains": + return "#7c7c94"; + } + }}; + width: 10px; + height: 10px; + border-radius: 1px; + background: transparent; + + &:checked { + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#426dda"; + case "dark": + case "dark-jetbrains": + return "#7891d0"; + } + }}; + } + + &:hover { + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#828797"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } + }}; + } + + &:disabled { + border: 1px solid + ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#dadada"; + case "dark": + case "dark-jetbrains": + return "#49494d"; + } + }}; + } + + & + ${CheckContainer} { + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#b9c0d4"; + case "dark": + case "dark-jetbrains": + return "#7c7c94"; + } + }}; + } + + &:checked + ${CheckContainer} { + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#426dda"; + case "dark": + case "dark-jetbrains": + return "#7891d0"; + } + }}; + } + + &:hover + ${CheckContainer} { + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#828797"; + case "dark": + case "dark-jetbrains": + return "#dadada"; + } + }}; + } + + &:disabled + ${CheckContainer} { + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#dadada"; + case "dark": + case "dark-jetbrains": + return "#49494d"; + } + }}; + } +`; + +export const Label = styled.label` + user-select: none; + cursor: pointer; display: flex; gap: 4px; align-items: center; @@ -26,13 +151,23 @@ export const Container = styled.div` } }}; } -`; -export const Checkbox = styled.input` - cursor: pointer; + &:has(${Checkbox}:disabled) { + cursor: initial; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#dadada"; + case "dark": + case "dark-jetbrains": + return "#49494d"; + } + }}; + } `; -export const Label = styled.label` - user-select: none; - cursor: pointer; +export const LabelText = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; diff --git a/src/components/common/Checkbox/types.ts b/src/components/common/Checkbox/types.ts index 54036c48d..c9f959bd0 100644 --- a/src/components/common/Checkbox/types.ts +++ b/src/components/common/Checkbox/types.ts @@ -1,9 +1,12 @@ import { ReactNode } from "react"; export interface CheckboxProps { - id: string; value: boolean; label: ReactNode; disabled?: boolean; onChange: (value: boolean) => void; } + +export interface LabelProps { + $disabled?: boolean; +} diff --git a/src/components/common/icons/CheckmarkIcon.tsx b/src/components/common/icons/CheckmarkIcon.tsx new file mode 100644 index 000000000..24878579a --- /dev/null +++ b/src/components/common/icons/CheckmarkIcon.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { useIconProps } from "./hooks"; +import { IconProps } from "./types"; + +interface CheckmarkIconComponentProps extends IconProps { + height?: number; +} + +const CheckmarkIconComponent = (props: CheckmarkIconComponentProps) => { + const { color } = useIconProps(props); + const height = props.height || 4; + + return ( + + + + ); +}; + +export const CheckmarkIcon = React.memo(CheckmarkIconComponent); diff --git a/src/components/common/icons/FilterIcon.tsx b/src/components/common/icons/FilterIcon.tsx new file mode 100644 index 000000000..a3edf4605 --- /dev/null +++ b/src/components/common/icons/FilterIcon.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { useIconProps } from "./hooks"; +import { IconProps } from "./types"; + +const FilterIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + ); +}; + +export const FilterIcon = React.memo(FilterIconComponent); From 3677d308634ae48052a27d737796e6e22a6eb924 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 28 Nov 2023 12:54:37 +0100 Subject: [PATCH 2/3] Update styles --- src/components/Assets/styles.ts | 1 - src/components/common/Checkbox/index.tsx | 2 +- src/components/common/Checkbox/styles.ts | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/Assets/styles.ts b/src/components/Assets/styles.ts index 8b07bfe38..0cd90e04f 100644 --- a/src/components/Assets/styles.ts +++ b/src/components/Assets/styles.ts @@ -84,7 +84,6 @@ export const Number = styled.span` justify-content: center; color: #fff; background: #5053d4; - padding: 4px; `; export const ServiceMenuChevronIconContainer = styled.span` diff --git a/src/components/common/Checkbox/index.tsx b/src/components/common/Checkbox/index.tsx index af108b876..bd21a9602 100644 --- a/src/components/common/Checkbox/index.tsx +++ b/src/components/common/Checkbox/index.tsx @@ -21,7 +21,7 @@ export const Checkbox = (props: CheckboxProps) => { /> {props.value && ( - + )} diff --git a/src/components/common/Checkbox/styles.ts b/src/components/common/Checkbox/styles.ts index 29ef5b2e0..0a477d69d 100644 --- a/src/components/common/Checkbox/styles.ts +++ b/src/components/common/Checkbox/styles.ts @@ -30,8 +30,8 @@ export const Checkbox = styled.input` return "#7c7c94"; } }}; - width: 10px; - height: 10px; + width: 12px; + height: 12px; border-radius: 1px; background: transparent; From 98f53eefd9956f6d97ed9fd40720ecc48ba2f5bd Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 28 Nov 2023 18:38:12 +0100 Subject: [PATCH 3/3] Fix styles and backend version check --- src/components/Assets/AssetList/index.tsx | 2 +- src/components/Assets/AssetList/styles.ts | 17 ++-- src/components/Assets/AssetTypeList/index.tsx | 6 +- src/components/Assets/AssetTypeList/styles.ts | 2 +- src/components/Assets/index.tsx | 91 ++++++++++++------- src/components/Assets/styles.ts | 4 +- 6 files changed, 71 insertions(+), 51 deletions(-) diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index af85a16fc..fd7585c63 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -207,7 +207,7 @@ export const AssetList = (props: AssetListProps) => { previousDebouncedSearchInputValue !== debouncedSearchInputValue) || (isString(previousAssetTypeId) && previousAssetTypeId !== props.assetTypeId) || - previousServices !== props.services + (Array.isArray(previousServices) && previousServices !== props.services) ) { window.sendMessageToDigma({ action: actions.GET_DATA, diff --git a/src/components/Assets/AssetList/styles.ts b/src/components/Assets/AssetList/styles.ts index 743543374..7195e614b 100644 --- a/src/components/Assets/AssetList/styles.ts +++ b/src/components/Assets/AssetList/styles.ts @@ -9,7 +9,8 @@ import { export const Container = styled.div` display: flex; flex-direction: column; - height: 100vh; + height: 100%; + overflow: hidden; `; export const BackButton = styled.button` @@ -20,6 +21,13 @@ export const BackButton = styled.button` cursor: pointer; `; +export const Toolbar = styled.div` + display: flex; + justify-content: space-between; + padding: 8px; + gap: 4px; +`; + export const Header = styled.div` display: flex; align-items: center; @@ -47,13 +55,6 @@ export const Header = styled.div` }}; `; -export const Toolbar = styled.div` - display: flex; - justify-content: space-between; - padding: 8px; - gap: 4px; -`; - export const PopoverContainer = styled.div` margin-left: auto; `; diff --git a/src/components/Assets/AssetTypeList/index.tsx b/src/components/Assets/AssetTypeList/index.tsx index b818a1d0a..ba5e18b29 100644 --- a/src/components/Assets/AssetTypeList/index.tsx +++ b/src/components/Assets/AssetTypeList/index.tsx @@ -72,7 +72,7 @@ export const AssetTypeList = (props: AssetTypeListProps) => { if ( (isString(previousEnvironment) && previousEnvironment !== config.environment) || - previousServices !== props.services + (Array.isArray(previousServices) && previousServices !== props.services) ) { window.sendMessageToDigma({ action: actions.GET_CATEGORIES_DATA, @@ -99,10 +99,6 @@ export const AssetTypeList = (props: AssetTypeListProps) => { } }); }, REFRESH_INTERVAL); - - return () => { - window.clearTimeout(refreshTimerId.current); - }; } }, [props.services, previousLastSetDataTimeStamp, lastSetDataTimeStamp]); diff --git a/src/components/Assets/AssetTypeList/styles.ts b/src/components/Assets/AssetTypeList/styles.ts index 5d6debe38..7602d6504 100644 --- a/src/components/Assets/AssetTypeList/styles.ts +++ b/src/components/Assets/AssetTypeList/styles.ts @@ -5,7 +5,7 @@ export const List = styled.ul` display: flex; flex-direction: column; gap: 8px; - padding: 8px; + padding: 0 8px 8px; margin: 0; color: ${({ theme }) => { switch (theme.mode) { diff --git a/src/components/Assets/index.tsx b/src/components/Assets/index.tsx index 83bdc9f64..7dd08b1b4 100644 --- a/src/components/Assets/index.tsx +++ b/src/components/Assets/index.tsx @@ -1,6 +1,14 @@ -import { useEffect, useMemo, useState } from "react"; +import { + useContext, + useEffect, + useLayoutEffect, + useMemo, + useState +} from "react"; +import { gte, valid } from "semver"; import { dispatcher } from "../../dispatcher"; import { isNumber } from "../../typeGuards/isNumber"; +import { ConfigContext } from "../common/App/ConfigContext"; import { NewPopover } from "../common/NewPopover"; import { ChevronIcon } from "../common/icons/ChevronIcon"; import { FilterIcon } from "../common/icons/FilterIcon"; @@ -24,8 +32,19 @@ export const Assets = (props: AssetsProps) => { const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); const [selectedServices, setSelectedServices] = useState([]); const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false); + const config = useContext(ConfigContext); - useEffect(() => { + const backendVersion = config.backendInfo?.applicationVersion; + + const isServiceFilterVisible = + backendVersion && + (backendVersion === "unknown" || + (valid(backendVersion) && gte(backendVersion, "v0.2.174"))); + + useLayoutEffect(() => { + window.sendMessageToDigma({ + action: actions.INITIALIZE + }); window.sendMessageToDigma({ action: actions.GET_SERVICES }); @@ -114,40 +133,42 @@ export const Assets = (props: AssetsProps) => { Assets - - } - onOpenChange={setIsServiceMenuOpen} - isOpen={isServiceMenuOpen} - placement={"bottom-start"} - > - - - - Services - {selectedServices.length > 0 ? ( - {selectedServices.length} - ) : ( - - All - - )} - - - - - - + } + onOpenChange={setIsServiceMenuOpen} + isOpen={isServiceMenuOpen} + placement={"bottom-start"} + > + + + + Services + {selectedServices.length > 0 ? ( + {selectedServices.length} + ) : ( + + All + + )} + + + + + + + )} {renderContent} diff --git a/src/components/Assets/styles.ts b/src/components/Assets/styles.ts index 0cd90e04f..414034c22 100644 --- a/src/components/Assets/styles.ts +++ b/src/components/Assets/styles.ts @@ -1,7 +1,7 @@ import styled from "styled-components"; export const Container = styled.div` - min-height: 100vh; + height: 100%; display: flex; flex-direction: column; background: ${({ theme }) => { @@ -21,6 +21,8 @@ export const Header = styled.div` gap: 8px; align-items: center; font-size: 16px; + flex-shrink: 0; + height: 36px; color: ${({ theme }) => { switch (theme.mode) { case "light":