diff --git a/apps/heureka/src/components/CustomAppShell.jsx b/apps/heureka/src/components/CustomAppShell.jsx
index 490ecf6eb..7ac694b70 100644
--- a/apps/heureka/src/components/CustomAppShell.jsx
+++ b/apps/heureka/src/components/CustomAppShell.jsx
@@ -5,7 +5,7 @@
import React from "react"
import { AppShell, PageHeader, TopNavigation, TopNavigationItem } from "@cloudoperators/juno-ui-components"
-import { useGlobalsActions, useGlobalsActiveView } from "./StoreProvider"
+import { useGlobalsActions, useGlobalsActiveView, useGlobalsEmbedded } from "./StoreProvider"
import ServicesView from "./services/ServicesView"
import IssueMatchesView from "./issueMatches/IssueMatchesView"
import ComponentsView from "./components/ComponentsView"
@@ -21,6 +21,7 @@ const VIEW_CONFIG = {
const CustomAppShell = ({ children }) => {
const { setActiveView, setShowPanel } = useGlobalsActions()
const activeView = useGlobalsActiveView()
+ const embedded = useGlobalsEmbedded()
const handleNavItemChange = (item) => {
setActiveView(item)
@@ -40,7 +41,7 @@ const CustomAppShell = ({ children }) => {
const ActiveComponent = VIEW_CONFIG[activeView]?.component
return (
-
} topNavigation={topNavigation}>
+
} topNavigation={topNavigation} embedded={embedded}>
{ActiveComponent &&
}
{children}
diff --git a/apps/heureka/src/components/issueMatches/IssueMatchesDetails.jsx b/apps/heureka/src/components/issueMatches/IssueMatchesDetails.jsx
index c45bb4b55..3e3c4d24c 100644
--- a/apps/heureka/src/components/issueMatches/IssueMatchesDetails.jsx
+++ b/apps/heureka/src/components/issueMatches/IssueMatchesDetails.jsx
@@ -16,7 +16,7 @@ const IssueMatchesDetails = () => {
const queryClientFnReady = useGlobalsQueryClientFnReady()
const issueElem = useQuery({
- queryKey: ["IssueMatchesMain", { filter: { id: [showIssueDetail] } }],
+ queryKey: ["IssueMatchesDetails", { filter: { id: [showIssueDetail] } }],
enabled: !!queryClientFnReady && !!showIssueDetail,
})
const issue = useMemo(() => {
@@ -101,7 +101,7 @@ const IssueMatchesDetails = () => {
Component Version
-
+
@@ -128,7 +128,7 @@ const IssueMatchesDetails = () => {
- Issue Variant
+ Issue Type
{ }
diff --git a/apps/heureka/src/components/services/ServicesDetails.jsx b/apps/heureka/src/components/services/ServicesDetails.jsx
index df817df91..9e6a1957b 100644
--- a/apps/heureka/src/components/services/ServicesDetails.jsx
+++ b/apps/heureka/src/components/services/ServicesDetails.jsx
@@ -32,7 +32,7 @@ const ServicesDetail = () => {
const { addMessage, resetMessages } = messageActions()
const serviceElem = useQuery({
- queryKey: ["ServicesMain", { filter: { serviceName: [showServiceDetail] } }],
+ queryKey: ["ServicesDetails", { filter: { serviceName: [showServiceDetail] } }],
enabled: !!queryClientFnReady && !!showServiceDetail,
})
@@ -239,7 +239,7 @@ const ServicesDetail = () => {
{service?.componentInstances?.edges?.map((componentInstance, i) => (
- {componentInstance?.node?.componentVersion?.component?.name}
+ {componentInstance?.node?.ccrn}
{componentInstance?.node?.componentVersion?.version}
diff --git a/apps/heureka/src/components/services/ServicesListItem.jsx b/apps/heureka/src/components/services/ServicesListItem.jsx
index ad3f50478..3b50f6c22 100644
--- a/apps/heureka/src/components/services/ServicesListItem.jsx
+++ b/apps/heureka/src/components/services/ServicesListItem.jsx
@@ -9,12 +9,6 @@ import { listOfCommaSeparatedObjs } from "../shared/Helper"
import constants from "../shared/constants"
import { useGlobalsActions, useGlobalsShowPanel, useGlobalsShowServiceDetail } from "../StoreProvider"
-const countIssueMatches = (service) => {
- return service?.componentInstances?.edges?.reduce((acc, edge) => {
- return acc + (edge?.node?.issueMatches?.edges?.length || 0)
- }, 0)
-}
-
const ServicesListItem = ({ item }) => {
const { setShowServiceDetail, setShowPanel } = useGlobalsActions()
const showServiceDetail = useGlobalsShowServiceDetail()
@@ -24,8 +18,6 @@ const ServicesListItem = ({ item }) => {
return item?.node
}, [item])
- const issueMatchesCount = useMemo(() => countIssueMatches(service), [service])
-
const handleClick = () => {
if (showServiceDetail === service?.name && showPanel === constants.PANEL_SERVICE) {
{
@@ -48,8 +40,8 @@ const ServicesListItem = ({ item }) => {
{service?.name}
{listOfCommaSeparatedObjs(service?.owners, "name")}
{listOfCommaSeparatedObjs(service?.supportGroups, "name")}
- {service?.componentInstances?.edges?.length || 0}
- {issueMatchesCount}
+ {service?.metadata?.componentInstanceCount}
+ {service?.metadata?.issueMatchCount}
)
}
diff --git a/apps/heureka/src/hooks/useQueryClientFn.js b/apps/heureka/src/hooks/useQueryClientFn.js
index 49361d265..2d9c83b4c 100644
--- a/apps/heureka/src/hooks/useQueryClientFn.js
+++ b/apps/heureka/src/hooks/useQueryClientFn.js
@@ -7,9 +7,9 @@ import { useEffect } from "react"
import { useQueryClient } from "@tanstack/react-query"
import { useGlobalsApiEndpoint, useGlobalsActions } from "../components/StoreProvider"
import { request } from "graphql-request"
-import { servicesMainQuery, servicesCountQuery } from "../lib/queries/services"
+import { servicesMainQuery, servicesDetailsQuery, servicesCountQuery } from "../lib/queries/services"
import { componentsMainQuery, componentsCountQuery } from "../lib/queries/components"
-import { issueMatchesMainQuery, issueMatchesCountQuery } from "../lib/queries/issueMatches"
+import { issueMatchesMainQuery, issueMatchesDetailsQuery, issueMatchesCountQuery } from "../lib/queries/issueMatches"
import serviceFilterValuesQuery from "../lib/queries/serviceFilterValues"
import issueMatchesFilterValuesQuery from "../lib/queries/issueMatchesFilterValues"
import addRemoveServiceOwners from "../lib/queries/addRemoveServiceOwners"
@@ -24,7 +24,7 @@ const useQueryClientFn = () => {
As stated in getQueryDefaults, the order of registration of query defaults does matter. Since the first matching defaults are returned by getQueryDefaults, the registration should be made in the following order: from the least generic key to the most generic one. This way, in case of specific key, the first matching one would be the expected one.
*/
useEffect(() => {
- if (!queryClient || !endpoint) return
+ if (!queryClient) return
// Services main query
queryClient.setQueryDefaults(["ServicesMain"], {
@@ -33,6 +33,13 @@ const useQueryClientFn = () => {
return await request(endpoint, servicesMainQuery(), options)
},
})
+ // Services details query
+ queryClient.setQueryDefaults(["ServicesDetails"], {
+ queryFn: async ({ queryKey }) => {
+ const [_key, options] = queryKey
+ return await request(endpoint, servicesDetailsQuery(), options)
+ },
+ })
// Services count query (for totalCount and pageInfo)
queryClient.setQueryDefaults(["ServicesCount"], {
@@ -58,7 +65,7 @@ const useQueryClientFn = () => {
},
})
- // Main IssueMatches query
+ // IssueMatches main query
queryClient.setQueryDefaults(["IssueMatchesMain"], {
queryFn: async ({ queryKey }) => {
const [_key, options] = queryKey
@@ -66,6 +73,14 @@ const useQueryClientFn = () => {
},
})
+ // IssueMatches details query
+ queryClient.setQueryDefaults(["IssueMatchesDetails"], {
+ queryFn: async ({ queryKey }) => {
+ const [_key, options] = queryKey
+ return await request(endpoint, issueMatchesDetailsQuery(), options)
+ },
+ })
+
// IssueMatches count query (for totalCount and pageInfo)
queryClient.setQueryDefaults(["IssueMatchesCount"], {
queryFn: async ({ queryKey }) => {
@@ -135,7 +150,7 @@ const useQueryClientFn = () => {
// Set queryClientFnReady to true once
setQueryClientFnReady(true)
- }, [queryClient, endpoint])
+ }, [queryClient])
}
export default useQueryClientFn
diff --git a/apps/heureka/src/lib/queries/components.js b/apps/heureka/src/lib/queries/components.js
index a3bea8e14..fa51ffd21 100644
--- a/apps/heureka/src/lib/queries/components.js
+++ b/apps/heureka/src/lib/queries/components.js
@@ -24,9 +24,6 @@ export const componentsMainQuery = () => gql`
node {
id
version
- issues {
- totalCount
- }
componentInstances {
totalCount
edges {
diff --git a/apps/heureka/src/lib/queries/issueMatches.js b/apps/heureka/src/lib/queries/issueMatches.js
index 459c03bec..e406f08b6 100644
--- a/apps/heureka/src/lib/queries/issueMatches.js
+++ b/apps/heureka/src/lib/queries/issueMatches.js
@@ -11,6 +11,57 @@ import { gql } from "graphql-request"
// Main query for fetching IssueMatches data excluding totalCount and pageInfo
export const issueMatchesMainQuery = () => gql`
+ query ($filter: IssueMatchFilter, $first: Int, $after: String) {
+ IssueMatches(filter: $filter, first: $first, after: $after) {
+ __typename
+ edges {
+ node {
+ id
+ status
+ targetRemediationDate
+ severity {
+ value
+ score
+ }
+ issue {
+ id
+ primaryName
+ lastModified
+ type
+ }
+ componentInstance {
+ id
+ ccrn
+ count
+ service {
+ name
+ owners {
+ edges {
+ node {
+ id
+ uniqueUserId
+ name
+ }
+ cursor
+ }
+ }
+ supportGroups {
+ edges {
+ node {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ cursor
+ }
+ }
+ }
+`
+// The query for fetching IssueMatches details data
+export const issueMatchesDetailsQuery = () => gql`
query ($filter: IssueMatchFilter, $first: Int, $after: String) {
IssueMatches(filter: $filter, first: $first, after: $after) {
__typename
@@ -34,28 +85,12 @@ export const issueMatchesMainQuery = () => gql`
}
}
}
- evidences {
- totalCount
- edges {
- node {
- id
- description
- }
- cursor
- }
- pageInfo {
- hasNextPage
- nextPageAfter
- }
- }
- issueId
issue {
id
primaryName
lastModified
type
}
- componentInstanceId
componentInstance {
id
ccrn
@@ -69,7 +104,6 @@ export const issueMatchesMainQuery = () => gql`
service {
name
owners {
- totalCount
edges {
node {
id
@@ -78,10 +112,6 @@ export const issueMatchesMainQuery = () => gql`
}
cursor
}
- pageInfo {
- hasNextPage
- nextPageAfter
- }
}
supportGroups {
edges {
@@ -92,29 +122,12 @@ export const issueMatchesMainQuery = () => gql`
}
}
}
- issueMatchChanges {
- totalCount
- edges {
- node {
- id
- action
- issueMatchId
- activityId
- }
- cursor
- }
- pageInfo {
- hasNextPage
- nextPageAfter
- }
- }
}
cursor
}
}
}
`
-
// Separate query for fetching totalCount and pageInfo only
export const issueMatchesCountQuery = () => gql`
query ($filter: IssueMatchFilter, $first: Int, $after: String) {
diff --git a/apps/heureka/src/lib/queries/services.js b/apps/heureka/src/lib/queries/services.js
index 5d9cd7e7c..1996384fd 100644
--- a/apps/heureka/src/lib/queries/services.js
+++ b/apps/heureka/src/lib/queries/services.js
@@ -9,7 +9,7 @@ import { gql } from "graphql-request"
// like prettier formatting and IDE syntax highlighting.
// You can use gql from graphql-tag if you need it for some reason too.
-// Main query for fetching Services data (excluding totalCount and pageInfo)
+// Main query for fetching Services list data (excluding totalCount and pageInfo)
export const servicesMainQuery = () => gql`
query ($filter: ServiceFilter, $first: Int, $after: String) {
Services(filter: $filter, first: $first, after: $after) {
@@ -17,8 +17,11 @@ export const servicesMainQuery = () => gql`
node {
id
name
+ metadata {
+ componentInstanceCount
+ issueMatchCount
+ }
owners {
- totalCount
edges {
node {
id
@@ -27,13 +30,8 @@ export const servicesMainQuery = () => gql`
}
cursor
}
- pageInfo {
- hasNextPage
- nextPageAfter
- }
}
supportGroups {
- totalCount
edges {
node {
id
@@ -41,22 +39,42 @@ export const servicesMainQuery = () => gql`
}
cursor
}
- pageInfo {
- hasNextPage
- nextPageAfter
- }
}
- activities {
- totalCount
+ }
+ cursor
+ }
+ }
+ }
+`
+// The query for fetching Services details data
+export const servicesDetailsQuery = () => gql`
+ query ($filter: ServiceFilter, $first: Int, $after: String) {
+ Services(filter: $filter, first: $first, after: $after) {
+ edges {
+ node {
+ id
+ name
+ metadata {
+ componentInstanceCount
+ issueMatchCount
+ }
+ owners {
edges {
node {
id
+ uniqueUserId
+ name
}
cursor
}
- pageInfo {
- hasNextPage
- nextPageAfter
+ }
+ supportGroups {
+ edges {
+ node {
+ id
+ name
+ }
+ cursor
}
}
componentInstances {
@@ -71,7 +89,7 @@ export const servicesMainQuery = () => gql`
name
}
}
- issueMatches {
+ issueMatches(first: 10000) {
totalCount
edges {
node {
@@ -80,36 +98,12 @@ export const servicesMainQuery = () => gql`
value
score
}
- issue {
- id
- primaryName
- }
}
}
}
}
}
}
- issueRepositories {
- totalCount
- edges {
- node {
- id
- name
- url
- created_at
- updated_at
- }
- cursor
- priority
- created_at
- updated_at
- }
- pageInfo {
- hasNextPage
- nextPageAfter
- }
- }
}
cursor
}
diff --git a/apps/heureka/src/lib/slices/createGlobalsSlice.js b/apps/heureka/src/lib/slices/createGlobalsSlice.js
index 8aedd6100..7dac0dca5 100644
--- a/apps/heureka/src/lib/slices/createGlobalsSlice.js
+++ b/apps/heureka/src/lib/slices/createGlobalsSlice.js
@@ -8,7 +8,7 @@ import constants from "../../components/shared/constants"
const createGlobalsSlice = (set, get, options) => ({
globals: {
- embedded: false, //Set to true if app is to be embedded in another existing app or page.
+ embedded: options?.embedded === true || options?.embedded === "true", //Set to true if app is to be embedded in another existing app or page.
apiEndpoint: options?.apiEndpoint, //The API endpoint to use for fetching data.
isUrlStateSetup: false, //Set to true when the URL state has been set up.
queryClientFnReady: false, //Set to true when the queryClient function is ready to be used.
diff --git a/packages/ui-components/src/components/Filters/Filters.test.js b/packages/ui-components/src/components/Filters/Filters.test.js
index 0565bc20a..9620854f9 100644
--- a/packages/ui-components/src/components/Filters/Filters.test.js
+++ b/packages/ui-components/src/components/Filters/Filters.test.js
@@ -7,7 +7,7 @@ import * as React from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { Filters } from "./index"
-import { SearchInput } from "../SearchInput/index"
+import { SearchInput } from "../../deprecated_js/SearchInput"
describe("Filters", () => {
test("renders Filters", async () => {
diff --git a/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js b/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js
index 3abdd0a22..fa7da3576 100644
--- a/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js
+++ b/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js
@@ -6,7 +6,7 @@
import PropTypes from "prop-types"
import React, { useContext, useLayoutEffect } from "react"
import * as themes from "./themes"
-import { SearchInput } from "../SearchInput/SearchInput.component"
+import { SearchInput } from "../../deprecated_js/SearchInput"
// DEFAULT THEME (DARK)
const DEFAULT_THEME = {
diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.component.tsx b/packages/ui-components/src/components/SearchInput/SearchInput.component.tsx
new file mode 100644
index 000000000..9794c38ec
--- /dev/null
+++ b/packages/ui-components/src/components/SearchInput/SearchInput.component.tsx
@@ -0,0 +1,225 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useEffect, ChangeEvent, KeyboardEvent, MouseEvent, useCallback } from "react"
+
+import { Icon } from "../Icon"
+import { Stack } from "../Stack"
+
+import "./searchinput.scss"
+
+export interface SearchInputProps {
+ /**
+ * Specifies the name attribute for the input element.
+ */
+ name?: string
+ /**
+ * Determines the visual styling variant of the SearchInput component.
+ * - "default": Standard search input styling.
+ * - "hero": A larger search input intended for standalone use on a dedicated search page, akin to the initial Google search page.
+ * - "rounded": A search input with rounded edges.
+ */
+ variant?: "rounded" | "hero" | "default"
+ /**
+ * Disables the search input when set to true.
+ */
+ disabled?: boolean
+ /**
+ * Custom placeholder text displayed in the search input.
+ */
+ placeholder?: string
+ /**
+ * Initial value for the search input.
+ */
+ value?: string
+ /**
+ * Controls the autocomplete attribute of the input element.
+ * Pass a valid autocomplete value.
+ * We do not enforce validity.
+ */
+ autoComplete?: string
+ /**
+ * Determines whether to show the 'Clear' button.
+ */
+ clear?: boolean
+ /**
+ * Pass an optional CSS class to apply to the search input.
+ */
+ className?: string
+ /**
+ * Callback function invoked when a search is triggered, either by pressing the 'Enter' key or by clicking the search icon.
+ */
+ onSearch?: (_value: string) => void
+ /**
+ * Click handler for the search icon.
+ */
+ onClick?: (_event: MouseEvent
) => void
+ /**
+ * Change handler for the search input.
+ */
+ onChange?: (_event: ChangeEvent) => void
+ /**
+ * KeyPress handler for the search input. By default, triggers the onSearch function when the 'Enter' key is pressed.
+ */
+ onKeyPress?: (_event: KeyboardEvent) => void
+ /**
+ * Click handler for the 'Clear' button.
+ */
+ onClear?: (_event: MouseEvent) => void
+}
+
+const getWrapperStyles = (variant: "rounded" | "hero" | "default"): string => {
+ const baseStyles = "jn-relative jn-inline-block jn-win-max"
+ switch (variant) {
+ case "rounded":
+ return `${baseStyles} jn-w-auto`
+ case "hero":
+ return `${baseStyles} jn-w-full`
+ default:
+ return `${baseStyles} jn-w-auto`
+ }
+}
+
+const getSearchStyles = (variant: "rounded" | "hero" | "default"): string => {
+ const baseStyles = `
+ jn-bg-theme-textinput
+ jn-text-theme-high
+ jn-shadow
+ jn-w-full
+ focus:jn-outline-none
+ focus:jn-ring-2
+ focus:jn-ring-theme-focus
+ disabled:jn-cursor-not-allowed
+ disabled:jn-opacity-50
+ `
+
+ const roundedStyles = "jn-rounded-full focus:jn-rounded-full"
+ switch (variant) {
+ case "rounded":
+ return `${baseStyles} ${roundedStyles} jn-text-base jn-w-auto jn-pl-3 jn-pr-16 jn-py-1`
+ case "hero":
+ return `${baseStyles} ${roundedStyles} jn-text-lg jn-w-full jn-pl-6 jn-pr-20 jn-py-2.5`
+ default:
+ return `${baseStyles} jn-rounded jn-text-base jn-leading-4 jn-pl-4 jn-pr-16 jn-py-2.5`
+ }
+}
+
+const getIconWrapperStyles = (variant: "rounded" | "hero" | "default"): string => {
+ switch (variant) {
+ case "rounded":
+ return "jn-absolute jn-inline-flex jn-right-3 jn-top-1"
+ case "hero":
+ return "jn-absolute jn-inline-flex jn-right-5"
+ default:
+ return "jn-absolute jn-inline-flex jn-right-3 jn-top-2"
+ }
+}
+
+const getClearIconStyles = (variant: "rounded" | "hero" | "default"): string => {
+ switch (variant) {
+ case "hero":
+ return "jn-mr-2.5"
+ default:
+ return "jn-mr-2"
+ }
+}
+
+const getClearIconSize = (variant: "rounded" | "hero" | "default"): string => {
+ return variant === "hero" ? "24" : "18"
+}
+
+/**
+ * A SearchInput is a controlled input component for searching.
+ * It provides a text field to enter a search query and optional clear and search icons.
+ * Three styling variants are supported: "rounded", "hero", and "default".
+ */
+export const SearchInput: React.FC = ({
+ value = "",
+ name = "search",
+ variant = "default",
+ disabled = false,
+ clear = true,
+ onSearch,
+ onChange,
+ onClick,
+ onKeyPress,
+ onClear,
+ autoComplete = "off",
+ placeholder = "Search…",
+ className = "",
+ ...props
+}) => {
+ const [val, setValue] = useState(value)
+
+ useEffect(() => {
+ setValue(value)
+ }, [value])
+
+ const handleInputChange = useCallback(
+ (event: ChangeEvent): void => {
+ setValue(event.target.value)
+ onChange?.(event)
+ },
+ [onChange]
+ )
+
+ const handleKeyPress = useCallback(
+ (event: KeyboardEvent): void => {
+ if (event.key === "Enter" && onSearch) {
+ onSearch(val)
+ }
+ onKeyPress?.(event)
+ },
+ [onSearch, onKeyPress, val]
+ )
+
+ const handleSearchClick = useCallback(
+ (event: MouseEvent): void => {
+ onSearch?.(val)
+ onClick?.(event)
+ },
+ [onSearch, onClick, val]
+ )
+
+ const handleClearClick = useCallback(
+ (event: MouseEvent): void => {
+ setValue("")
+ onClear?.(event)
+ },
+ [onClear]
+ )
+
+ return (
+
+
+
+
+ {clear && val?.length > 0 && (
+
+ )}
+
+
+
+
+ )
+}
diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.stories.js b/packages/ui-components/src/components/SearchInput/SearchInput.stories.tsx
similarity index 91%
rename from packages/ui-components/src/components/SearchInput/SearchInput.stories.js
rename to packages/ui-components/src/components/SearchInput/SearchInput.stories.tsx
index ebeaf199f..ddf1b7dba 100644
--- a/packages/ui-components/src/components/SearchInput/SearchInput.stories.js
+++ b/packages/ui-components/src/components/SearchInput/SearchInput.stories.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { SearchInput } from "./index.js"
+import { SearchInput } from "./"
export default {
title: "Components/SearchInput",
diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.test.tsx b/packages/ui-components/src/components/SearchInput/SearchInput.test.tsx
new file mode 100644
index 000000000..966fe55e0
--- /dev/null
+++ b/packages/ui-components/src/components/SearchInput/SearchInput.test.tsx
@@ -0,0 +1,160 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as React from "react"
+import { render, screen } from "@testing-library/react"
+import userEvent from "@testing-library/user-event"
+import { describe, expect, test, vi } from "vitest"
+import { SearchInput } from "./"
+
+describe("SearchInput Component", () => {
+ describe("Basic Rendering", () => {
+ test("should render a valid HTML input type 'search'", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toBeInTheDocument()
+ expect(screen.getByRole("searchbox")).toHaveAttribute("type", "search")
+ })
+ })
+
+ describe("Placeholder Handling", () => {
+ test("should render a default placeholder 'Search…'", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toHaveAttribute("placeholder", "Search…")
+ })
+
+ test("should render a specific placeholder when provided", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toHaveAttribute("placeholder", "My custom placeholder")
+ })
+ })
+
+ describe("Value Handling", () => {
+ test("should render a specific value when provided", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toHaveValue("blah")
+ })
+
+ test("should render a Clear icon if the field has a value", () => {
+ render( )
+ expect(screen.getByTitle("Clear")).toBeInTheDocument()
+ })
+
+ test("should update the input's value when typing", async () => {
+ render( )
+ const input = screen.getByRole("searchbox")
+ await userEvent.type(input, "abc")
+ expect(input).toHaveValue("abc")
+ })
+ })
+
+ describe("Attributes and ClassNames", () => {
+ test("should render with the default name 'search'", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toHaveAttribute("name", "search")
+ })
+
+ test("should render with a specific name when provided", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toHaveAttribute("name", "searchbox")
+ })
+
+ test("should apply all provided props", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toHaveAttribute("name", "My shiny little Message")
+ })
+
+ test("should apply custom classNames when provided", () => {
+ render( )
+ expect(screen.getByRole("search")).toHaveClass("my-custom-class")
+ })
+ })
+
+ describe("Disabled State", () => {
+ test("should disable the search input when 'disabled' prop is passed", () => {
+ render( )
+ expect(screen.getByRole("searchbox")).toBeDisabled()
+ })
+ })
+
+ describe("Event Handlers", () => {
+ test("should trigger 'onSearch' handler when search button is clicked", async () => {
+ const handleSearch = vi.fn()
+ render( )
+ await userEvent.click(screen.getByRole("button", { name: /search/i }))
+ expect(handleSearch).toHaveBeenCalledTimes(1)
+ })
+
+ test("should trigger 'onClick' handler when search button is clicked", async () => {
+ const handleClick = vi.fn()
+ render( )
+ await userEvent.click(screen.getByRole("button", { name: /search/i }))
+ expect(handleClick).toHaveBeenCalledTimes(1)
+ })
+
+ test("should trigger both 'onClick' and 'onSearch' handlers when search button is clicked if both are provided", async () => {
+ const handleClick = vi.fn()
+ const handleSearch = vi.fn()
+ render( )
+ await userEvent.click(screen.getByRole("button", { name: /search/i }))
+ expect(handleClick).toHaveBeenCalledTimes(1)
+ expect(handleSearch).toHaveBeenCalledTimes(1)
+ })
+
+ test("should trigger 'onSearch' handler when Enter key is pressed", async () => {
+ const handleSearch = vi.fn()
+ render( )
+ await userEvent.type(screen.getByRole("searchbox"), "{enter}")
+ expect(handleSearch).toHaveBeenCalledTimes(1)
+ })
+
+ test("should not trigger 'onSearch' handler when keys other than Enter are pressed", async () => {
+ const handleSearch = vi.fn()
+ render( )
+ await userEvent.type(screen.getByRole("searchbox"), "{shift}")
+ expect(handleSearch).toHaveBeenCalledTimes(0)
+ })
+
+ test("should trigger 'onKeyPress' handler when any key is pressed, including Enter", async () => {
+ const handleKeyPress = vi.fn()
+ render( )
+ await userEvent.type(screen.getByRole("searchbox"), "{enter}abc")
+ expect(handleKeyPress).toHaveBeenCalledTimes(4)
+ })
+
+ test("should trigger 'onChange' handler as text is typed", async () => {
+ const handleChange = vi.fn()
+ render( )
+ await userEvent.type(screen.getByRole("searchbox"), "abc")
+ expect(handleChange).toHaveBeenCalledTimes(3)
+ })
+
+ test("should trigger 'onClear' handler when Clear icon is clicked", async () => {
+ const handleClear = vi.fn()
+ render( )
+ await userEvent.click(screen.getByTitle("Clear"))
+ expect(handleClear).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe("Clear Button Logic", () => {
+ test("should render a Clear icon if the field has a value", () => {
+ render( )
+ expect(screen.getByTitle("Clear")).toBeInTheDocument()
+ })
+
+ test("should clear the input when the Clear icon is clicked", async () => {
+ render( )
+ await userEvent.click(screen.getByTitle("Clear"))
+ expect(screen.getByRole("searchbox")).toHaveValue("")
+ })
+
+ test("should trigger 'onClear' handler when the Clear icon is clicked", async () => {
+ const handleClear = vi.fn()
+ render( )
+ await userEvent.click(screen.getByTitle("Clear"))
+ expect(handleClear).toHaveBeenCalledTimes(1)
+ })
+ })
+})
diff --git a/packages/ui-components/src/components/SearchInput/index.js b/packages/ui-components/src/components/SearchInput/index.ts
similarity index 100%
rename from packages/ui-components/src/components/SearchInput/index.js
rename to packages/ui-components/src/components/SearchInput/index.ts
diff --git a/packages/ui-components/src/components/SearchInput/searchinput.scss b/packages/ui-components/src/components/SearchInput/searchinput.scss
index a8bfe21cc..e08f1f05b 100644
--- a/packages/ui-components/src/components/SearchInput/searchinput.scss
+++ b/packages/ui-components/src/components/SearchInput/searchinput.scss
@@ -1,6 +1,8 @@
-// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
-// SPDX-License-Identifier: Apache-2.0
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
.juno-search-input::-webkit-search-cancel-button {
- -webkit-appearance: none;
-}
\ No newline at end of file
+ -webkit-appearance: none;
+}
diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.component.js b/packages/ui-components/src/deprecated_js/SearchInput/SearchInput.component.js
similarity index 100%
rename from packages/ui-components/src/components/SearchInput/SearchInput.component.js
rename to packages/ui-components/src/deprecated_js/SearchInput/SearchInput.component.js
diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.test.js b/packages/ui-components/src/deprecated_js/SearchInput/SearchInput.test.js
similarity index 100%
rename from packages/ui-components/src/components/SearchInput/SearchInput.test.js
rename to packages/ui-components/src/deprecated_js/SearchInput/SearchInput.test.js
diff --git a/packages/ui-components/src/deprecated_js/SearchInput/index.js b/packages/ui-components/src/deprecated_js/SearchInput/index.js
new file mode 100644
index 000000000..3c1d79629
--- /dev/null
+++ b/packages/ui-components/src/deprecated_js/SearchInput/index.js
@@ -0,0 +1,6 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { SearchInput } from "./SearchInput.component"
diff --git a/packages/ui-components/src/deprecated_js/SearchInput/searchinput.scss b/packages/ui-components/src/deprecated_js/SearchInput/searchinput.scss
new file mode 100644
index 000000000..ec67207fd
--- /dev/null
+++ b/packages/ui-components/src/deprecated_js/SearchInput/searchinput.scss
@@ -0,0 +1,6 @@
+// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+// SPDX-License-Identifier: Apache-2.0
+
+.juno-search-input::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+}