Skip to content

Commit 32aa946

Browse files
committed
chore(heureka): load filters asynchrounously
1 parent 1fc1590 commit 32aa946

15 files changed

Lines changed: 214 additions & 148 deletions

File tree

apps/heureka/src/api/fetchServicesFilters.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ApolloQueryResult } from "@apollo/client"
7-
import { GetServiceFiltersDocument, GetServiceFiltersQuery } from "../generated/graphql"
6+
import { GetServiceFiltersDocument } from "../generated/graphql"
87
import { RouteContext } from "../routes/-types"
8+
import { getNormalizedFilters } from "../components/Services/utils"
99

1010
type FetchServicesFiltersParams = Pick<RouteContext, "queryClient" | "apiClient">
1111

12-
export const fetchServicesFilters = ({
13-
queryClient,
14-
apiClient,
15-
}: FetchServicesFiltersParams): Promise<ApolloQueryResult<GetServiceFiltersQuery>> =>
12+
export const fetchServicesFilters = ({ queryClient, apiClient }: FetchServicesFiltersParams) =>
1613
queryClient.ensureQueryData({
1714
queryKey: ["serviceFilters"],
18-
queryFn: () => apiClient.query({ query: GetServiceFiltersDocument }),
15+
queryFn: () => apiClient.query({ query: GetServiceFiltersDocument }).then((res) => getNormalizedFilters(res.data)),
1916
})

apps/heureka/src/api/fetchVulnerabilityFilters.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ApolloQueryResult } from "@apollo/client"
7-
import { GetVulnerabilityFiltersDocument, GetVulnerabilityFiltersQuery } from "../generated/graphql"
6+
import { GetVulnerabilityFiltersDocument } from "../generated/graphql"
87
import { RouteContext } from "../routes/-types"
8+
import { getNormalizedFilters } from "../components/Vulnerabilities/utils"
99

1010
type FetchVulnerabilityFiltersParams = Pick<RouteContext, "queryClient" | "apiClient">
1111

12-
export const fetchVulnerabilityFilters = ({
13-
queryClient,
14-
apiClient,
15-
}: FetchVulnerabilityFiltersParams): Promise<ApolloQueryResult<GetVulnerabilityFiltersQuery>> =>
12+
export const fetchVulnerabilityFilters = ({ queryClient, apiClient }: FetchVulnerabilityFiltersParams) =>
1613
queryClient.ensureQueryData({
1714
queryKey: ["vulnerabilityFilters"],
18-
queryFn: () => apiClient.query({ query: GetVulnerabilityFiltersDocument }),
15+
queryFn: () =>
16+
apiClient.query({ query: GetVulnerabilityFiltersDocument }).then((res) => getNormalizedFilters(res.data)),
1917
})

apps/heureka/src/components/Services/ServicesFilters.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { SELECTED_FILTER_PREFIX } from "../../constants"
1212

1313
export const ServicesFilters = () => {
1414
const navigate = useNavigate()
15-
const { filters, filterSettings } = useLoaderData({ from: "/services/" })
15+
const { filtersPromise, filterSettings } = useLoaderData({ from: "/services/" })
1616

1717
const handleFilterChange = useCallback(
1818
(updatedFilterSettings: FilterSettings) => {
@@ -35,7 +35,7 @@ export const ServicesFilters = () => {
3535

3636
return (
3737
<Filters
38-
filters={filters}
38+
filtersPromise={filtersPromise}
3939
filterSettings={filterSettings}
4040
onFilterChange={handleFilterChange}
4141
searchInputPlaceholder="search term for services name"

apps/heureka/src/components/Vulnerabilities/VulnerabilitiesFilters.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { SELECTED_FILTER_PREFIX } from "../../constants"
1212

1313
export const VulnerabilitiesFilters = () => {
1414
const navigate = useNavigate()
15-
const { filters, filterSettings } = useLoaderData({ from: "/vulnerabilities/" })
15+
const { filtersPromise, filterSettings } = useLoaderData({ from: "/vulnerabilities/" })
1616

1717
const handleFilterChange = useCallback(
1818
(updatedFilterSettings: FilterSettings) => {
@@ -40,7 +40,7 @@ export const VulnerabilitiesFilters = () => {
4040

4141
return (
4242
<Filters
43-
filters={filters}
43+
filtersPromise={filtersPromise}
4444
filterSettings={filterSettings}
4545
onFilterChange={handleFilterChange}
4646
searchInputPlaceholder="search term for vulnerabilities name"

apps/heureka/src/components/Vulnerabilities/utils.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { FilterSettings } from "../common/Filters/types"
4+
import { Filter, FilterSettings } from "../common/Filters/types"
55
import { GetVulnerabilityFiltersQuery, Page, GetVulnerabilitiesQuery } from "../../generated/graphql"
66
import { SELECTED_FILTER_PREFIX } from "../../constants"
77
import { VulnerabilitiesSearchParams } from "../../routes/vulnerabilities"
88
import { IssuesCountsType } from "../types"
9+
import { isEmpty, omit } from "../../utils"
910

1011
const DEFAULT_COUNT = 0
1112

@@ -37,17 +38,14 @@ export function extractFilterSettingsFromSearchParams(searchParams: Vulnerabilit
3738
}
3839
}
3940

40-
export function getNormalizedFilters(data: GetVulnerabilityFiltersQuery | undefined) {
41-
if (!data?.VulnerabilityFilterValues) return []
42-
const filterValues = data.VulnerabilityFilterValues
43-
return Object.entries(filterValues)
44-
.filter(([, v]) => v && (v as any).values?.length)
45-
.map(([filterName, v]: any) => ({
46-
filterName,
47-
displayName: (v as any).displayName,
48-
values: (v as any).values?.filter((value: any) => value !== null) || [],
49-
}))
50-
}
41+
export const getNormalizedFilters = (data: GetVulnerabilityFiltersQuery | undefined | null): Filter[] =>
42+
isEmpty(data) || isEmpty(data?.VulnerabilityFilterValues)
43+
? []
44+
: Object.values(omit(data!.VulnerabilityFilterValues!, ["__typename"])).map((filter) => ({
45+
displayName: filter?.displayName || "",
46+
filterName: filter?.filterName || "",
47+
values: filter?.values?.filter((value) => value !== null) || [],
48+
}))
5149

5250
export function sanitizeFilterSettings(filters: any, filterSettings: FilterSettings) {
5351
// Only keep filters that are supported by the backend

apps/heureka/src/components/common/Filters/FilterSelect.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import React from "react"
77
import { describe, it, expect, vi } from "vitest"
88
import { render, screen } from "@testing-library/react"
99
import userEvent from "@testing-library/user-event"
10-
import { FilterSelect } from "./FilterSelect"
1110
import { AppShellProvider } from "@cloudoperators/juno-ui-components/index"
11+
import { FilterSelect } from "./FilterSelect"
1212

13-
const mockFilters = [
13+
const mockFiltersPromise = Promise.resolve([
1414
{
1515
displayName: "Category",
1616
filterName: "category",
@@ -26,7 +26,7 @@ const mockFilters = [
2626
filterName: "region",
2727
values: ["America", "Europe", "Asia"],
2828
},
29-
]
29+
])
3030

3131
const mockOnChange = vi.fn()
3232

@@ -38,7 +38,7 @@ describe("FiltersSelect", () => {
3838
it.skip("should render the component with filter select dropdown", async () => {
3939
render(
4040
<AppShellProvider shadowRoot={false}>
41-
<FilterSelect filters={mockFilters} onChange={mockOnChange} />
41+
<FilterSelect filtersPromise={mockFiltersPromise} onChange={mockOnChange} />
4242
</AppShellProvider>
4343
)
4444

@@ -52,7 +52,7 @@ describe("FiltersSelect", () => {
5252
it.skip("displays all filter options in the select dropdown", async () => {
5353
render(
5454
<AppShellProvider shadowRoot={false}>
55-
<FilterSelect filters={mockFilters} onChange={mockOnChange} />
55+
<FilterSelect filtersPromise={mockFiltersPromise} onChange={mockOnChange} />
5656
</AppShellProvider>
5757
)
5858

@@ -65,7 +65,7 @@ describe("FiltersSelect", () => {
6565
const user = userEvent.setup({ delay: 0 })
6666
render(
6767
<AppShellProvider shadowRoot={false}>
68-
<FilterSelect filters={mockFilters} onChange={mockOnChange} />
68+
<FilterSelect filtersPromise={mockFiltersPromise} onChange={mockOnChange} />
6969
</AppShellProvider>
7070
)
7171

apps/heureka/src/components/common/Filters/FilterSelect.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { useCallback, useState } from "react"
6+
import React, { use, useCallback, useState } from "react"
77
import { isEmpty } from "../../../utils"
88
import { InputGroup, ComboBox, ComboBoxOption, SelectOption, Select, Stack } from "@cloudoperators/juno-ui-components"
99
import { Filter, SelectedFilter } from "./types"
1010

1111
type FilterSelectProps = {
12-
filters: Filter[]
12+
filtersPromise: Promise<Filter[]>
1313
onChange: (filter: SelectedFilter) => void
1414
}
1515

16-
export const FilterSelect = ({ filters, onChange }: FilterSelectProps) => {
16+
export const FilterSelect = ({ filtersPromise, onChange }: FilterSelectProps) => {
1717
const [selectedFilterName, setSelectedFilterName] = useState<string>("")
1818
const [selectedFilterValue, setSelectedFilterValue] = useState<string>("")
19-
19+
const filters = use(filtersPromise)
2020
// first filter gets the values, second one filters emtpy values
21-
const filterValues: string[] | undefined = filters
21+
const filterValues = filters
2222
.find((filter) => filter.filterName === selectedFilterName)
2323
?.values?.filter((value) => value)
2424

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from "react"
7+
import { render, screen } from "@testing-library/react"
8+
import { LoadingFilterSelect } from "./LoadingFilterSelect"
9+
import { AppShellProvider } from "@cloudoperators/juno-ui-components/index"
10+
11+
const renderComponent = () => {
12+
return render(
13+
<AppShellProvider shadowRoot={false}>
14+
<LoadingFilterSelect />
15+
</AppShellProvider>
16+
)
17+
}
18+
19+
describe("LoadingFilterSelect", () => {
20+
it("renders the loading filter select component", () => {
21+
renderComponent()
22+
expect(screen.getByRole("combobox")).toBeInTheDocument()
23+
})
24+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from "react"
7+
import { Stack, InputGroup, Select, ComboBox } from "@cloudoperators/juno-ui-components/index"
8+
9+
export const LoadingFilterSelect = () => (
10+
<Stack alignment="center" gap="8">
11+
<InputGroup>
12+
<Select
13+
loading
14+
className="filter-label-select w-64 mb-0"
15+
name="filter"
16+
data-testid="select-filterValue"
17+
label="Filter"
18+
></Select>
19+
<ComboBox
20+
disabled
21+
className="filter-value-select w-64 bg-theme-background-lvl-0"
22+
name="filterValue"
23+
data-testid="combobox-filterValue"
24+
></ComboBox>
25+
</InputGroup>
26+
</Stack>
27+
)

apps/heureka/src/components/common/Filters/index.test.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import React from "react"
77
import { cleanup, render, screen } from "@testing-library/react"
88
import userEvent from "@testing-library/user-event"
9-
import { Filters, FiltersProps } from "./index"
109
import { AppShellProvider } from "@cloudoperators/juno-ui-components/index"
10+
import { Filters, FiltersProps } from "./index"
1111

1212
const filters = [
1313
{
@@ -27,16 +27,18 @@ const filters = [
2727
},
2828
]
2929

30+
const filtersPromise = Promise.resolve(filters)
31+
3032
const filterSettings = {
3133
selectedFilters: [],
3234
searchTerm: "",
3335
}
3436

35-
const renderShell = ({ filters, filterSettings, onFilterChange }: FiltersProps) => ({
37+
const renderShell = ({ filtersPromise, filterSettings, onFilterChange }: FiltersProps) => ({
3638
user: userEvent.setup({ delay: 0 }),
3739
...render(
3840
<AppShellProvider shadowRoot={false}>
39-
<Filters filters={filters} filterSettings={filterSettings} onFilterChange={onFilterChange} />
41+
<Filters filtersPromise={filtersPromise} filterSettings={filterSettings} onFilterChange={onFilterChange} />
4042
</AppShellProvider>
4143
),
4244
})
@@ -48,15 +50,15 @@ describe("Filters", () => {
4850
})
4951

5052
it.skip("renders the component with search, select and combobox", async () => {
51-
renderShell({ filters, filterSettings, onFilterChange: vi.fn() })
53+
renderShell({ filtersPromise, filterSettings, onFilterChange: vi.fn() })
5254
expect(await screen.findByTestId("select-filterValue")).toBeInTheDocument()
5355
expect(await screen.findByTestId("combobox-filterValue")).toBeInTheDocument()
5456
expect(await screen.findByTestId("searchbar")).toBeInTheDocument()
5557
})
5658

5759
it.skip("should allow filtering by text", async () => {
5860
const onFilterChangeSpy = vi.fn()
59-
const { user } = renderShell({ filters, filterSettings, onFilterChange: onFilterChangeSpy })
61+
const { user } = renderShell({ filtersPromise, filterSettings, onFilterChange: onFilterChangeSpy })
6062
const searchbox = await screen.findByRole("searchbox")
6163
await user.type(searchbox, "Europe")
6264
const searchButton = await screen.findByRole("button", { name: "Search" })
@@ -71,7 +73,7 @@ describe("Filters", () => {
7173

7274
it.skip("should select filter and filter value", async () => {
7375
const onFilterChangeSpy = vi.fn()
74-
const { user } = renderShell({ filters, filterSettings, onFilterChange: onFilterChangeSpy })
76+
const { user } = renderShell({ filtersPromise, filterSettings, onFilterChange: onFilterChangeSpy })
7577

7678
const filterSelect = await screen.findByTestId("select-filterValue")
7779
await user.click(filterSelect)

0 commit comments

Comments
 (0)