From f5ad90d7dcfd7ce6db1ce03d2a6eb55278ae9c0d Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Thu, 12 Dec 2024 16:14:57 +0000 Subject: [PATCH 01/27] extrated search bar from TopicList to be its own component --- .../suite-base/src/components/SearchBar.tsx | 70 +++++++++++++++++++ .../src/components/TopicList/TopicList.tsx | 19 ++--- 2 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 packages/suite-base/src/components/SearchBar.tsx diff --git a/packages/suite-base/src/components/SearchBar.tsx b/packages/suite-base/src/components/SearchBar.tsx new file mode 100644 index 0000000000..1a5563c800 --- /dev/null +++ b/packages/suite-base/src/components/SearchBar.tsx @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import ClearIcon from "@mui/icons-material/Clear"; +import SearchIcon from "@mui/icons-material/Search"; +import { IconButton, TextField, InputAdornment } from "@mui/material"; +import { TextFieldProps } from "@mui/material/TextField"; +import { PropsWithChildren } from "react"; +import { makeStyles } from "tss-react/mui"; + +const useStyles = makeStyles()(() => ({ + filterStartAdornment: { + display: "flex", + }, +})); + +function SearchBar( + props: PropsWithChildren< + TextFieldProps & { + onClear?: () => void; + showClearIcon?: boolean; + startAdornment?: React.ReactNode; + } + >, +): React.JSX.Element { + const { + id = "search-bar", + variant = "filled", + disabled = false, + value, + onChange, + onClear, + showClearIcon = false, + startAdornment = , + placeholder = "Search...", + ...rest + } = props; + + const { classes } = useStyles(); + + return ( + + {startAdornment} + + ), + endAdornment: showClearIcon && ( + + + + + + ), + }} + {...rest} + /> + ); +} + +export default SearchBar; diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index 94548a0d3a..11a15651b2 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -7,15 +7,7 @@ import ClearIcon from "@mui/icons-material/Clear"; import SearchIcon from "@mui/icons-material/Search"; -import { - IconButton, - List, - ListItem, - ListItemText, - PopoverPosition, - Skeleton, - TextField, -} from "@mui/material"; +import { IconButton, List, ListItem, ListItemText, PopoverPosition, Skeleton } from "@mui/material"; import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLatest } from "react-use"; @@ -34,6 +26,7 @@ import { useMessagePipeline, } from "@lichtblick/suite-base/components/MessagePipeline"; import { DraggedMessagePath } from "@lichtblick/suite-base/components/PanelExtensionAdapter"; +import SearchBar from "@lichtblick/suite-base/components/SearchBar"; import { ContextMenu } from "@lichtblick/suite-base/components/TopicList/ContextMenu"; import { PlayerPresence } from "@lichtblick/suite-base/players/types"; import { MessagePathSelectionProvider } from "@lichtblick/suite-base/services/messagePathDragging/MessagePathSelectionProvider"; @@ -208,7 +201,7 @@ export function TopicList(): React.JSX.Element { return ( <>
-
- { setFilterText(event.target.value); }} value={undebouncedFilterText} - fullWidth - placeholder={t("searchBarPlaceholder")} InputProps={{ inputProps: { "data-testid": "topic-filter" }, size: "small", From 5710b7c913ab6564ecc7b8eb0eacb8f0f158d26c Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Thu, 12 Dec 2024 17:42:01 +0000 Subject: [PATCH 02/27] update on SearchBar component and TopicList use of it --- .../suite-base/src/components/SearchBar.tsx | 63 +++++++++++-------- .../src/components/TopicList/TopicList.tsx | 52 +++++---------- 2 files changed, 52 insertions(+), 63 deletions(-) diff --git a/packages/suite-base/src/components/SearchBar.tsx b/packages/suite-base/src/components/SearchBar.tsx index 1a5563c800..d42801a395 100644 --- a/packages/suite-base/src/components/SearchBar.tsx +++ b/packages/suite-base/src/components/SearchBar.tsx @@ -8,10 +8,17 @@ import { TextFieldProps } from "@mui/material/TextField"; import { PropsWithChildren } from "react"; import { makeStyles } from "tss-react/mui"; -const useStyles = makeStyles()(() => ({ +const useStyles = makeStyles()((theme) => ({ filterStartAdornment: { display: "flex", }, + filterBar: { + top: 0, + zIndex: theme.zIndex.appBar, + padding: theme.spacing(0.5), + position: "sticky", + backgroundColor: theme.palette.background.paper, + }, })); function SearchBar( @@ -39,31 +46,35 @@ function SearchBar( const { classes } = useStyles(); return ( - - {startAdornment} - - ), - endAdornment: showClearIcon && ( - - - - - - ), - }} - {...rest} - /> + <> +
+ + {startAdornment} + + ), + endAdornment: showClearIcon && ( + + + + + + ), + }} + {...rest} + /> +
+ ); } diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index 11a15651b2..0211f25b6b 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -5,9 +5,8 @@ // License, v2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/ -import ClearIcon from "@mui/icons-material/Clear"; import SearchIcon from "@mui/icons-material/Search"; -import { IconButton, List, ListItem, ListItemText, PopoverPosition, Skeleton } from "@mui/material"; +import { List, ListItem, ListItemText, PopoverPosition, Skeleton } from "@mui/material"; import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLatest } from "react-use"; @@ -54,9 +53,6 @@ const useStyles = makeStyles()((theme) => ({ position: "sticky", backgroundColor: theme.palette.background.paper, }, - filterStartAdornment: { - display: "flex", - }, skeletonText: { marginTop: theme.spacing(0.5), marginBottom: theme.spacing(0.5), @@ -89,6 +85,9 @@ export function TopicList(): React.JSX.Element { const { classes } = useStyles(); const [undebouncedFilterText, setFilterText] = useState(""); const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); + const onClear = () => { + setFilterText(""); + }; const playerPresence = useMessagePipeline(selectPlayerPresence); const { topics, datatypes } = useDataSourceInfo(); @@ -231,38 +230,17 @@ export function TopicList(): React.JSX.Element { return (
-
- { - setFilterText(event.target.value); - }} - value={undebouncedFilterText} - InputProps={{ - inputProps: { "data-testid": "topic-filter" }, - size: "small", - startAdornment: ( - - ), - endAdornment: undebouncedFilterText && ( - { - setFilterText(""); - }} - edge="end" - > - - - ), - }} - /> -
+ { + setFilterText(event.target.value); + }} + value={undebouncedFilterText} + showClearIcon={!!debouncedFilterText} + onClear={onClear} + /> {treeItems.length > 0 ? (
From 56add804f4ad6aa05a7d3f33a32d345c53591b65 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 13 Dec 2024 11:59:06 +0000 Subject: [PATCH 03/27] added search bar to extensions menu --- .../components/ExtensionsSettings/index.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index 91d69f0091..996a9cb98b 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -19,10 +19,12 @@ import * as _ from "lodash-es"; import { useEffect, useMemo, useState } from "react"; import { useAsyncFn } from "react-use"; import { makeStyles } from "tss-react/mui"; +import { useDebounce } from "use-debounce"; import Log from "@lichtblick/log"; import { Immutable } from "@lichtblick/suite"; import { ExtensionDetails } from "@lichtblick/suite-base/components/ExtensionDetails"; +import SearchBar from "@lichtblick/suite-base/components/SearchBar"; import Stack from "@lichtblick/suite-base/components/Stack"; import { useExtensionCatalog } from "@lichtblick/suite-base/context/ExtensionCatalogContext"; import { @@ -88,6 +90,11 @@ function ExtensionListEntry(props: { } export default function ExtensionsSettings(): React.ReactElement { + const [undebouncedFilterText, setFilterText] = useState(""); + const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); + const onClear = () => { + setFilterText(""); + }; const [focusedExtension, setFocusedExtension] = useState< | { installed: boolean; @@ -183,6 +190,19 @@ export default function ExtensionsSettings(): React.ReactElement { Check your internet connection and try again. )} +
+ { + setFilterText(event.target.value); + }} + value={undebouncedFilterText} + showClearIcon={!!debouncedFilterText} + onClear={onClear} + /> +
{!_.isEmpty(namespacedEntries) ? ( Object.entries(namespacedEntries).map(([namespace, entries]) => ( From 834a0a28433aa355b2d7f51f550bb7198644ae16 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 13 Dec 2024 16:41:50 +0000 Subject: [PATCH 04/27] added searching logic to search bar on extensions --- .../components/ExtensionsSettings/index.tsx | 87 +++++++++++-------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index 996a9cb98b..b46830045a 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -90,10 +90,10 @@ function ExtensionListEntry(props: { } export default function ExtensionsSettings(): React.ReactElement { - const [undebouncedFilterText, setFilterText] = useState(""); + const [undebouncedFilterText, setUndebouncedFilterText] = useState(""); const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); const onClear = () => { - setFilterText(""); + setUndebouncedFilterText(""); }; const [focusedExtension, setFocusedExtension] = useState< | { @@ -115,9 +115,10 @@ export default function ExtensionsSettings(): React.ReactElement { [marketplaceEntries], ); - const installedEntries = useMemo( - () => - (installed ?? []).map((entry) => { + const installedEntries = useMemo(() => { + const searchLower = debouncedFilterText.toLowerCase(); + return (installed ?? []) + .map((entry) => { const marketplaceEntry = marketplaceMap[entry.id]; if (marketplaceEntry != undefined) { return { ...marketplaceEntry, namespace: entry.namespace }; @@ -137,9 +138,13 @@ export default function ExtensionsSettings(): React.ReactElement { namespace: entry.namespace, qualifiedName: entry.qualifiedName, }; - }), - [installed, marketplaceMap], - ); + }) + .filter( + (entry) => + entry.name.toLowerCase().includes(searchLower) || + entry.description.toLowerCase().includes(searchLower), + ); + }, [installed, marketplaceMap, debouncedFilterText]); const namespacedEntries = useMemo( () => _.groupBy(installedEntries, (entry) => entry.namespace), @@ -175,6 +180,43 @@ export default function ExtensionsSettings(): React.ReactElement { ); } + function generatePlaceholderList(message?: string): React.ReactElement { + return ( + + + + + + ); + } + + function listExtensions() { + if (!_.isEmpty(namespacedEntries)) { + return Object.entries(namespacedEntries).map(([namespace, entries]) => ( + + + + {displayNameForNamespace(namespace)} + + + {entries.map((entry) => ( + { + setFocusedExtension({ installed: true, entry }); + }} + /> + ))} + + )); + } else if (_.isEmpty(namespacedEntries) && undebouncedFilterText) { + return generatePlaceholderList("No extensions found"); //translate this!!!! + } else { + return generatePlaceholderList("No extensions installed"); //translate this!!!! + } + } + return ( {marketplaceEntries.error && ( @@ -196,39 +238,14 @@ export default function ExtensionsSettings(): React.ReactElement { placeholder="Search extensions..." variant="outlined" onChange={(event) => { - setFilterText(event.target.value); + setUndebouncedFilterText(event.target.value); }} value={undebouncedFilterText} showClearIcon={!!debouncedFilterText} onClear={onClear} />
- {!_.isEmpty(namespacedEntries) ? ( - Object.entries(namespacedEntries).map(([namespace, entries]) => ( - - - - {displayNameForNamespace(namespace)} - - - {entries.map((entry) => ( - { - setFocusedExtension({ installed: true, entry }); - }} - /> - ))} - - )) - ) : ( - - - - - - )} + {listExtensions()} From 9d2ae92959909296b20b103f30bacdaad5553834 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 13 Dec 2024 16:57:59 +0000 Subject: [PATCH 05/27] separting styles to its own files --- .../ExtensionsSettings/Index.style.ts | 10 +++ .../components/ExtensionsSettings/index.tsx | 9 +-- .../src/components/SearchBar.style.ts | 17 +++++ .../suite-base/src/components/SearchBar.tsx | 70 ++++++++----------- .../components/TopicList/TopicList.style.ts | 26 +++++++ .../src/components/TopicList/TopicList.tsx | 24 +------ 6 files changed, 84 insertions(+), 72 deletions(-) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/Index.style.ts create mode 100644 packages/suite-base/src/components/SearchBar.style.ts create mode 100644 packages/suite-base/src/components/TopicList/TopicList.style.ts diff --git a/packages/suite-base/src/components/ExtensionsSettings/Index.style.ts b/packages/suite-base/src/components/ExtensionsSettings/Index.style.ts new file mode 100644 index 0000000000..030e93efaf --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/Index.style.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { makeStyles } from "tss-react/mui"; + +export const useStyles = makeStyles()((theme) => ({ + listItemButton: { + "&:hover": { color: theme.palette.primary.main }, + }, +})); diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index b46830045a..d97446dd92 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -18,7 +18,6 @@ import { import * as _ from "lodash-es"; import { useEffect, useMemo, useState } from "react"; import { useAsyncFn } from "react-use"; -import { makeStyles } from "tss-react/mui"; import { useDebounce } from "use-debounce"; import Log from "@lichtblick/log"; @@ -32,13 +31,9 @@ import { useExtensionMarketplace, } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; -const log = Log.getLogger(__filename); +import { useStyles } from "./Index.style"; -const useStyles = makeStyles()((theme) => ({ - listItemButton: { - "&:hover": { color: theme.palette.primary.main }, - }, -})); +const log = Log.getLogger(__filename); function displayNameForNamespace(namespace: string): string { switch (namespace) { diff --git a/packages/suite-base/src/components/SearchBar.style.ts b/packages/suite-base/src/components/SearchBar.style.ts new file mode 100644 index 0000000000..41e3eda1a8 --- /dev/null +++ b/packages/suite-base/src/components/SearchBar.style.ts @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { makeStyles } from "tss-react/mui"; + +export const useStyles = makeStyles()((theme) => ({ + filterStartAdornment: { + display: "flex", + }, + filterBar: { + top: 0, + zIndex: theme.zIndex.appBar, + padding: theme.spacing(0.5), + position: "sticky", + backgroundColor: theme.palette.background.paper, + }, +})); diff --git a/packages/suite-base/src/components/SearchBar.tsx b/packages/suite-base/src/components/SearchBar.tsx index d42801a395..7fe4be1b7d 100644 --- a/packages/suite-base/src/components/SearchBar.tsx +++ b/packages/suite-base/src/components/SearchBar.tsx @@ -6,20 +6,8 @@ import SearchIcon from "@mui/icons-material/Search"; import { IconButton, TextField, InputAdornment } from "@mui/material"; import { TextFieldProps } from "@mui/material/TextField"; import { PropsWithChildren } from "react"; -import { makeStyles } from "tss-react/mui"; -const useStyles = makeStyles()((theme) => ({ - filterStartAdornment: { - display: "flex", - }, - filterBar: { - top: 0, - zIndex: theme.zIndex.appBar, - padding: theme.spacing(0.5), - position: "sticky", - backgroundColor: theme.palette.background.paper, - }, -})); +import { useStyles } from "@lichtblick/suite-base/components/SearchBar.style"; function SearchBar( props: PropsWithChildren< @@ -46,35 +34,33 @@ function SearchBar( const { classes } = useStyles(); return ( - <> -
- - {startAdornment} - - ), - endAdornment: showClearIcon && ( - - - - - - ), - }} - {...rest} - /> -
- +
+ + {startAdornment} + + ), + endAdornment: showClearIcon && ( + + + + + + ), + }} + {...rest} + /> +
); } diff --git a/packages/suite-base/src/components/TopicList/TopicList.style.ts b/packages/suite-base/src/components/TopicList/TopicList.style.ts new file mode 100644 index 0000000000..937078b042 --- /dev/null +++ b/packages/suite-base/src/components/TopicList/TopicList.style.ts @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { makeStyles } from "tss-react/mui"; + +export const useStyles = makeStyles()((theme) => ({ + root: { + width: "100%", + height: "100%", + overflow: "hidden", + display: "flex", + flexDirection: "column", + containerType: "inline-size", + }, + filterBar: { + top: 0, + zIndex: theme.zIndex.appBar, + padding: theme.spacing(0.5), + position: "sticky", + backgroundColor: theme.palette.background.paper, + }, + skeletonText: { + marginTop: theme.spacing(0.5), + marginBottom: theme.spacing(0.5), + }, +})); diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index 0211f25b6b..5127d2a1d0 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -12,7 +12,6 @@ import { useTranslation } from "react-i18next"; import { useLatest } from "react-use"; import { AutoSizer } from "react-virtualized"; import { ListChildComponentProps, VariableSizeList } from "react-window"; -import { makeStyles } from "tss-react/mui"; import { useDebounce } from "use-debounce"; import { filterMap } from "@lichtblick/den/collection"; @@ -31,34 +30,13 @@ import { PlayerPresence } from "@lichtblick/suite-base/players/types"; import { MessagePathSelectionProvider } from "@lichtblick/suite-base/services/messagePathDragging/MessagePathSelectionProvider"; import { MessagePathRow } from "./MessagePathRow"; +import { useStyles } from "./TopicList.style"; import { TopicRow } from "./TopicRow"; import { useMultiSelection } from "./useMultiSelection"; import { TopicListItem, useTopicListSearch } from "./useTopicListSearch"; const selectPlayerPresence = ({ playerState }: MessagePipelineContext) => playerState.presence; -const useStyles = makeStyles()((theme) => ({ - root: { - width: "100%", - height: "100%", - overflow: "hidden", - display: "flex", - flexDirection: "column", - containerType: "inline-size", - }, - filterBar: { - top: 0, - zIndex: theme.zIndex.appBar, - padding: theme.spacing(0.5), - position: "sticky", - backgroundColor: theme.palette.background.paper, - }, - skeletonText: { - marginTop: theme.spacing(0.5), - marginBottom: theme.spacing(0.5), - }, -})); - function getDraggedMessagePath(treeItem: TopicListItem): DraggedMessagePath { switch (treeItem.type) { case "topic": From b04412f46dd02dce5675337f872383913e3541a9 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 13 Dec 2024 18:13:10 +0000 Subject: [PATCH 06/27] added padding on searchBar | implemented translations on extensionsSettings --- .../components/ExtensionsSettings/index.tsx | 19 +++++++++++-------- .../src/components/SearchBar.style.ts | 2 +- .../suite-base/src/components/SearchBar.tsx | 4 +--- .../src/i18n/en/extensionsSettings.ts | 12 ++++++++++++ packages/suite-base/src/i18n/en/index.ts | 1 + 5 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 packages/suite-base/src/i18n/en/extensionsSettings.ts diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index d97446dd92..6eaa58bcc1 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -17,6 +17,7 @@ import { } from "@mui/material"; import * as _ from "lodash-es"; import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useAsyncFn } from "react-use"; import { useDebounce } from "use-debounce"; @@ -85,6 +86,7 @@ function ExtensionListEntry(props: { } export default function ExtensionsSettings(): React.ReactElement { + const { t } = useTranslation("extensionsSettings"); const [undebouncedFilterText, setUndebouncedFilterText] = useState(""); const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); const onClear = () => { @@ -189,7 +191,7 @@ export default function ExtensionsSettings(): React.ReactElement { if (!_.isEmpty(namespacedEntries)) { return Object.entries(namespacedEntries).map(([namespace, entries]) => ( - + {displayNameForNamespace(namespace)} @@ -206,9 +208,9 @@ export default function ExtensionsSettings(): React.ReactElement { )); } else if (_.isEmpty(namespacedEntries) && undebouncedFilterText) { - return generatePlaceholderList("No extensions found"); //translate this!!!! + return generatePlaceholderList(t("noExtensionsFound")); } else { - return generatePlaceholderList("No extensions installed"); //translate this!!!! + return generatePlaceholderList(t("noExtensionsAvailable")); } } @@ -223,14 +225,15 @@ export default function ExtensionsSettings(): React.ReactElement { } > - Failed to retrieve the list of available marketplace extensions - Check your internet connection and try again. + {t("failedToRetrieveMarketplaceExtensions")} + {t("checkInternetConnection")} )}
{ setUndebouncedFilterText(event.target.value); @@ -242,9 +245,9 @@ export default function ExtensionsSettings(): React.ReactElement {
{listExtensions()} - + - Available + {t("available")} {filteredMarketplaceEntries.map((entry) => ( diff --git a/packages/suite-base/src/components/SearchBar.style.ts b/packages/suite-base/src/components/SearchBar.style.ts index 41e3eda1a8..f3b85321d5 100644 --- a/packages/suite-base/src/components/SearchBar.style.ts +++ b/packages/suite-base/src/components/SearchBar.style.ts @@ -7,7 +7,7 @@ export const useStyles = makeStyles()((theme) => ({ filterStartAdornment: { display: "flex", }, - filterBar: { + filterSearchBar: { top: 0, zIndex: theme.zIndex.appBar, padding: theme.spacing(0.5), diff --git a/packages/suite-base/src/components/SearchBar.tsx b/packages/suite-base/src/components/SearchBar.tsx index 7fe4be1b7d..9faff9ca78 100644 --- a/packages/suite-base/src/components/SearchBar.tsx +++ b/packages/suite-base/src/components/SearchBar.tsx @@ -27,21 +27,19 @@ function SearchBar( onClear, showClearIcon = false, startAdornment = , - placeholder = "Search...", ...rest } = props; const { classes } = useStyles(); return ( -
+
+// SPDX-License-Identifier: MPL-2.0 + +export const extensionsSettings = { + noExtensionsFound: "No extensions found", + noExtensionsAvailable: "No extensions available", + failedToRetrieveMarketplaceExtensions: + "Failed to retrieve the list of available marketplace extensions", + checkInternetConnection: "Check your internet connection and try again.", + searchExtensions: "Search extensions...", + available: "Available", +}; diff --git a/packages/suite-base/src/i18n/en/index.ts b/packages/suite-base/src/i18n/en/index.ts index b8063996d2..63bdb0cb9c 100644 --- a/packages/suite-base/src/i18n/en/index.ts +++ b/packages/suite-base/src/i18n/en/index.ts @@ -10,6 +10,7 @@ export * from "./appBar"; export * from "./appSettings"; export * from "./dataSourceInfo"; export * from "./desktopWindow"; +export * from "./extensionsSettings"; export * from "./gauge"; export * from "./general"; export * from "./incompatibleLayoutVersion"; From 022d9009171abb81d1fc112498054076efe26d9d Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Thu, 12 Dec 2024 16:14:57 +0000 Subject: [PATCH 07/27] extrated search bar from TopicList to be its own component --- .../suite-base/src/components/SearchBar.tsx | 70 +++++++++++++++++++ .../src/components/TopicList/TopicList.tsx | 19 ++--- 2 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 packages/suite-base/src/components/SearchBar.tsx diff --git a/packages/suite-base/src/components/SearchBar.tsx b/packages/suite-base/src/components/SearchBar.tsx new file mode 100644 index 0000000000..1a5563c800 --- /dev/null +++ b/packages/suite-base/src/components/SearchBar.tsx @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import ClearIcon from "@mui/icons-material/Clear"; +import SearchIcon from "@mui/icons-material/Search"; +import { IconButton, TextField, InputAdornment } from "@mui/material"; +import { TextFieldProps } from "@mui/material/TextField"; +import { PropsWithChildren } from "react"; +import { makeStyles } from "tss-react/mui"; + +const useStyles = makeStyles()(() => ({ + filterStartAdornment: { + display: "flex", + }, +})); + +function SearchBar( + props: PropsWithChildren< + TextFieldProps & { + onClear?: () => void; + showClearIcon?: boolean; + startAdornment?: React.ReactNode; + } + >, +): React.JSX.Element { + const { + id = "search-bar", + variant = "filled", + disabled = false, + value, + onChange, + onClear, + showClearIcon = false, + startAdornment = , + placeholder = "Search...", + ...rest + } = props; + + const { classes } = useStyles(); + + return ( + + {startAdornment} + + ), + endAdornment: showClearIcon && ( + + + + + + ), + }} + {...rest} + /> + ); +} + +export default SearchBar; diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index 14619e8364..ceef28e897 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -7,15 +7,7 @@ import ClearIcon from "@mui/icons-material/Clear"; import SearchIcon from "@mui/icons-material/Search"; -import { - IconButton, - List, - ListItem, - ListItemText, - PopoverPosition, - Skeleton, - TextField, -} from "@mui/material"; +import { IconButton, List, ListItem, ListItemText, PopoverPosition, Skeleton } from "@mui/material"; import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLatest } from "react-use"; @@ -34,6 +26,7 @@ import { useMessagePipeline, } from "@lichtblick/suite-base/components/MessagePipeline"; import { DraggedMessagePath } from "@lichtblick/suite-base/components/PanelExtensionAdapter"; +import SearchBar from "@lichtblick/suite-base/components/SearchBar"; import { ContextMenu } from "@lichtblick/suite-base/components/TopicList/ContextMenu"; import { PlayerPresence } from "@lichtblick/suite-base/players/types"; import { MessagePathSelectionProvider } from "@lichtblick/suite-base/services/messagePathDragging/MessagePathSelectionProvider"; @@ -208,7 +201,7 @@ export function TopicList(): React.JSX.Element { return ( <>
-
- { setFilterText(event.target.value); }} value={undebouncedFilterText} - fullWidth - placeholder={t("searchBarPlaceholder")} InputProps={{ inputProps: { "data-testid": "topic-filter" }, size: "small", From 7c9dc2b72195da2d7f48656bd2a16d908addecc8 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Thu, 12 Dec 2024 17:42:01 +0000 Subject: [PATCH 08/27] update on SearchBar component and TopicList use of it --- .../suite-base/src/components/SearchBar.tsx | 63 +++++++++++-------- .../src/components/TopicList/TopicList.tsx | 52 +++++---------- 2 files changed, 52 insertions(+), 63 deletions(-) diff --git a/packages/suite-base/src/components/SearchBar.tsx b/packages/suite-base/src/components/SearchBar.tsx index 1a5563c800..d42801a395 100644 --- a/packages/suite-base/src/components/SearchBar.tsx +++ b/packages/suite-base/src/components/SearchBar.tsx @@ -8,10 +8,17 @@ import { TextFieldProps } from "@mui/material/TextField"; import { PropsWithChildren } from "react"; import { makeStyles } from "tss-react/mui"; -const useStyles = makeStyles()(() => ({ +const useStyles = makeStyles()((theme) => ({ filterStartAdornment: { display: "flex", }, + filterBar: { + top: 0, + zIndex: theme.zIndex.appBar, + padding: theme.spacing(0.5), + position: "sticky", + backgroundColor: theme.palette.background.paper, + }, })); function SearchBar( @@ -39,31 +46,35 @@ function SearchBar( const { classes } = useStyles(); return ( - - {startAdornment} - - ), - endAdornment: showClearIcon && ( - - - - - - ), - }} - {...rest} - /> + <> +
+ + {startAdornment} + + ), + endAdornment: showClearIcon && ( + + + + + + ), + }} + {...rest} + /> +
+ ); } diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index ceef28e897..4a97abcc8d 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -5,9 +5,8 @@ // License, v2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/ -import ClearIcon from "@mui/icons-material/Clear"; import SearchIcon from "@mui/icons-material/Search"; -import { IconButton, List, ListItem, ListItemText, PopoverPosition, Skeleton } from "@mui/material"; +import { List, ListItem, ListItemText, PopoverPosition, Skeleton } from "@mui/material"; import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLatest } from "react-use"; @@ -54,9 +53,6 @@ const useStyles = makeStyles()((theme) => ({ position: "sticky", backgroundColor: theme.palette.background.paper, }, - filterStartAdornment: { - display: "flex", - }, skeletonText: { marginTop: theme.spacing(0.5), marginBottom: theme.spacing(0.5), @@ -89,6 +85,9 @@ export function TopicList(): React.JSX.Element { const { classes } = useStyles(); const [undebouncedFilterText, setFilterText] = useState(""); const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); + const onClear = () => { + setFilterText(""); + }; const playerPresence = useMessagePipeline(selectPlayerPresence); const { topics, datatypes } = useDataSourceInfo(); @@ -231,38 +230,17 @@ export function TopicList(): React.JSX.Element { return (
-
- { - setFilterText(event.target.value); - }} - value={undebouncedFilterText} - InputProps={{ - inputProps: { "data-testid": "topic-filter" }, - size: "small", - startAdornment: ( - - ), - endAdornment: undebouncedFilterText && ( - { - setFilterText(""); - }} - edge="end" - > - - - ), - }} - /> -
+ { + setFilterText(event.target.value); + }} + value={undebouncedFilterText} + showClearIcon={!!debouncedFilterText} + onClear={onClear} + /> {treeItems.length > 0 ? (
From d8651ab03bdf2c3a04623d52cd05fb31010d70e4 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 13 Dec 2024 11:59:06 +0000 Subject: [PATCH 09/27] added search bar to extensions menu --- .../components/ExtensionsSettings/index.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index 91d69f0091..996a9cb98b 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -19,10 +19,12 @@ import * as _ from "lodash-es"; import { useEffect, useMemo, useState } from "react"; import { useAsyncFn } from "react-use"; import { makeStyles } from "tss-react/mui"; +import { useDebounce } from "use-debounce"; import Log from "@lichtblick/log"; import { Immutable } from "@lichtblick/suite"; import { ExtensionDetails } from "@lichtblick/suite-base/components/ExtensionDetails"; +import SearchBar from "@lichtblick/suite-base/components/SearchBar"; import Stack from "@lichtblick/suite-base/components/Stack"; import { useExtensionCatalog } from "@lichtblick/suite-base/context/ExtensionCatalogContext"; import { @@ -88,6 +90,11 @@ function ExtensionListEntry(props: { } export default function ExtensionsSettings(): React.ReactElement { + const [undebouncedFilterText, setFilterText] = useState(""); + const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); + const onClear = () => { + setFilterText(""); + }; const [focusedExtension, setFocusedExtension] = useState< | { installed: boolean; @@ -183,6 +190,19 @@ export default function ExtensionsSettings(): React.ReactElement { Check your internet connection and try again. )} +
+ { + setFilterText(event.target.value); + }} + value={undebouncedFilterText} + showClearIcon={!!debouncedFilterText} + onClear={onClear} + /> +
{!_.isEmpty(namespacedEntries) ? ( Object.entries(namespacedEntries).map(([namespace, entries]) => ( From f71fd3ec88625bcaa3848869f5cb44b757318b0e Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 13 Dec 2024 16:41:50 +0000 Subject: [PATCH 10/27] added searching logic to search bar on extensions --- .../components/ExtensionsSettings/index.tsx | 87 +++++++++++-------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index 996a9cb98b..b46830045a 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -90,10 +90,10 @@ function ExtensionListEntry(props: { } export default function ExtensionsSettings(): React.ReactElement { - const [undebouncedFilterText, setFilterText] = useState(""); + const [undebouncedFilterText, setUndebouncedFilterText] = useState(""); const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); const onClear = () => { - setFilterText(""); + setUndebouncedFilterText(""); }; const [focusedExtension, setFocusedExtension] = useState< | { @@ -115,9 +115,10 @@ export default function ExtensionsSettings(): React.ReactElement { [marketplaceEntries], ); - const installedEntries = useMemo( - () => - (installed ?? []).map((entry) => { + const installedEntries = useMemo(() => { + const searchLower = debouncedFilterText.toLowerCase(); + return (installed ?? []) + .map((entry) => { const marketplaceEntry = marketplaceMap[entry.id]; if (marketplaceEntry != undefined) { return { ...marketplaceEntry, namespace: entry.namespace }; @@ -137,9 +138,13 @@ export default function ExtensionsSettings(): React.ReactElement { namespace: entry.namespace, qualifiedName: entry.qualifiedName, }; - }), - [installed, marketplaceMap], - ); + }) + .filter( + (entry) => + entry.name.toLowerCase().includes(searchLower) || + entry.description.toLowerCase().includes(searchLower), + ); + }, [installed, marketplaceMap, debouncedFilterText]); const namespacedEntries = useMemo( () => _.groupBy(installedEntries, (entry) => entry.namespace), @@ -175,6 +180,43 @@ export default function ExtensionsSettings(): React.ReactElement { ); } + function generatePlaceholderList(message?: string): React.ReactElement { + return ( + + + + + + ); + } + + function listExtensions() { + if (!_.isEmpty(namespacedEntries)) { + return Object.entries(namespacedEntries).map(([namespace, entries]) => ( + + + + {displayNameForNamespace(namespace)} + + + {entries.map((entry) => ( + { + setFocusedExtension({ installed: true, entry }); + }} + /> + ))} + + )); + } else if (_.isEmpty(namespacedEntries) && undebouncedFilterText) { + return generatePlaceholderList("No extensions found"); //translate this!!!! + } else { + return generatePlaceholderList("No extensions installed"); //translate this!!!! + } + } + return ( {marketplaceEntries.error && ( @@ -196,39 +238,14 @@ export default function ExtensionsSettings(): React.ReactElement { placeholder="Search extensions..." variant="outlined" onChange={(event) => { - setFilterText(event.target.value); + setUndebouncedFilterText(event.target.value); }} value={undebouncedFilterText} showClearIcon={!!debouncedFilterText} onClear={onClear} />
- {!_.isEmpty(namespacedEntries) ? ( - Object.entries(namespacedEntries).map(([namespace, entries]) => ( - - - - {displayNameForNamespace(namespace)} - - - {entries.map((entry) => ( - { - setFocusedExtension({ installed: true, entry }); - }} - /> - ))} - - )) - ) : ( - - - - - - )} + {listExtensions()} From c8204371da4c8d2acc82e589c3ed78def274c896 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 13 Dec 2024 16:57:59 +0000 Subject: [PATCH 11/27] separting styles to its own files --- .../ExtensionsSettings/Index.style.ts | 10 +++ .../components/ExtensionsSettings/index.tsx | 9 +-- .../src/components/SearchBar.style.ts | 17 +++++ .../suite-base/src/components/SearchBar.tsx | 70 ++++++++----------- .../components/TopicList/TopicList.style.ts | 26 +++++++ .../src/components/TopicList/TopicList.tsx | 24 +------ 6 files changed, 84 insertions(+), 72 deletions(-) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/Index.style.ts create mode 100644 packages/suite-base/src/components/SearchBar.style.ts create mode 100644 packages/suite-base/src/components/TopicList/TopicList.style.ts diff --git a/packages/suite-base/src/components/ExtensionsSettings/Index.style.ts b/packages/suite-base/src/components/ExtensionsSettings/Index.style.ts new file mode 100644 index 0000000000..030e93efaf --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/Index.style.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { makeStyles } from "tss-react/mui"; + +export const useStyles = makeStyles()((theme) => ({ + listItemButton: { + "&:hover": { color: theme.palette.primary.main }, + }, +})); diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index b46830045a..d97446dd92 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -18,7 +18,6 @@ import { import * as _ from "lodash-es"; import { useEffect, useMemo, useState } from "react"; import { useAsyncFn } from "react-use"; -import { makeStyles } from "tss-react/mui"; import { useDebounce } from "use-debounce"; import Log from "@lichtblick/log"; @@ -32,13 +31,9 @@ import { useExtensionMarketplace, } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; -const log = Log.getLogger(__filename); +import { useStyles } from "./Index.style"; -const useStyles = makeStyles()((theme) => ({ - listItemButton: { - "&:hover": { color: theme.palette.primary.main }, - }, -})); +const log = Log.getLogger(__filename); function displayNameForNamespace(namespace: string): string { switch (namespace) { diff --git a/packages/suite-base/src/components/SearchBar.style.ts b/packages/suite-base/src/components/SearchBar.style.ts new file mode 100644 index 0000000000..41e3eda1a8 --- /dev/null +++ b/packages/suite-base/src/components/SearchBar.style.ts @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { makeStyles } from "tss-react/mui"; + +export const useStyles = makeStyles()((theme) => ({ + filterStartAdornment: { + display: "flex", + }, + filterBar: { + top: 0, + zIndex: theme.zIndex.appBar, + padding: theme.spacing(0.5), + position: "sticky", + backgroundColor: theme.palette.background.paper, + }, +})); diff --git a/packages/suite-base/src/components/SearchBar.tsx b/packages/suite-base/src/components/SearchBar.tsx index d42801a395..7fe4be1b7d 100644 --- a/packages/suite-base/src/components/SearchBar.tsx +++ b/packages/suite-base/src/components/SearchBar.tsx @@ -6,20 +6,8 @@ import SearchIcon from "@mui/icons-material/Search"; import { IconButton, TextField, InputAdornment } from "@mui/material"; import { TextFieldProps } from "@mui/material/TextField"; import { PropsWithChildren } from "react"; -import { makeStyles } from "tss-react/mui"; -const useStyles = makeStyles()((theme) => ({ - filterStartAdornment: { - display: "flex", - }, - filterBar: { - top: 0, - zIndex: theme.zIndex.appBar, - padding: theme.spacing(0.5), - position: "sticky", - backgroundColor: theme.palette.background.paper, - }, -})); +import { useStyles } from "@lichtblick/suite-base/components/SearchBar.style"; function SearchBar( props: PropsWithChildren< @@ -46,35 +34,33 @@ function SearchBar( const { classes } = useStyles(); return ( - <> -
- - {startAdornment} - - ), - endAdornment: showClearIcon && ( - - - - - - ), - }} - {...rest} - /> -
- +
+ + {startAdornment} + + ), + endAdornment: showClearIcon && ( + + + + + + ), + }} + {...rest} + /> +
); } diff --git a/packages/suite-base/src/components/TopicList/TopicList.style.ts b/packages/suite-base/src/components/TopicList/TopicList.style.ts new file mode 100644 index 0000000000..937078b042 --- /dev/null +++ b/packages/suite-base/src/components/TopicList/TopicList.style.ts @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { makeStyles } from "tss-react/mui"; + +export const useStyles = makeStyles()((theme) => ({ + root: { + width: "100%", + height: "100%", + overflow: "hidden", + display: "flex", + flexDirection: "column", + containerType: "inline-size", + }, + filterBar: { + top: 0, + zIndex: theme.zIndex.appBar, + padding: theme.spacing(0.5), + position: "sticky", + backgroundColor: theme.palette.background.paper, + }, + skeletonText: { + marginTop: theme.spacing(0.5), + marginBottom: theme.spacing(0.5), + }, +})); diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index 4a97abcc8d..dfe0514ff2 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -12,7 +12,6 @@ import { useTranslation } from "react-i18next"; import { useLatest } from "react-use"; import AutoSizer from "react-virtualized-auto-sizer"; import { ListChildComponentProps, VariableSizeList } from "react-window"; -import { makeStyles } from "tss-react/mui"; import { useDebounce } from "use-debounce"; import { filterMap } from "@lichtblick/den/collection"; @@ -31,34 +30,13 @@ import { PlayerPresence } from "@lichtblick/suite-base/players/types"; import { MessagePathSelectionProvider } from "@lichtblick/suite-base/services/messagePathDragging/MessagePathSelectionProvider"; import { MessagePathRow } from "./MessagePathRow"; +import { useStyles } from "./TopicList.style"; import { TopicRow } from "./TopicRow"; import { useMultiSelection } from "./useMultiSelection"; import { TopicListItem, useTopicListSearch } from "./useTopicListSearch"; const selectPlayerPresence = ({ playerState }: MessagePipelineContext) => playerState.presence; -const useStyles = makeStyles()((theme) => ({ - root: { - width: "100%", - height: "100%", - overflow: "hidden", - display: "flex", - flexDirection: "column", - containerType: "inline-size", - }, - filterBar: { - top: 0, - zIndex: theme.zIndex.appBar, - padding: theme.spacing(0.5), - position: "sticky", - backgroundColor: theme.palette.background.paper, - }, - skeletonText: { - marginTop: theme.spacing(0.5), - marginBottom: theme.spacing(0.5), - }, -})); - function getDraggedMessagePath(treeItem: TopicListItem): DraggedMessagePath { switch (treeItem.type) { case "topic": From eacec98a648b41d71d37ca3b3b0c4dca76857215 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Tue, 17 Dec 2024 15:39:11 +0000 Subject: [PATCH 12/27] SearchBar code restructuring --- .../ExtensionList/ExtensionList.tsx | 80 ++++++ .../ExtensionListEntry.style.ts} | 0 .../ExtensionListEntry/ExtensionListEntry.tsx | 54 ++++ .../hooks/useExtensionSettings.ts | 100 ++++++++ .../components/ExtensionsSettings/index.tsx | 238 +++--------------- .../components/ExtensionsSettings/types.ts | 42 ++++ 6 files changed, 317 insertions(+), 197 deletions(-) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx rename packages/suite-base/src/components/ExtensionsSettings/{Index.style.ts => components/ExtensionListEntry/ExtensionListEntry.style.ts} (100%) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.tsx create mode 100644 packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.ts create mode 100644 packages/suite-base/src/components/ExtensionsSettings/types.ts diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx new file mode 100644 index 0000000000..6f68edaf13 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { List, ListItem, ListItemText, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; + +import { Immutable } from "@lichtblick/suite"; +import { FocusedExtension } from "@lichtblick/suite-base/components/ExtensionsSettings/types"; +import Stack from "@lichtblick/suite-base/components/Stack"; +import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; + +import ExtensionListEntry from "../ExtensionListEntry/ExtensionListEntry"; + +function displayNameForNamespace(namespace: string): string { + switch (namespace) { + case "org": + return "Organization"; + default: + return namespace; + } +} + +function generatePlaceholderList(message?: string): React.ReactElement { + return ( + + + + + + ); +} + +type ExtensionListProps = { + namespace: string; + entries: Immutable[]; + filterText: string; + selectExtension: (newFocusedExtension: FocusedExtension) => void; +}; + +export default function ExtensionList({ + namespace, + entries, + filterText, + selectExtension, +}: ExtensionListProps): React.JSX.Element { + const { t } = useTranslation("extensionsSettings"); + + const renderComponent = () => { + if (entries.length === 0 && filterText) { + return generatePlaceholderList(t("noExtensionsFound")); + } else if (entries.length === 0) { + return generatePlaceholderList(t("noExtensionsAvailable")); + } + return ( + <> + {entries.map((entry) => ( + { + selectExtension({ installed: true, entry }); + }} + searchText={filterText} + /> + ))} + + ); + }; + + return ( + + + + {displayNameForNamespace(namespace)} + + + {renderComponent()} + + ); +} diff --git a/packages/suite-base/src/components/ExtensionsSettings/Index.style.ts b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.style.ts similarity index 100% rename from packages/suite-base/src/components/ExtensionsSettings/Index.style.ts rename to packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.style.ts diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.tsx new file mode 100644 index 0000000000..ea40a74393 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.tsx @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { ListItem, ListItemButton, ListItemText, Typography } from "@mui/material"; + +import { Immutable } from "@lichtblick/suite"; +import Stack from "@lichtblick/suite-base/components/Stack"; +import TextHighlight from "@lichtblick/suite-base/components/TextHighlight"; +import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; + +import { useStyles } from "./ExtensionListEntry.style"; + +type Props = { + entry: Immutable; + onClick: () => void; + searchText: string; +}; + +export default function ExtensionListEntry({ + entry: { id, description, name, publisher, version }, + searchText, + onClick, +}: Props): React.JSX.Element { + const { classes } = useStyles(); + return ( + + + + + + + + {version} + +
+ } + secondary={ + + + {description} + + + {publisher} + + + } + /> + + + ); +} diff --git a/packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.ts b/packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.ts new file mode 100644 index 0000000000..d9b41ce781 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.ts @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import * as _ from "lodash-es"; +import { useEffect, useMemo, useState } from "react"; +import { useAsyncFn } from "react-use"; +import { useDebounce } from "use-debounce"; + +import Log from "@lichtblick/log"; +import { UseExtensionSettingsHook } from "@lichtblick/suite-base/components/ExtensionsSettings/types"; +import { useExtensionCatalog } from "@lichtblick/suite-base/context/ExtensionCatalogContext"; +import { useExtensionMarketplace } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; + +const log = Log.getLogger(__filename); + +const useExtensionSettings = (): UseExtensionSettingsHook => { + const [undebouncedFilterText, setUndebouncedFilterText] = useState(""); + const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); + + const installed = useExtensionCatalog((state) => state.installedExtensions); + const marketplace = useExtensionMarketplace(); + + const [marketplaceEntries, refreshMarketplaceEntries] = useAsyncFn( + async () => await marketplace.getAvailableExtensions(), + [marketplace], + ); + + const marketplaceMap = useMemo( + () => _.keyBy(marketplaceEntries.value ?? [], (entry) => entry.id), + [marketplaceEntries], + ); + + const groupedMarketplaceEntries = useMemo(() => { + const entries = marketplaceEntries.value ?? []; + return _.groupBy(entries, (entry) => entry.namespace ?? "default"); + }, [marketplaceEntries]); + + const groupedMarketplaceData = useMemo(() => { + return Object.entries(groupedMarketplaceEntries).map(([namespace, entries]) => ({ + namespace, + entries: entries.filter((entry) => + entry.name.toLowerCase().includes(debouncedFilterText.toLowerCase()), + ), + })); + }, [groupedMarketplaceEntries, debouncedFilterText]); + + const installedEntries = useMemo(() => { + return (installed ?? []).map((entry) => { + const marketplaceEntry = marketplaceMap[entry.id]; + if (marketplaceEntry != undefined) { + return { ...marketplaceEntry, namespace: entry.namespace }; + } + + return { + id: entry.id, + installed: true, + name: entry.displayName, + displayName: entry.displayName, + description: entry.description, + publisher: entry.publisher, + homepage: entry.homepage, + license: entry.license, + version: entry.version, + keywords: entry.keywords, + namespace: entry.namespace, + qualifiedName: entry.qualifiedName, + }; + }); + }, [installed, marketplaceMap]); + + const namespacedEntries = useMemo( + () => _.groupBy(installedEntries, (entry) => entry.namespace), + [installedEntries], + ); + + useEffect(() => { + refreshMarketplaceEntries().catch((error: unknown) => { + log.error(error); + }); + }, [refreshMarketplaceEntries]); + + const namespacedData = Object.entries(namespacedEntries).map(([namespace, entries]) => ({ + namespace, + entries: entries.filter((entry) => + entry.name.toLowerCase().includes(debouncedFilterText.toLowerCase()), + ), + })); + + return { + setUndebouncedFilterText, + marketplaceEntries, + refreshMarketplaceEntries, + undebouncedFilterText, + namespacedData, + groupedMarketplaceData, + debouncedFilterText, + }; +}; + +export default useExtensionSettings; diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index 6eaa58bcc1..b24e802516 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -5,166 +5,44 @@ // License, v2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/ -import { - Alert, - AlertTitle, - Button, - List, - ListItem, - ListItemButton, - ListItemText, - Typography, -} from "@mui/material"; -import * as _ from "lodash-es"; -import { useEffect, useMemo, useState } from "react"; +import { Alert, AlertTitle, Button } from "@mui/material"; +import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAsyncFn } from "react-use"; -import { useDebounce } from "use-debounce"; -import Log from "@lichtblick/log"; -import { Immutable } from "@lichtblick/suite"; import { ExtensionDetails } from "@lichtblick/suite-base/components/ExtensionDetails"; +import useExtensionSettings from "@lichtblick/suite-base/components/ExtensionsSettings/hooks/useExtensionSettings"; +import { FocusedExtension } from "@lichtblick/suite-base/components/ExtensionsSettings/types"; import SearchBar from "@lichtblick/suite-base/components/SearchBar"; import Stack from "@lichtblick/suite-base/components/Stack"; -import { useExtensionCatalog } from "@lichtblick/suite-base/context/ExtensionCatalogContext"; -import { - ExtensionMarketplaceDetail, - useExtensionMarketplace, -} from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; -import { useStyles } from "./Index.style"; +import ExtensionList from "./components/ExtensionList/ExtensionList"; -const log = Log.getLogger(__filename); +export default function ExtensionsSettings(): React.ReactElement { + const { t } = useTranslation("extensionsSettings"); -function displayNameForNamespace(namespace: string): string { - switch (namespace) { - case "org": - return "Organization"; - default: - return namespace; - } -} + const [focusedExtension, setFocusedExtension] = useState(); -function ExtensionListEntry(props: { - entry: Immutable; - onClick: () => void; -}): React.JSX.Element { const { - entry: { id, description, name, publisher, version }, - onClick, - } = props; - const { classes } = useStyles(); - return ( - - - - - {name} - - - {version} - - - } - secondary={ - - - {description} - - - {publisher} - - - } - /> - - - ); -} + setUndebouncedFilterText, + marketplaceEntries, + refreshMarketplaceEntries, + undebouncedFilterText, + namespacedData, + groupedMarketplaceData, + debouncedFilterText, + } = useExtensionSettings(); -export default function ExtensionsSettings(): React.ReactElement { - const { t } = useTranslation("extensionsSettings"); - const [undebouncedFilterText, setUndebouncedFilterText] = useState(""); - const [debouncedFilterText] = useDebounce(undebouncedFilterText, 50); const onClear = () => { setUndebouncedFilterText(""); }; - const [focusedExtension, setFocusedExtension] = useState< - | { - installed: boolean; - entry: Immutable; - } - | undefined - >(undefined); - const installed = useExtensionCatalog((state) => state.installedExtensions); - const marketplace = useExtensionMarketplace(); - - const [marketplaceEntries, refreshMarketplaceEntries] = useAsyncFn( - async () => await marketplace.getAvailableExtensions(), - [marketplace], - ); - - const marketplaceMap = useMemo( - () => _.keyBy(marketplaceEntries.value ?? [], (entry) => entry.id), - [marketplaceEntries], - ); - - const installedEntries = useMemo(() => { - const searchLower = debouncedFilterText.toLowerCase(); - return (installed ?? []) - .map((entry) => { - const marketplaceEntry = marketplaceMap[entry.id]; - if (marketplaceEntry != undefined) { - return { ...marketplaceEntry, namespace: entry.namespace }; - } - - return { - id: entry.id, - installed: true, - name: entry.displayName, - displayName: entry.displayName, - description: entry.description, - publisher: entry.publisher, - homepage: entry.homepage, - license: entry.license, - version: entry.version, - keywords: entry.keywords, - namespace: entry.namespace, - qualifiedName: entry.qualifiedName, - }; - }) - .filter( - (entry) => - entry.name.toLowerCase().includes(searchLower) || - entry.description.toLowerCase().includes(searchLower), - ); - }, [installed, marketplaceMap, debouncedFilterText]); - - const namespacedEntries = useMemo( - () => _.groupBy(installedEntries, (entry) => entry.namespace), - [installedEntries], - ); - // Hide installed extensions from the list of available extensions - const filteredMarketplaceEntries = useMemo( - () => - _.differenceWith( - marketplaceEntries.value ?? [], - installed ?? [], - (a, b) => a.id === b.id && a.namespace === b.namespace, - ), - [marketplaceEntries, installed], + const selectFocusedExtension = useCallback( + (newFocusedExtension: FocusedExtension) => { + setFocusedExtension(newFocusedExtension); + }, + [setFocusedExtension], ); - useEffect(() => { - refreshMarketplaceEntries().catch((error: unknown) => { - log.error(error); - }); - }, [refreshMarketplaceEntries]); - if (focusedExtension != undefined) { return ( - - - -
- ); - } - - function listExtensions() { - if (!_.isEmpty(namespacedEntries)) { - return Object.entries(namespacedEntries).map(([namespace, entries]) => ( - - - - {displayNameForNamespace(namespace)} - - - {entries.map((entry) => ( - { - setFocusedExtension({ installed: true, entry }); - }} - /> - ))} - - )); - } else if (_.isEmpty(namespacedEntries) && undebouncedFilterText) { - return generatePlaceholderList(t("noExtensionsFound")); - } else { - return generatePlaceholderList(t("noExtensionsAvailable")); - } - } - return ( {marketplaceEntries.error && ( @@ -243,23 +84,26 @@ export default function ExtensionsSettings(): React.ReactElement { onClear={onClear} />
- {listExtensions()} - - - - {t("available")} - - - {filteredMarketplaceEntries.map((entry) => ( - { - setFocusedExtension({ installed: false, entry }); - }} - /> - ))} - + + {namespacedData.map(({ namespace, entries }) => ( + + ))} + + {groupedMarketplaceData.map(({ namespace, entries }) => ( + + ))} ); } diff --git a/packages/suite-base/src/components/ExtensionsSettings/types.ts b/packages/suite-base/src/components/ExtensionsSettings/types.ts new file mode 100644 index 0000000000..e465e3c512 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/types.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { AsyncState } from "react-use/lib/useAsyncFn"; + +import { Immutable } from "@lichtblick/suite"; +import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; + +export type InstalledExtension = { + id: string; + installed: boolean; + name: string; + displayName: string; + description: string; + publisher: string; + homepage?: string; + license?: string; + version: string; + keywords?: string[]; + namespace: string; + qualifiedName: string; +}; + +export type FocusedExtension = { + installed: boolean; + entry: Immutable; +}; + +export type EntryGroupedData = { + namespace: string; + entries: Immutable[]; +}; + +export type UseExtensionSettingsHook = { + setUndebouncedFilterText: (newFilterText: string) => void; + marketplaceEntries: AsyncState; + refreshMarketplaceEntries: () => Promise; + undebouncedFilterText: string; + namespacedData: EntryGroupedData[]; + groupedMarketplaceData: EntryGroupedData[]; + debouncedFilterText: string; +}; From 2295a97132b6d33a0d2188655ccb57e170be260e Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Tue, 17 Dec 2024 17:43:17 +0000 Subject: [PATCH 13/27] ExtensionListEntry unit tests --- .../ExtensionList/ExtensionList.tsx | 9 ++- .../ExtensionListEntry.test.tsx | 67 +++++++++++++++++++ .../ExtensionListEntry.test.tsx.snap | 51 ++++++++++++++ 3 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx create mode 100644 packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx index 6f68edaf13..137e0c00fd 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx @@ -12,11 +12,10 @@ import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/Exten import ExtensionListEntry from "../ExtensionListEntry/ExtensionListEntry"; function displayNameForNamespace(namespace: string): string { - switch (namespace) { - case "org": - return "Organization"; - default: - return namespace; + if (namespace === "org") { + return "Organization"; + } else { + return namespace; } } diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx new file mode 100644 index 0000000000..23b0ac89da --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx @@ -0,0 +1,67 @@ +/** @jest-environment jsdom */ + +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 +import { render, screen, fireEvent } from "@testing-library/react"; + +import { Immutable } from "@lichtblick/suite"; +import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; +import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder"; +import "@testing-library/jest-dom"; + +import ExtensionListEntry from "./ExtensionListEntry"; + +describe("ExtensionListEntry Component", () => { + const mockEntry: Immutable = { + id: BasicBuilder.string(), + name: BasicBuilder.string(), + qualifiedName: BasicBuilder.string(), + description: BasicBuilder.string(), + publisher: BasicBuilder.string(), + homepage: BasicBuilder.string(), + license: BasicBuilder.string(), + version: BasicBuilder.string(), + }; + + const mockOnClick = jest.fn(); + + it("renders primary text with name and highlights search text", () => { + render(); + + const name = screen.getByText(new RegExp(mockEntry.name, "i")); + expect(name).toBeInTheDocument(); + + const highlightedText = screen.getByText(new RegExp(mockEntry.name, "i")); + expect(highlightedText).toBeInTheDocument(); + expect(highlightedText.tagName).toBe("SPAN"); + }); + + it("renders secondary text with description and publisher", () => { + render(); + + + const description = screen.getByText(new RegExp(mockEntry.description, "i")); + expect(description).toBeInTheDocument(); + + + const publisher = screen.getByText(new RegExp(mockEntry.publisher, "i")); + expect(publisher).toBeInTheDocument(); + }); + + it("displays version next to name", () => { + render(); + + // Check for version + const version = screen.getByText(new RegExp(mockEntry.version, "i")); + expect(version).toBeInTheDocument(); + }); + + it("calls onClick when ListItemButton is clicked", () => { + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap new file mode 100644 index 0000000000..f69da702b5 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExtensionListEntry Component matches the snapshot 1`] = ` + +
  • +
    +
    +
    +
    + wtRZDv +
    + + ahQlck + +
    +
    +

    + CFYPtL +

    +

    + sWzoRy +

    +
    +
    + +
    +
  • +
    +`; From 72a27bb7fd845f8568a46e83049a6d21b0a6fce8 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Wed, 18 Dec 2024 10:26:13 +0000 Subject: [PATCH 14/27] ExtensionListEntry lint and test fix --- .../ExtensionListEntry.test.tsx | 14 +++-- .../ExtensionListEntry.test.tsx.snap | 51 ------------------- 2 files changed, 9 insertions(+), 56 deletions(-) delete mode 100644 packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx index 23b0ac89da..807c4286e0 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/ExtensionListEntry.test.tsx @@ -26,7 +26,9 @@ describe("ExtensionListEntry Component", () => { const mockOnClick = jest.fn(); it("renders primary text with name and highlights search text", () => { - render(); + render( + , + ); const name = screen.getByText(new RegExp(mockEntry.name, "i")); expect(name).toBeInTheDocument(); @@ -37,19 +39,21 @@ describe("ExtensionListEntry Component", () => { }); it("renders secondary text with description and publisher", () => { - render(); - + render( + , + ); const description = screen.getByText(new RegExp(mockEntry.description, "i")); expect(description).toBeInTheDocument(); - const publisher = screen.getByText(new RegExp(mockEntry.publisher, "i")); expect(publisher).toBeInTheDocument(); }); it("displays version next to name", () => { - render(); + render( + , + ); // Check for version const version = screen.getByText(new RegExp(mockEntry.version, "i")); diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap deleted file mode 100644 index f69da702b5..0000000000 --- a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionListEntry/__snapshots__/ExtensionListEntry.test.tsx.snap +++ /dev/null @@ -1,51 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ExtensionListEntry Component matches the snapshot 1`] = ` - -
  • -
    -
    -
    -
    - wtRZDv -
    - - ahQlck - -
    -
    -

    - CFYPtL -

    -

    - sWzoRy -

    -
    -
    - -
    -
  • -
    -`; From 3b32af163c3102d4fa3289e563fa35348a9b118d Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Wed, 18 Dec 2024 10:57:02 +0000 Subject: [PATCH 15/27] code structure fix --- .../components/ExtensionsSettings/index.style.ts | 15 +++++++++++++++ .../src/components/ExtensionsSettings/index.tsx | 10 +++++----- .../components/{ => SearchBar}/SearchBar.style.ts | 0 .../src/components/{ => SearchBar}/SearchBar.tsx | 2 +- .../src/components/TopicList/TopicList.tsx | 2 +- 5 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/index.style.ts rename packages/suite-base/src/components/{ => SearchBar}/SearchBar.style.ts (100%) rename packages/suite-base/src/components/{ => SearchBar}/SearchBar.tsx (98%) diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.style.ts b/packages/suite-base/src/components/ExtensionsSettings/index.style.ts new file mode 100644 index 0000000000..48c790e2f2 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/index.style.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { makeStyles } from "tss-react/mui"; + +export const useStyles = makeStyles()(() => ({ + searchBarDiv: { + position: "sticky", + top: 0, + zIndex: 1, + }, + searchBarPadding: { + paddingBottom: 13, + }, +})); diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index b24e802516..81fb5aa66e 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -12,13 +12,15 @@ import { useTranslation } from "react-i18next"; import { ExtensionDetails } from "@lichtblick/suite-base/components/ExtensionDetails"; import useExtensionSettings from "@lichtblick/suite-base/components/ExtensionsSettings/hooks/useExtensionSettings"; import { FocusedExtension } from "@lichtblick/suite-base/components/ExtensionsSettings/types"; -import SearchBar from "@lichtblick/suite-base/components/SearchBar"; +import SearchBar from "@lichtblick/suite-base/components/SearchBar/SearchBar"; import Stack from "@lichtblick/suite-base/components/Stack"; import ExtensionList from "./components/ExtensionList/ExtensionList"; +import { useStyles } from "./index.style"; export default function ExtensionsSettings(): React.ReactElement { const { t } = useTranslation("extensionsSettings"); + const { classes } = useStyles(); const [focusedExtension, setFocusedExtension] = useState(); @@ -70,9 +72,9 @@ export default function ExtensionsSettings(): React.ReactElement { {t("checkInternetConnection")} )} -
    +
    - {namespacedData.map(({ namespace, entries }) => ( ))} - {groupedMarketplaceData.map(({ namespace, entries }) => ( Date: Wed, 18 Dec 2024 11:55:09 +0000 Subject: [PATCH 16/27] SearchBar unit tests --- .../components/SearchBar/SearchBar.test.tsx | 56 +++++++++++++++++++ .../src/components/SearchBar/SearchBar.tsx | 5 +- 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/suite-base/src/components/SearchBar/SearchBar.test.tsx diff --git a/packages/suite-base/src/components/SearchBar/SearchBar.test.tsx b/packages/suite-base/src/components/SearchBar/SearchBar.test.tsx new file mode 100644 index 0000000000..6969e844c1 --- /dev/null +++ b/packages/suite-base/src/components/SearchBar/SearchBar.test.tsx @@ -0,0 +1,56 @@ +/** @jest-environment jsdom */ + +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { render, screen, fireEvent } from "@testing-library/react"; + +import SearchBar from "@lichtblick/suite-base/components/SearchBar/SearchBar"; +import "@testing-library/jest-dom"; +import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder"; + +describe("SearchBar component", () => { + const mockOnChange = jest.fn(); + const mockOnClear = jest.fn(); + + it("renders with default props", () => { + render(); + + const input = screen.getByTestId("SearchBarComponent"); + expect(input).toBeInTheDocument(); + expect(screen.getByTestId("SearchIcon")).toBeInTheDocument(); + }); + + it("renders with clear icon when showClearIcon is true", () => { + render( + , + ); + + const clearIcon = screen.getByTitle("Clear"); + expect(clearIcon).toBeInTheDocument(); + + fireEvent.click(clearIcon); + expect(mockOnClear).toHaveBeenCalledTimes(1); + }); + it("calls onChange handler when input value changes", () => { + render(); + + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: BasicBuilder.string() } }); + + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); + + it("does not render clear icon when showClearIcon is false", () => { + render( + , + ); + + expect(screen.queryByTitle("Clear")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/suite-base/src/components/SearchBar/SearchBar.tsx b/packages/suite-base/src/components/SearchBar/SearchBar.tsx index 11bfb6ff25..adaefca64c 100644 --- a/packages/suite-base/src/components/SearchBar/SearchBar.tsx +++ b/packages/suite-base/src/components/SearchBar/SearchBar.tsx @@ -26,7 +26,7 @@ function SearchBar( onChange, onClear, showClearIcon = false, - startAdornment = , + startAdornment = , ...rest } = props; @@ -35,6 +35,7 @@ function SearchBar( return (
    - + ), From adab744944d145003693c6bcecb451fe3741a605 Mon Sep 17 00:00:00 2001 From: Lais Portugal Date: Wed, 18 Dec 2024 15:40:19 +0000 Subject: [PATCH 17/27] Adding tests for TopicList.tsx --- .../components/TopicList/TopicList.test.tsx | 123 ++++++++++++++++++ .../src/components/TopicList/TopicList.tsx | 2 +- 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 packages/suite-base/src/components/TopicList/TopicList.test.tsx diff --git a/packages/suite-base/src/components/TopicList/TopicList.test.tsx b/packages/suite-base/src/components/TopicList/TopicList.test.tsx new file mode 100644 index 0000000000..7af050e4ce --- /dev/null +++ b/packages/suite-base/src/components/TopicList/TopicList.test.tsx @@ -0,0 +1,123 @@ +/** + * @jest-environment jsdom + */ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import "@testing-library/jest-dom"; + +import { render} from "@testing-library/react"; + +import { useMessagePipeline } from "@lichtblick/suite-base/components/MessagePipeline"; +import { PlayerPresence } from "@lichtblick/suite-base/players/types"; +import { TopicListItem } from "./useTopicListSearch"; +import { getDraggedMessagePath, TopicList } from "./TopicList"; + +// Mock dependencies +jest.mock("@lichtblick/suite-base/components/MessagePipeline"); +jest.mock("./useTopicListSearch"); +jest.mock("./useMultiSelection", () => ({ + useMultiSelection: jest.fn().mockReturnValue({ selectedIndexes: [] }), +})); + +// Mock for useMessagePipeline +const mockUseMessagePipeline = (playerPresence: PlayerPresence) => { + (useMessagePipeline as jest.Mock).mockReturnValue(playerPresence); +}; +// Helper to render TopicList with default mocks +const setup = (playerPresence: PlayerPresence) => { + mockUseMessagePipeline(playerPresence); + return render(); +}; + +// Helper to render and get text +const renderAndGetText = (playerPresence: PlayerPresence, text: string) => { + const { getByText } = setup(playerPresence); + expect(getByText(text)).toBeInTheDocument(); +}; + +describe("TopicList Component", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders EmptyState when playerPresence is NOT_PRESENT", () => { + renderAndGetText(PlayerPresence.NOT_PRESENT, "No data source selected"); + }); + + it("renders EmptyState when playerPresence is ERROR", () => { + renderAndGetText(PlayerPresence.ERROR, "An error occurred"); + }); + + it("renders loading state when playerPresence is INITIALIZING", () => { + const { getByPlaceholderText, getAllByRole } = setup(PlayerPresence.INITIALIZING); + expect(getByPlaceholderText("Waiting for data…")).toBeInTheDocument(); + expect(getAllByRole("listitem")).toHaveLength(16); + }); +}); + +describe("getDraggedMessagePath", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const testDraggedMessagePath = (treeItem: TopicListItem, expectedPath: object) => { + const result = getDraggedMessagePath(treeItem); + expect(result).toEqual(expectedPath); + }; + + it("should return correct path for topic type", () => { + const treeItem: TopicListItem = { + type: "topic", + item: { + item: { + name: "testTopic", + schemaName: "testSchema", + }, + positions: new Set(), + start: 0, + end: 0, + score: 0, + }, + }; + testDraggedMessagePath(treeItem, { + path: "testTopic", + rootSchemaName: "testSchema", + isTopic: true, + isLeaf: false, + topicName: "testTopic", + }); + }); + + it("should return correct path for schema type", () => { + const treeItem: TopicListItem = { + type: "schema", + item: { + item: { + fullPath: "test/full/path", + topic: { + schemaName: "testSchema", + name: "testTopic", + }, + offset: 0, + suffix: { + isLeaf: true, + pathSuffix: "", + type: "" + }, + }, + positions: new Set(), + start: 0, + end: 0, + score: 0, + }, + }; + testDraggedMessagePath(treeItem, { + path: "test/full/path", + rootSchemaName: "testSchema", + isTopic: false, + isLeaf: true, + topicName: "testTopic", + }); + }); +}); diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index ff4c9a13b1..0ba67328bb 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -37,7 +37,7 @@ import { TopicListItem, useTopicListSearch } from "./useTopicListSearch"; const selectPlayerPresence = ({ playerState }: MessagePipelineContext) => playerState.presence; -function getDraggedMessagePath(treeItem: TopicListItem): DraggedMessagePath { +export function getDraggedMessagePath(treeItem: TopicListItem): DraggedMessagePath { switch (treeItem.type) { case "topic": return { From 55cbf22782c87f9e21909e69e15635886754f1ee Mon Sep 17 00:00:00 2001 From: Lais Portugal Date: Wed, 18 Dec 2024 15:53:40 +0000 Subject: [PATCH 18/27] Fix lint:ci --- .../components/TopicList/TopicList.test.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/suite-base/src/components/TopicList/TopicList.test.tsx b/packages/suite-base/src/components/TopicList/TopicList.test.tsx index 7af050e4ce..66ab3107f0 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.test.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.test.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + /** * @jest-environment jsdom */ @@ -5,13 +8,13 @@ // SPDX-License-Identifier: MPL-2.0 import "@testing-library/jest-dom"; - -import { render} from "@testing-library/react"; +import { render } from "@testing-library/react"; import { useMessagePipeline } from "@lichtblick/suite-base/components/MessagePipeline"; import { PlayerPresence } from "@lichtblick/suite-base/players/types"; -import { TopicListItem } from "./useTopicListSearch"; + import { getDraggedMessagePath, TopicList } from "./TopicList"; +import { TopicListItem } from "./useTopicListSearch"; // Mock dependencies jest.mock("@lichtblick/suite-base/components/MessagePipeline"); @@ -30,23 +33,19 @@ const setup = (playerPresence: PlayerPresence) => { return render(); }; -// Helper to render and get text -const renderAndGetText = (playerPresence: PlayerPresence, text: string) => { - const { getByText } = setup(playerPresence); - expect(getByText(text)).toBeInTheDocument(); -}; - describe("TopicList Component", () => { beforeEach(() => { jest.clearAllMocks(); }); it("renders EmptyState when playerPresence is NOT_PRESENT", () => { - renderAndGetText(PlayerPresence.NOT_PRESENT, "No data source selected"); + const { getByText } = setup(PlayerPresence.NOT_PRESENT); + expect(getByText("No data source selected")).toBeInTheDocument(); }); it("renders EmptyState when playerPresence is ERROR", () => { - renderAndGetText(PlayerPresence.ERROR, "An error occurred"); + const { getByText } = setup(PlayerPresence.ERROR); + expect(getByText("An error occurred")).toBeInTheDocument(); }); it("renders loading state when playerPresence is INITIALIZING", () => { @@ -103,7 +102,7 @@ describe("getDraggedMessagePath", () => { suffix: { isLeaf: true, pathSuffix: "", - type: "" + type: "", }, }, positions: new Set(), From 84ffb77c4cb9af0fdf84a3c3e4bb28e82b2842f1 Mon Sep 17 00:00:00 2001 From: Lais Portugal Date: Wed, 18 Dec 2024 15:55:42 +0000 Subject: [PATCH 19/27] Fix headers --- .../suite-base/src/components/TopicList/TopicList.test.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/suite-base/src/components/TopicList/TopicList.test.tsx b/packages/suite-base/src/components/TopicList/TopicList.test.tsx index 66ab3107f0..f935ebb596 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.test.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.test.tsx @@ -1,9 +1,5 @@ -// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) -// SPDX-License-Identifier: MPL-2.0 +/** @jest-environment jsdom */ -/** - * @jest-environment jsdom - */ // SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) // SPDX-License-Identifier: MPL-2.0 From 7b019eeb5bf1267801d94bb20a9ba02fbfdb61fc Mon Sep 17 00:00:00 2001 From: Lais Portugal Date: Wed, 18 Dec 2024 16:02:27 +0000 Subject: [PATCH 20/27] Edit expect form tests to avoid lint errors --- .../src/components/TopicList/TopicList.test.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/suite-base/src/components/TopicList/TopicList.test.tsx b/packages/suite-base/src/components/TopicList/TopicList.test.tsx index f935ebb596..c7869dad7d 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.test.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.test.tsx @@ -56,11 +56,6 @@ describe("getDraggedMessagePath", () => { jest.clearAllMocks(); }); - const testDraggedMessagePath = (treeItem: TopicListItem, expectedPath: object) => { - const result = getDraggedMessagePath(treeItem); - expect(result).toEqual(expectedPath); - }; - it("should return correct path for topic type", () => { const treeItem: TopicListItem = { type: "topic", @@ -75,7 +70,8 @@ describe("getDraggedMessagePath", () => { score: 0, }, }; - testDraggedMessagePath(treeItem, { + const result = getDraggedMessagePath(treeItem); + expect(result).toEqual({ path: "testTopic", rootSchemaName: "testSchema", isTopic: true, @@ -107,7 +103,8 @@ describe("getDraggedMessagePath", () => { score: 0, }, }; - testDraggedMessagePath(treeItem, { + const result = getDraggedMessagePath(treeItem); + expect(result).toEqual({ path: "test/full/path", rootSchemaName: "testSchema", isTopic: false, From ab21107ed1e710ad318f668baa7c59fbe69c2ac4 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Wed, 18 Dec 2024 16:19:53 +0000 Subject: [PATCH 21/27] Added ExtensionList unit tests --- .../ExtensionList/ExtensioList.test.tsx | 114 ++++++++++++++++++ .../ExtensionList/ExtensionList.tsx | 4 +- 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx new file mode 100644 index 0000000000..e8e6d04c14 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx @@ -0,0 +1,114 @@ +/** @jest-environment jsdom */ + +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { render, screen } from "@testing-library/react"; + +import "@testing-library/jest-dom"; +import { Immutable } from "@lichtblick/suite"; +import ExtensionList from "@lichtblick/suite-base/components/ExtensionsSettings/components/ExtensionList/ExtensionList"; +import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; +import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder"; + +import { displayNameForNamespace, generatePlaceholderList } from "./ExtensionList"; + +describe("ExtensionList utility functions", () => { + describe("displayNameForNamespace", () => { + it("returns 'Organization' for 'org'", () => { + expect(displayNameForNamespace("org")).toBe("Organization"); + }); + + it("returns the namespace itself for other values", () => { + const customNamespace = BasicBuilder.string(); + expect(displayNameForNamespace(customNamespace)).toBe(customNamespace); + }); + }); + + describe("generatePlaceholderList", () => { + it("renders a placeholder list with the given message", () => { + const message = BasicBuilder.string(); + render(generatePlaceholderList(message)); + expect(screen.getByText(message)).toBeInTheDocument(); + }); + + it("renders an empty list item when no message is provided", () => { + render(generatePlaceholderList()); + expect(screen.getByRole("listitem")).toBeInTheDocument(); + }); + }); +}); + +describe("ExtensionList Component", () => { + const mockNamespace = "org"; + const mockEntries = [ + { + id: "1", + name: "Extension", + description: "Description of Extension 1", + publisher: "Publisher 1", + version: "1.0.0", + qualifiedName: "org.extension1", + homepage: BasicBuilder.string(), + license: BasicBuilder.string(), + }, + { + id: "2", + name: "Extension2", + description: "Description of Extension 2", + publisher: "Publisher 2", + version: "1.0.0", + qualifiedName: "org.extension2", + homepage: BasicBuilder.string(), + license: BasicBuilder.string(), + }, + ]; + const mockFilterText = "Extension"; + const mockSelectExtension = jest.fn(); + + const emptyMockEntries: Immutable[] = []; + + it("renders the list of extensions correctly", () => { + render( + , + ); + //Since namespace passed was 'org' displayNameForNamespace() transformed it to 'Organization' + expect(screen.getByText("Organization")).toBeInTheDocument(); + + //finds 2 elements that represent the entries from mockEntries + const elements = screen.getAllByText("Extension"); + expect(elements.length).toEqual(2); + }); + + it("renders 'No extensions found' message when entries are empty and there's filterText", () => { + const randomSearchValue = BasicBuilder.string(); + render( + , + ); + + expect(screen.getByText("No extensions found")).toBeInTheDocument(); + }); + + it("renders 'No extensions available' message when entries are empty", () => { + render( + , + ); + + expect(screen.getByText("No extensions available")).toBeInTheDocument(); + }); +}); diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx index 137e0c00fd..6f064e25a1 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensionList.tsx @@ -11,7 +11,7 @@ import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/Exten import ExtensionListEntry from "../ExtensionListEntry/ExtensionListEntry"; -function displayNameForNamespace(namespace: string): string { +export function displayNameForNamespace(namespace: string): string { if (namespace === "org") { return "Organization"; } else { @@ -19,7 +19,7 @@ function displayNameForNamespace(namespace: string): string { } } -function generatePlaceholderList(message?: string): React.ReactElement { +export function generatePlaceholderList(message?: string): React.ReactElement { return ( From 98eb3155954f8033bd10cfc4d0ec98057aeb573c Mon Sep 17 00:00:00 2001 From: Lais Portugal Date: Wed, 18 Dec 2024 16:51:52 +0000 Subject: [PATCH 22/27] Moving logic and tests to another file --- .../components/TopicList/TopicList.test.tsx | 66 +----------------- .../src/components/TopicList/TopicList.tsx | 25 +------ .../TopicList/getDraggedMessagePath.test.ts | 68 +++++++++++++++++++ .../TopicList/getDraggedMessagePath.ts | 27 ++++++++ 4 files changed, 98 insertions(+), 88 deletions(-) create mode 100644 packages/suite-base/src/components/TopicList/getDraggedMessagePath.test.ts create mode 100644 packages/suite-base/src/components/TopicList/getDraggedMessagePath.ts diff --git a/packages/suite-base/src/components/TopicList/TopicList.test.tsx b/packages/suite-base/src/components/TopicList/TopicList.test.tsx index c7869dad7d..ff16f7d58d 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.test.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.test.tsx @@ -9,8 +9,7 @@ import { render } from "@testing-library/react"; import { useMessagePipeline } from "@lichtblick/suite-base/components/MessagePipeline"; import { PlayerPresence } from "@lichtblick/suite-base/players/types"; -import { getDraggedMessagePath, TopicList } from "./TopicList"; -import { TopicListItem } from "./useTopicListSearch"; +import { TopicList } from "./TopicList"; // Mock dependencies jest.mock("@lichtblick/suite-base/components/MessagePipeline"); @@ -50,66 +49,3 @@ describe("TopicList Component", () => { expect(getAllByRole("listitem")).toHaveLength(16); }); }); - -describe("getDraggedMessagePath", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should return correct path for topic type", () => { - const treeItem: TopicListItem = { - type: "topic", - item: { - item: { - name: "testTopic", - schemaName: "testSchema", - }, - positions: new Set(), - start: 0, - end: 0, - score: 0, - }, - }; - const result = getDraggedMessagePath(treeItem); - expect(result).toEqual({ - path: "testTopic", - rootSchemaName: "testSchema", - isTopic: true, - isLeaf: false, - topicName: "testTopic", - }); - }); - - it("should return correct path for schema type", () => { - const treeItem: TopicListItem = { - type: "schema", - item: { - item: { - fullPath: "test/full/path", - topic: { - schemaName: "testSchema", - name: "testTopic", - }, - offset: 0, - suffix: { - isLeaf: true, - pathSuffix: "", - type: "", - }, - }, - positions: new Set(), - start: 0, - end: 0, - score: 0, - }, - }; - const result = getDraggedMessagePath(treeItem); - expect(result).toEqual({ - path: "test/full/path", - rootSchemaName: "testSchema", - isTopic: false, - isLeaf: true, - topicName: "testTopic", - }); - }); -}); diff --git a/packages/suite-base/src/components/TopicList/TopicList.tsx b/packages/suite-base/src/components/TopicList/TopicList.tsx index 0ba67328bb..bea5264d6e 100644 --- a/packages/suite-base/src/components/TopicList/TopicList.tsx +++ b/packages/suite-base/src/components/TopicList/TopicList.tsx @@ -15,7 +15,6 @@ import { ListChildComponentProps, VariableSizeList } from "react-window"; import { useDebounce } from "use-debounce"; import { filterMap } from "@lichtblick/den/collection"; -import { quoteTopicNameIfNeeded } from "@lichtblick/message-path"; import { useDataSourceInfo } from "@lichtblick/suite-base/PanelAPI"; import { DirectTopicStatsUpdater } from "@lichtblick/suite-base/components/DirectTopicStatsUpdater"; import EmptyState from "@lichtblick/suite-base/components/EmptyState"; @@ -26,6 +25,7 @@ import { import { DraggedMessagePath } from "@lichtblick/suite-base/components/PanelExtensionAdapter"; import SearchBar from "@lichtblick/suite-base/components/SearchBar/SearchBar"; import { ContextMenu } from "@lichtblick/suite-base/components/TopicList/ContextMenu"; +import { getDraggedMessagePath } from "@lichtblick/suite-base/components/TopicList/getDraggedMessagePath"; import { PlayerPresence } from "@lichtblick/suite-base/players/types"; import { MessagePathSelectionProvider } from "@lichtblick/suite-base/services/messagePathDragging/MessagePathSelectionProvider"; @@ -33,31 +33,10 @@ import { MessagePathRow } from "./MessagePathRow"; import { useStyles } from "./TopicList.style"; import { TopicRow } from "./TopicRow"; import { useMultiSelection } from "./useMultiSelection"; -import { TopicListItem, useTopicListSearch } from "./useTopicListSearch"; +import { useTopicListSearch } from "./useTopicListSearch"; const selectPlayerPresence = ({ playerState }: MessagePipelineContext) => playerState.presence; -export function getDraggedMessagePath(treeItem: TopicListItem): DraggedMessagePath { - switch (treeItem.type) { - case "topic": - return { - path: quoteTopicNameIfNeeded(treeItem.item.item.name), - rootSchemaName: treeItem.item.item.schemaName, - isTopic: true, - isLeaf: false, - topicName: treeItem.item.item.name, - }; - case "schema": - return { - path: treeItem.item.item.fullPath, - rootSchemaName: treeItem.item.item.topic.schemaName, - isTopic: false, - isLeaf: treeItem.item.item.suffix.isLeaf, - topicName: treeItem.item.item.topic.name, - }; - } -} - export function TopicList(): React.JSX.Element { const { t } = useTranslation("topicList"); const { classes } = useStyles(); diff --git a/packages/suite-base/src/components/TopicList/getDraggedMessagePath.test.ts b/packages/suite-base/src/components/TopicList/getDraggedMessagePath.test.ts new file mode 100644 index 0000000000..0ccb71252c --- /dev/null +++ b/packages/suite-base/src/components/TopicList/getDraggedMessagePath.test.ts @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { getDraggedMessagePath } from "@lichtblick/suite-base/components/TopicList/getDraggedMessagePath"; +import { TopicListItem } from "@lichtblick/suite-base/components/TopicList/useTopicListSearch"; + +describe("getDraggedMessagePath", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should return correct path for topic type", () => { + const treeItem: TopicListItem = { + type: "topic", + item: { + item: { + name: "testTopic", + schemaName: "testSchema", + }, + positions: new Set(), + start: 0, + end: 0, + score: 0, + }, + }; + const result = getDraggedMessagePath(treeItem); + expect(result).toEqual({ + path: "testTopic", + rootSchemaName: "testSchema", + isTopic: true, + isLeaf: false, + topicName: "testTopic", + }); + }); + + it("should return correct path for schema type", () => { + const treeItem: TopicListItem = { + type: "schema", + item: { + item: { + fullPath: "test/full/path", + topic: { + schemaName: "testSchema", + name: "testTopic", + }, + offset: 0, + suffix: { + isLeaf: true, + pathSuffix: "", + type: "", + }, + }, + positions: new Set(), + start: 0, + end: 0, + score: 0, + }, + }; + const result = getDraggedMessagePath(treeItem); + expect(result).toEqual({ + path: "test/full/path", + rootSchemaName: "testSchema", + isTopic: false, + isLeaf: true, + topicName: "testTopic", + }); + }); +}); diff --git a/packages/suite-base/src/components/TopicList/getDraggedMessagePath.ts b/packages/suite-base/src/components/TopicList/getDraggedMessagePath.ts new file mode 100644 index 0000000000..edc0c2c87c --- /dev/null +++ b/packages/suite-base/src/components/TopicList/getDraggedMessagePath.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { quoteTopicNameIfNeeded } from "@lichtblick/message-path"; +import { DraggedMessagePath } from "@lichtblick/suite-base/components/PanelExtensionAdapter"; +import { TopicListItem } from "@lichtblick/suite-base/components/TopicList/useTopicListSearch"; + +export function getDraggedMessagePath(treeItem: TopicListItem): DraggedMessagePath { + switch (treeItem.type) { + case "topic": + return { + path: quoteTopicNameIfNeeded(treeItem.item.item.name), + rootSchemaName: treeItem.item.item.schemaName, + isTopic: true, + isLeaf: false, + topicName: treeItem.item.item.name, + }; + case "schema": + return { + path: treeItem.item.item.fullPath, + rootSchemaName: treeItem.item.item.topic.schemaName, + isTopic: false, + isLeaf: treeItem.item.item.suffix.isLeaf, + topicName: treeItem.item.item.topic.name, + }; + } +} From ddeee6c5e9cbf6b2d1f93a33f0a70582c597a592 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Thu, 19 Dec 2024 10:59:52 +0000 Subject: [PATCH 23/27] Added ExtensionSettings unit tests --- .../ExtensionList/ExtensioList.test.tsx | 20 +++++ .../ExtensionsSettings/index.test.tsx | 80 +++++++++++++++++++ .../components/ExtensionsSettings/index.tsx | 1 + 3 files changed, 101 insertions(+) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/index.test.tsx diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx index e8e6d04c14..3bd8699093 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx @@ -4,6 +4,7 @@ // SPDX-License-Identifier: MPL-2.0 import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import { Immutable } from "@lichtblick/suite"; @@ -111,4 +112,23 @@ describe("ExtensionList Component", () => { expect(screen.getByText("No extensions available")).toBeInTheDocument(); }); + + it("calls selectExtension with the correct parameters when an entry is clicked", async () => { + render( + + ); + + const firstEntry = screen.getByText("Extension"); + await userEvent.click(firstEntry); + + expect(mockSelectExtension).toHaveBeenCalledWith({ + installed: true, + entry: mockEntries[0], + }); + }); }); diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.test.tsx new file mode 100644 index 0000000000..560b7013a2 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/index.test.tsx @@ -0,0 +1,80 @@ +/** @jest-environment jsdom */ + +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { useTranslation } from "react-i18next"; +import "@testing-library/jest-dom"; + +import useExtensionSettings from "@lichtblick/suite-base/components/ExtensionsSettings/hooks/useExtensionSettings"; + +import ExtensionsSettings from "./index"; + +jest.mock("@lichtblick/suite-base/components/ExtensionsSettings/hooks/useExtensionSettings"); +jest.mock("react-i18next"); + +describe("ExtensionsSettings", () => { + const mockSetUndebouncedFilterText = jest.fn(); + const mockRefreshMarketplaceEntries = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + (useExtensionSettings as jest.Mock).mockReturnValue({ + setUndebouncedFilterText: mockSetUndebouncedFilterText, + marketplaceEntries: { error: undefined }, + refreshMarketplaceEntries: mockRefreshMarketplaceEntries, + undebouncedFilterText: "", + namespacedData: [{ namespace: "org", entries: [] }], + groupedMarketplaceData: [], + debouncedFilterText: "", + }); + + (useTranslation as jest.Mock).mockReturnValue({ + t: (key: string) => key, + }); + }); + + it("renders the search bar and extension lists", () => { + render(); + + expect(screen.getByTestId("SearchBarComponent")).toBeInTheDocument(); + + expect(screen.getByText("Organization")).toBeInTheDocument(); + }); + + it("handles search bar input", async () => { + render(); + + const searchInput = screen.getByPlaceholderText("searchExtensions"); + await userEvent.type(searchInput, "test"); + + expect(mockSetUndebouncedFilterText).toHaveBeenCalledWith("t"); + expect(mockSetUndebouncedFilterText).toHaveBeenCalledWith("e"); + expect(mockSetUndebouncedFilterText).toHaveBeenCalledWith("s"); + expect(mockSetUndebouncedFilterText).toHaveBeenCalledWith("t"); + }); + + it("displays an error alert when marketplaceEntries.error is set", () => { + (useExtensionSettings as jest.Mock).mockReturnValue({ + setUndebouncedFilterText: mockSetUndebouncedFilterText, + marketplaceEntries: { error: true }, + refreshMarketplaceEntries: mockRefreshMarketplaceEntries, + undebouncedFilterText: "", + namespacedData: [], + groupedMarketplaceData: [], + debouncedFilterText: "", + }); + + render(); + + expect(screen.getByText("failedToRetrieveMarketplaceExtensions")).toBeInTheDocument(); + + const retryButton = screen.getByText("Retry"); + fireEvent.click(retryButton); + + expect(mockRefreshMarketplaceEntries).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.tsx index 81fb5aa66e..086899cb3e 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.tsx @@ -74,6 +74,7 @@ export default function ExtensionsSettings(): React.ReactElement { )}
    Date: Thu, 19 Dec 2024 16:00:27 +0000 Subject: [PATCH 24/27] more ExtensionSettings tests added --- .../ExtensionList/ExtensioList.test.tsx | 4 +- .../ExtensionsSettings/index.test.tsx | 84 +++++++++++++++---- .../src/components/SearchBar/SearchBar.tsx | 4 +- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx index 3bd8699093..ccb40a5336 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx @@ -116,11 +116,11 @@ describe("ExtensionList Component", () => { it("calls selectExtension with the correct parameters when an entry is clicked", async () => { render( + />, ); const firstEntry = screen.getByText("Extension"); diff --git a/packages/suite-base/src/components/ExtensionsSettings/index.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/index.test.tsx index 560b7013a2..20e4aa5e90 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/index.test.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/index.test.tsx @@ -7,42 +7,82 @@ import { render, screen, fireEvent } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useTranslation } from "react-i18next"; import "@testing-library/jest-dom"; +import { AsyncState } from "react-use/lib/useAsyncFn"; import useExtensionSettings from "@lichtblick/suite-base/components/ExtensionsSettings/hooks/useExtensionSettings"; +import { UseExtensionSettingsHook } from "@lichtblick/suite-base/components/ExtensionsSettings/types"; +import { ExtensionMarketplaceDetail } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; +import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder"; import ExtensionsSettings from "./index"; jest.mock("@lichtblick/suite-base/components/ExtensionsSettings/hooks/useExtensionSettings"); jest.mock("react-i18next"); +jest.mock("@lichtblick/suite-base/components/ExtensionDetails", () => ({ + ExtensionDetails: ({ extension, onClose }: any) => { + return ( +
    +

    {extension.name}

    + +
    + ); + }, +})); + describe("ExtensionsSettings", () => { const mockSetUndebouncedFilterText = jest.fn(); const mockRefreshMarketplaceEntries = jest.fn(); - beforeEach(() => { - jest.clearAllMocks(); - + function setUpHook(props?: Partial) { (useExtensionSettings as jest.Mock).mockReturnValue({ setUndebouncedFilterText: mockSetUndebouncedFilterText, marketplaceEntries: { error: undefined }, refreshMarketplaceEntries: mockRefreshMarketplaceEntries, undebouncedFilterText: "", - namespacedData: [{ namespace: "org", entries: [] }], - groupedMarketplaceData: [], + namespacedData: [ + { + namespace: "org", + entries: [ + { + id: "1", + name: "Extension", + description: "Description of Extension 1", + publisher: "Publisher 1", + version: "1.0.0", + qualifiedName: "org.extension1", + homepage: BasicBuilder.string(), + license: BasicBuilder.string(), + }, + ], + }, + { namespace: "Org2", entries: [] }, + ], + groupedMarketplaceData: [{ namespace: "MarketPlace", entries: [] }], debouncedFilterText: "", + ...props, }); + } + + beforeEach(() => { + jest.clearAllMocks(); + setUpHook(); (useTranslation as jest.Mock).mockReturnValue({ t: (key: string) => key, }); }); - it("renders the search bar and extension lists", () => { + it("renders the search bar and three extension lists", () => { render(); expect(screen.getByTestId("SearchBarComponent")).toBeInTheDocument(); expect(screen.getByText("Organization")).toBeInTheDocument(); + expect(screen.getByText("Org2")).toBeInTheDocument(); + expect(screen.getByText("MarketPlace")).toBeInTheDocument(); }); it("handles search bar input", async () => { @@ -57,15 +97,19 @@ describe("ExtensionsSettings", () => { expect(mockSetUndebouncedFilterText).toHaveBeenCalledWith("t"); }); + it("should clear text when onClose() is called", async () => { + setUpHook({ debouncedFilterText: BasicBuilder.string() }); + render(); + + const clearSearchButton = screen.getByTestId("ClearIcon"); + await userEvent.click(clearSearchButton); + + expect(mockSetUndebouncedFilterText).toHaveBeenCalledWith(""); + }); + it("displays an error alert when marketplaceEntries.error is set", () => { - (useExtensionSettings as jest.Mock).mockReturnValue({ - setUndebouncedFilterText: mockSetUndebouncedFilterText, - marketplaceEntries: { error: true }, - refreshMarketplaceEntries: mockRefreshMarketplaceEntries, - undebouncedFilterText: "", - namespacedData: [], - groupedMarketplaceData: [], - debouncedFilterText: "", + setUpHook({ + marketplaceEntries: { error: true } as unknown as AsyncState, }); render(); @@ -77,4 +121,16 @@ describe("ExtensionsSettings", () => { expect(mockRefreshMarketplaceEntries).toHaveBeenCalledTimes(1); }); + + it("should render ExtensionDetails component if focusedExtension is defined and close it", async () => { + render(); + const listItem = screen.getByText("Extension"); + + await userEvent.click(listItem); + expect(screen.queryByTestId("mock-extension-details")).toBeInTheDocument(); + + const closeExtensionButton = screen.getByTestId("mockCloseExtension"); + await userEvent.click(closeExtensionButton); + expect(screen.queryByTestId("mock-extension-details")).not.toBeInTheDocument(); + }); }); diff --git a/packages/suite-base/src/components/SearchBar/SearchBar.tsx b/packages/suite-base/src/components/SearchBar/SearchBar.tsx index adaefca64c..f88dd65259 100644 --- a/packages/suite-base/src/components/SearchBar/SearchBar.tsx +++ b/packages/suite-base/src/components/SearchBar/SearchBar.tsx @@ -33,7 +33,7 @@ function SearchBar( const { classes } = useStyles(); return ( -
    +
    -
    +
    ); } From a9e9cd1d350617fbcc973637bc60b53a7320bb3b Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Thu, 19 Dec 2024 16:45:16 +0000 Subject: [PATCH 25/27] add tests to useExtensionSettings --- .../hooks/useExtensionSettings.test.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.test.ts diff --git a/packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.test.ts b/packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.test.ts new file mode 100644 index 0000000000..a165d7c587 --- /dev/null +++ b/packages/suite-base/src/components/ExtensionsSettings/hooks/useExtensionSettings.test.ts @@ -0,0 +1,109 @@ +/** @jest-environment jsdom */ + +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { renderHook, act } from "@testing-library/react"; + +import { useExtensionCatalog } from "@lichtblick/suite-base/context/ExtensionCatalogContext"; +import { useExtensionMarketplace } from "@lichtblick/suite-base/context/ExtensionMarketplaceContext"; + +import useExtensionSettings from "./useExtensionSettings"; + +jest.mock("@lichtblick/suite-base/context/ExtensionCatalogContext"); +jest.mock("@lichtblick/suite-base/context/ExtensionMarketplaceContext"); + +describe("useExtensionSettings", () => { + const mockInstalledExtensions = [ + { + id: "1", + displayName: "Extension 1", + description: "Description 1", + publisher: "Publisher 1", + homepage: "http://example.com", + license: "MIT", + version: "1.0.0", + keywords: ["keyword1"], + namespace: "namespace1", + qualifiedName: "qualifiedName1", + }, + ]; + + const mockAvailableExtensions = [ + { + id: "2", + name: "Extension 2", + description: "Description 2", + publisher: "Publisher 2", + homepage: "http://example.com", + license: "MIT", + version: "1.0.0", + keywords: ["keyword2"], + namespace: "namespace2", + }, + ]; + + const setupHook = async () => { + const renderHookReturn = renderHook(() => useExtensionSettings()); + + // Needed to trigger useEffect + await act(async () => { + await renderHookReturn.result.current.refreshMarketplaceEntries(); + }); + + return renderHookReturn; + }; + + beforeEach(() => { + (useExtensionCatalog as jest.Mock).mockReturnValue(mockInstalledExtensions); + + (useExtensionMarketplace as jest.Mock).mockReturnValue({ + getAvailableExtensions: jest.fn().mockResolvedValue(mockAvailableExtensions), + }); + }); + + it("should initialize correctly", async () => { + const { result } = await setupHook(); + + expect(result.current.undebouncedFilterText).toBe(""); + expect(result.current.debouncedFilterText).toBe(""); + }); + + it("should update filter text", async () => { + const { result } = await setupHook(); + + act(() => { + result.current.setUndebouncedFilterText("test"); + }); + + expect(result.current.undebouncedFilterText).toBe("test"); + }); + + it("should group marketplace entries by namespace", async () => { + const { result } = await setupHook(); + + expect(result.current.groupedMarketplaceData).toEqual([ + { + namespace: "namespace2", + entries: [mockAvailableExtensions[0]], + }, + ]); + }); + + it("should group installed entries by namespace", async () => { + const { result } = await setupHook(); + + expect(result.current.namespacedData).toEqual([ + { + namespace: "namespace1", + entries: [ + { + ...mockInstalledExtensions[0], + installed: true, + name: mockInstalledExtensions[0]!.displayName, + }, + ], + }, + ]); + }); +}); From 636ec1c2d1ab6233151066832993b589cd6d7ea9 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 20 Dec 2024 10:18:59 +0000 Subject: [PATCH 26/27] ExtensionList test fix --- .../components/ExtensionList/ExtensioList.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx index ccb40a5336..14df474d54 100644 --- a/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx +++ b/packages/suite-base/src/components/ExtensionsSettings/components/ExtensionList/ExtensioList.test.tsx @@ -4,7 +4,6 @@ // SPDX-License-Identifier: MPL-2.0 import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import { Immutable } from "@lichtblick/suite"; @@ -113,7 +112,7 @@ describe("ExtensionList Component", () => { expect(screen.getByText("No extensions available")).toBeInTheDocument(); }); - it("calls selectExtension with the correct parameters when an entry is clicked", async () => { + it("calls selectExtension with the correct parameters when an entry is clicked", () => { render( { ); const firstEntry = screen.getByText("Extension"); - await userEvent.click(firstEntry); + firstEntry.click(); expect(mockSelectExtension).toHaveBeenCalledWith({ installed: true, From 681be2baf58455805e47b53318b91393c2da6b75 Mon Sep 17 00:00:00 2001 From: ctw-joao-luis Date: Fri, 20 Dec 2024 10:38:35 +0000 Subject: [PATCH 27/27] added testing library missing --- packages/suite-base/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/suite-base/package.json b/packages/suite-base/package.json index 3107878ca7..b2a34e114f 100644 --- a/packages/suite-base/package.json +++ b/packages/suite-base/package.json @@ -66,6 +66,7 @@ "@tanstack/react-table": "8.11.7", "@testing-library/jest-dom": "6.6.2", "@testing-library/react": "16.0.0", + "@testing-library/user-event": "14.5.2", "@types/base16": "^1.0.5", "@types/cytoscape": "^3.19.16", "@types/geojson": "7946.0.11", diff --git a/yarn.lock b/yarn.lock index c0996ade29..cb78959841 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,6 +3182,7 @@ __metadata: "@tanstack/react-table": 8.11.7 "@testing-library/jest-dom": 6.6.2 "@testing-library/react": 16.0.0 + "@testing-library/user-event": 14.5.2 "@types/base16": ^1.0.5 "@types/cytoscape": ^3.19.16 "@types/geojson": 7946.0.11