diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx index 7e0e88d6c148d..3aa7651320bc3 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate_rule_editor.tsx @@ -31,7 +31,7 @@ type Props = Pick< export function BurnRateRuleEditor(props: Props) { const { setRuleParams, ruleParams, errors } = props; - const { isLoading: loadingInitialSlo, slo: initialSlo } = useFetchSloDetails({ + const { isLoading: loadingInitialSlo, data: initialSlo } = useFetchSloDetails({ sloId: ruleParams?.sloId, }); diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx index 5e8947a6c5ba9..392b0b1dcc400 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_overview.tsx @@ -26,7 +26,12 @@ export function SloOverview({ sloId, sloInstanceId, lastReloadRequestTime }: Emb application: { navigateToUrl }, http: { basePath }, } = useKibana().services; - const { isLoading, slo, refetch, isRefetching } = useFetchSloDetails({ + const { + isLoading, + data: slo, + refetch, + isRefetching, + } = useFetchSloDetails({ sloId, instanceId: sloInstanceId, }); diff --git a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx index 468358127bd18..8ad4985e0096d 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/overview/slo_selector.tsx @@ -26,7 +26,11 @@ export function SloSelector({ initialSlo, onSelected, hasError }: Props) { const [options, setOptions] = useState>>([]); const [selectedOptions, setSelectedOptions] = useState>>(); const [searchValue, setSearchValue] = useState(''); - const { isInitialLoading, isLoading, sloList } = useFetchSloList({ + const { + isInitialLoading, + isLoading, + data: sloList, + } = useFetchSloList({ kqlQuery: `slo.name: ${searchValue.replaceAll(' ', '*')}*`, }); diff --git a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts index 2faa0887b82ea..4e7d1889e5bb0 100644 --- a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts +++ b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts @@ -15,7 +15,7 @@ export const useFetchSloList = (): UseFetchSloListResponse => { isRefetching: false, isError: false, isSuccess: true, - sloList, + data: sloList, refetch: function () {} as UseFetchSloListResponse['refetch'], }; }; diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_details.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_details.ts index 38f980ee1bdc9..ad5511433522e 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_details.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_details.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { ALL_VALUE, GetSLOResponse } from '@kbn/slo-schema'; import { QueryObserverResult, RefetchOptions, RefetchQueryFilters, useQuery, } from '@tanstack/react-query'; -import { ALL_VALUE, GetSLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; @@ -21,7 +21,7 @@ export interface UseFetchSloDetailsResponse { isRefetching: boolean; isSuccess: boolean; isError: boolean; - slo: SLOWithSummaryResponse | undefined; + data: GetSLOResponse | undefined; refetch: ( options?: (RefetchOptions & RefetchQueryFilters) | undefined ) => Promise>; @@ -65,7 +65,7 @@ export function useFetchSloDetails({ ); return { - slo: data, + data, isLoading, isInitialLoading, isRefetching, diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts index 22a918eb23127..a4a67b0aecceb 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts @@ -33,7 +33,7 @@ export interface UseFetchSloListResponse { isRefetching: boolean; isSuccess: boolean; isError: boolean; - sloList: FindSLOResponse | undefined; + data: FindSLOResponse | undefined; refetch: ( options?: (RefetchOptions & RefetchQueryFilters) | undefined ) => Promise>; @@ -48,16 +48,13 @@ export function useFetchSloList({ sortBy = 'status', sortDirection = 'desc', shouldRefetch, -}: SLOListParams | undefined = {}): UseFetchSloListResponse { +}: SLOListParams = {}): UseFetchSloListResponse { const { http, notifications: { toasts }, } = useKibana().services; const queryClient = useQueryClient(); - - const [stateRefetchInterval, setStateRefetchInterval] = useState( - SHORT_REFETCH_INTERVAL - ); + const [stateRefetchInterval, setStateRefetchInterval] = useState(SHORT_REFETCH_INTERVAL); const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery( { @@ -115,7 +112,7 @@ export function useFetchSloList({ ); return { - sloList: data, + data, isInitialLoading, isLoading, isRefetching, diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx index 5dad78763fe04..f31d36c822264 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.test.tsx @@ -124,7 +124,7 @@ describe('SLO Details Page', () => { it('navigates to the SLO List page', async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => false }); render(); @@ -135,7 +135,7 @@ describe('SLO Details Page', () => { it('renders the PageNotFound when the SLO cannot be found', async () => { useParamsMock.mockReturnValue('nonexistent'); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo: undefined }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: undefined }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -146,7 +146,7 @@ describe('SLO Details Page', () => { it('renders the loading spinner when fetching the SLO', async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: true, slo: undefined }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: true, data: undefined }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -159,7 +159,7 @@ describe('SLO Details Page', () => { it('renders the SLO details page with loading charts when summary data is loading', async () => { const slo = buildSlo({ id: HEALTHY_STEP_DOWN_ROLLING_SLO }); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: true, @@ -178,7 +178,7 @@ describe('SLO Details Page', () => { it('renders the SLO details page with the overview and chart panels', async () => { const slo = buildSlo({ id: HEALTHY_STEP_DOWN_ROLLING_SLO }); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -194,7 +194,7 @@ describe('SLO Details Page', () => { it("renders a 'Edit' button under actions menu", async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -206,7 +206,7 @@ describe('SLO Details Page', () => { it("renders a 'Create alert rule' button under actions menu", async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -218,7 +218,7 @@ describe('SLO Details Page', () => { it("renders a 'Manage rules' button under actions menu", async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -230,7 +230,7 @@ describe('SLO Details Page', () => { it("renders a 'Clone' button under actions menu", async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -271,7 +271,7 @@ describe('SLO Details Page', () => { it("renders a 'Delete' button under actions menu", async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -301,7 +301,7 @@ describe('SLO Details Page', () => { it('renders the Overview tab by default', async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, @@ -320,7 +320,7 @@ describe('SLO Details Page', () => { it("renders a 'Explore in APM' button under actions menu", async () => { const slo = buildSlo({ indicator: buildApmAvailabilityIndicator() }); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); @@ -334,7 +334,7 @@ describe('SLO Details Page', () => { it("does not render a 'Explore in APM' button under actions menu", async () => { const slo = buildSlo(); useParamsMock.mockReturnValue(slo.id); - useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo }); useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(); diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx index 6b50fbde29268..195cb164e5197 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx @@ -45,7 +45,7 @@ export function SloDetailsPage() { const sloInstanceId = useGetInstanceIdQueryParam(); const { storeAutoRefreshState, getAutoRefreshState } = useAutoRefreshStorage(); const [isAutoRefreshing, setIsAutoRefreshing] = useState(getAutoRefreshState()); - const { isLoading, slo } = useFetchSloDetails({ + const { isLoading, data: slo } = useFetchSloDetails({ sloId, instanceId: sloInstanceId, shouldRefetch: isAutoRefreshing, diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx index c38689200a164..b158094a67aab 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx @@ -147,7 +147,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: undefined }); useFetchIndicesMock.mockReturnValue({ isLoading: false, @@ -201,7 +201,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: undefined }); useFetchIndicesMock.mockReturnValue({ isLoading: false, @@ -237,7 +237,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: undefined }); useFetchIndicesMock.mockReturnValue({ isLoading: false, @@ -287,7 +287,7 @@ describe('SLO Edit Page', () => { data: ['some-index'], }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: undefined }); const mockCreate = jest.fn(); const mockUpdate = jest.fn(); @@ -377,7 +377,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: undefined }); useFetchApmSuggestionsMock.mockReturnValue({ suggestions: ['cartService'], @@ -428,7 +428,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: slo }); useFetchIndicesMock.mockReturnValue({ isLoading: false, @@ -496,7 +496,7 @@ describe('SLO Edit Page', () => { data: ['some-index'], }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: slo }); const mockCreate = jest.fn(); const mockUpdate = jest.fn(); @@ -537,7 +537,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: slo }); useFetchApmSuggestionsMock.mockReturnValue({ suggestions: ['cartService'], @@ -607,7 +607,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: slo }); useFetchIndicesMock.mockReturnValue({ isLoading: false, @@ -648,7 +648,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: slo }); useFetchIndicesMock.mockReturnValue({ isLoading: false, @@ -693,7 +693,7 @@ describe('SLO Edit Page', () => { .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: 'create-rule=true', state: '', hash: '' }); - useFetchSloMock.mockReturnValue({ isLoading: false, slo }); + useFetchSloMock.mockReturnValue({ isLoading: false, data: slo }); useFetchIndicesMock.mockReturnValue({ isLoading: false, diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx index a9da5c0c0ee97..1f5cc7f38ed3c 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx @@ -33,7 +33,7 @@ export function SloEditPage() { const { sloId } = useParams<{ sloId: string | undefined }>(); const { hasAtLeast } = useLicense(); const hasRightLicense = hasAtLeast('platinum'); - const { slo, isInitialLoading } = useFetchSloDetails({ sloId }); + const { data: slo, isInitialLoading } = useFetchSloDetails({ sloId }); useBreadcrumbs([ { diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index 651a97d364439..380d0100db1a1 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -9,27 +9,35 @@ import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui'; import { useIsMutating } from '@tanstack/react-query'; import React, { useState } from 'react'; import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list'; +import { useUrlSearchState } from '../hooks/use_url_search_state'; import { SloListItems } from './slo_list_items'; -import { SloListSearchFilterSortBar, SortField } from './slo_list_search_filter_sort_bar'; +import { SloListSearchBar, SortField } from './slo_list_search_bar'; export interface Props { autoRefresh: boolean; } export function SloList({ autoRefresh }: Props) { - const [activePage, setActivePage] = useState(0); - const [query, setQuery] = useState(''); - const [sort, setSort] = useState('status'); + const { state, store: storeState } = useUrlSearchState(); + const [page, setPage] = useState(state.page); + const [query, setQuery] = useState(state.kqlQuery); + const [sort, setSort] = useState(state.sort.by); + const [direction] = useState<'asc' | 'desc'>(state.sort.direction); - const { isLoading, isRefetching, isError, sloList } = useFetchSloList({ - page: activePage + 1, + const { + isLoading, + isRefetching, + isError, + data: sloList, + } = useFetchSloList({ + page: page + 1, kqlQuery: query, sortBy: sort, - sortDirection: 'desc', + sortDirection: direction, shouldRefetch: autoRefresh, }); - const { results = [], total = 0, perPage = 0 } = sloList || {}; + const { results = [], total = 0, perPage = 0 } = sloList ?? {}; const isCreatingSlo = Boolean(useIsMutating(['creatingSlo'])); const isCloningSlo = Boolean(useIsMutating(['cloningSlo'])); @@ -37,40 +45,43 @@ export function SloList({ autoRefresh }: Props) { const isDeletingSlo = Boolean(useIsMutating(['deleteSlo'])); const handlePageClick = (pageNumber: number) => { - setActivePage(pageNumber); + setPage(pageNumber); + storeState({ page: pageNumber }); }; const handleChangeQuery = (newQuery: string) => { - setActivePage(0); + setPage(0); setQuery(newQuery); + storeState({ page: 0, kqlQuery: newQuery }); }; - const handleChangeSort = (newSort: SortField | undefined) => { - setActivePage(0); + const handleChangeSort = (newSort: SortField) => { + setPage(0); setSort(newSort); + storeState({ page: 0, sort: { by: newSort, direction: state.sort.direction } }); }; return ( - - - {results.length ? ( + {total > 0 ? ( diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_bar.stories.tsx similarity index 55% rename from x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx rename to x-pack/plugins/observability/public/pages/slos/components/slo_list_search_bar.stories.tsx index 4b1c31209de3b..fb39f46e81e96 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_bar.stories.tsx @@ -9,26 +9,23 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; -import { - SloListSearchFilterSortBar as Component, - SloListSearchFilterSortBarProps, -} from './slo_list_search_filter_sort_bar'; +import { SloListSearchBar as Component, Props } from './slo_list_search_bar'; +import { DEFAULT_STATE } from '../hooks/use_url_search_state'; export default { component: Component, - title: 'app/SLO/ListPage/SloListSearchFilterSortBar', + title: 'app/SLO/ListPage/SloListSearchBar', decorators: [KibanaReactStorybookDecorator], }; -const Template: ComponentStory = (props: SloListSearchFilterSortBarProps) => ( - -); +const Template: ComponentStory = (props: Props) => ; -const defaultProps: SloListSearchFilterSortBarProps = { +const defaultProps: Props = { loading: false, onChangeQuery: () => {}, onChangeSort: () => {}, + initialState: DEFAULT_STATE, }; -export const SloListSearchFilterSortBar = Template.bind({}); -SloListSearchFilterSortBar.args = defaultProps; +export const SloListSearchBar = Template.bind({}); +SloListSearchBar.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_bar.tsx similarity index 88% rename from x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx rename to x-pack/plugins/observability/public/pages/slos/components/slo_list_search_bar.tsx index c1d9c6b3c3a07..dc6aebfd3eae7 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_bar.tsx @@ -22,11 +22,13 @@ import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import React, { useState } from 'react'; import { useCreateDataView } from '../../../hooks/use_create_data_view'; import { useKibana } from '../../../utils/kibana_react'; +import { SearchState } from '../hooks/use_url_search_state'; -export interface SloListSearchFilterSortBarProps { +export interface Props { loading: boolean; + initialState: SearchState; onChangeQuery: (query: string) => void; - onChangeSort: (sort: SortField | undefined) => void; + onChangeSort: (sort: SortField) => void; } export type SortField = 'sli_value' | 'error_budget_consumed' | 'error_budget_remaining' | 'status'; @@ -49,7 +51,6 @@ const SORT_OPTIONS: Array> = [ defaultMessage: 'SLO status', }), type: 'status', - checked: 'on', }, { label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', { @@ -65,26 +66,26 @@ const SORT_OPTIONS: Array> = [ }, ]; -export function SloListSearchFilterSortBar({ - loading, - onChangeQuery, - onChangeSort, -}: SloListSearchFilterSortBarProps) { +export function SloListSearchBar({ loading, onChangeQuery, onChangeSort, initialState }: Props) { const { data, dataViews, docLinks, http, notifications, storage, uiSettings, unifiedSearch } = useKibana().services; const { dataView } = useCreateDataView({ indexPatternString: '.slo-observability.summary-*' }); + const [query, setQuery] = useState(initialState.kqlQuery); const [isSortPopoverOpen, setSortPopoverOpen] = useState(false); - const [sortOptions, setSortOptions] = useState(SORT_OPTIONS); - const [query, setQuery] = useState(''); - + const [sortOptions, setSortOptions] = useState>>( + SORT_OPTIONS.map((option) => ({ + ...option, + checked: option.type === initialState.sort.by ? 'on' : undefined, + })) + ); const selectedSort = sortOptions.find((option) => option.checked === 'on'); - const handleToggleSortButton = () => setSortPopoverOpen(!isSortPopoverOpen); + const handleToggleSortButton = () => setSortPopoverOpen(!isSortPopoverOpen); const handleChangeSort = (newOptions: Array>) => { setSortOptions(newOptions); setSortPopoverOpen(false); - onChangeSort(newOptions.find((o) => o.checked)?.type); + onChangeSort(newOptions.find((o) => o.checked)!.type); }; return ( @@ -133,7 +134,7 @@ export function SloListSearchFilterSortBar({ > {i18n.translate('xpack.observability.slo.list.sortByType', { defaultMessage: 'Sort by {type}', - values: { type: selectedSort?.label.toLowerCase() || '' }, + values: { type: selectedSort?.label.toLowerCase() ?? '' }, })} } @@ -149,7 +150,7 @@ export function SloListSearchFilterSortBar({ })} > - singleSelection + singleSelection="always" options={sortOptions} onChange={handleChangeSort} isLoading={loading} diff --git a/x-pack/plugins/observability/public/pages/slos/hooks/use_url_search_state.ts b/x-pack/plugins/observability/public/pages/slos/hooks/use_url_search_state.ts new file mode 100644 index 0000000000000..2d7fb860900fa --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/hooks/use_url_search_state.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useHistory } from 'react-router-dom'; +import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import deepmerge from 'deepmerge'; +import { SortField } from '../components/slo_list_search_bar'; + +export interface SearchState { + kqlQuery: string; + page: number; + sort: { + by: SortField; + direction: 'asc' | 'desc'; + }; +} + +export const DEFAULT_STATE = { + kqlQuery: '', + page: 0, + sort: { by: 'status' as const, direction: 'desc' as const }, +}; + +export function useUrlSearchState(): { + state: SearchState; + store: (state: Partial) => Promise; +} { + const history = useHistory(); + const urlStateStorage = createKbnUrlStateStorage({ + history, + useHash: false, + useHashQuery: false, + }); + + const searchState = urlStateStorage.get('search') ?? DEFAULT_STATE; + + return { + state: deepmerge(DEFAULT_STATE, searchState), + store: (state: Partial) => + urlStateStorage.set('search', deepmerge(searchState, state), { replace: true }), + }; +} diff --git a/x-pack/plugins/observability/public/pages/slos/slos.test.tsx b/x-pack/plugins/observability/public/pages/slos/slos.test.tsx index 09d5af66b55dc..3e19b7a466be5 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.test.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.test.tsx @@ -5,25 +5,25 @@ * 2.0. */ +import { act, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import { screen, act, waitFor } from '@testing-library/react'; -import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; -import { render } from '../../utils/test_helper'; -import { useKibana } from '../../utils/kibana_react'; -import { useCreateSlo } from '../../hooks/slo/use_create_slo'; +import { paths } from '../../../common/locators/paths'; +import { historicalSummaryData } from '../../data/slo/historical_summary_data'; +import { emptySloList, sloList } from '../../data/slo/slo'; +import { useCapabilities } from '../../hooks/slo/use_capabilities'; import { useCloneSlo } from '../../hooks/slo/use_clone_slo'; +import { useCreateSlo } from '../../hooks/slo/use_create_slo'; import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; -import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; +import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list'; import { useLicense } from '../../hooks/use_license'; +import { useKibana } from '../../utils/kibana_react'; +import { render } from '../../utils/test_helper'; import { SlosPage } from './slos'; -import { emptySloList, sloList } from '../../data/slo/slo'; -import { historicalSummaryData } from '../../data/slo/historical_summary_data'; -import { useCapabilities } from '../../hooks/slo/use_capabilities'; -import { paths } from '../../../common/locators/paths'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -51,11 +51,10 @@ const useCapabilitiesMock = useCapabilities as jest.Mock; const mockCreateSlo = jest.fn(); const mockCloneSlo = jest.fn(); +const mockDeleteSlo = jest.fn(); useCreateSloMock.mockReturnValue({ mutate: mockCreateSlo }); useCloneSloMock.mockReturnValue({ mutate: mockCloneSlo }); - -const mockDeleteSlo = jest.fn(); useDeleteSloMock.mockReturnValue({ mutate: mockDeleteSlo }); const mockNavigate = jest.fn(); @@ -155,7 +154,7 @@ describe('SLOs Page', () => { }); it('navigates to the SLOs Welcome Page when the API has finished loading and there are no results', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: emptySloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, data: {}, @@ -171,7 +170,7 @@ describe('SLOs Page', () => { }); it('should have a create new SLO button', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, @@ -186,7 +185,7 @@ describe('SLOs Page', () => { }); it('should have an Auto Refresh button', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, @@ -202,7 +201,7 @@ describe('SLOs Page', () => { describe('when API has returned results', () => { it('renders the SLO list with SLO items', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, @@ -220,7 +219,7 @@ describe('SLOs Page', () => { }); it('allows editing an SLO', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, @@ -247,7 +246,7 @@ describe('SLOs Page', () => { }); it('allows creating a new rule for an SLO', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, @@ -272,7 +271,7 @@ describe('SLOs Page', () => { }); it('allows managing rules for an SLO', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, @@ -297,7 +296,7 @@ describe('SLOs Page', () => { }); it('allows deleting an SLO', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, @@ -327,7 +326,7 @@ describe('SLOs Page', () => { }); it('allows cloning an SLO', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useFetchHistoricalSummaryMock.mockReturnValue({ isLoading: false, diff --git a/x-pack/plugins/observability/public/pages/slos/slos.tsx b/x-pack/plugins/observability/public/pages/slos/slos.tsx index a183d7941f7e1..18fa418e4358b 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.tsx @@ -32,8 +32,8 @@ export function SlosPage() { const { hasWriteCapabilities } = useCapabilities(); const { hasAtLeast } = useLicense(); - const { isInitialLoading, isLoading, isError, sloList } = useFetchSloList(); - const { total } = sloList || { total: 0 }; + const { isInitialLoading, isLoading, isError, data: sloList } = useFetchSloList(); + const { total } = sloList ?? { total: 0 }; const { storeAutoRefreshState, getAutoRefreshState } = useAutoRefreshStorage(); const [isAutoRefreshing, setIsAutoRefreshing] = useState(getAutoRefreshState()); diff --git a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx index fbeae4cb872b2..a6b4f671d306c 100644 --- a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx +++ b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.test.tsx @@ -56,7 +56,7 @@ describe('SLOs Welcome Page', () => { describe('when the incorrect license is found', () => { it('renders the welcome message with subscription buttons', async () => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: emptySloList }); useLicenseMock.mockReturnValue({ hasAtLeast: () => false }); useGlobalDiagnosisMock.mockReturnValue({ data: { @@ -82,7 +82,7 @@ describe('SLOs Welcome Page', () => { describe('when loading is done and no results are found', () => { beforeEach(() => { - useFetchSloListMock.mockReturnValue({ isLoading: false, emptySloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: emptySloList }); }); it('disables the create slo button when no write capabilities', async () => { @@ -146,7 +146,7 @@ describe('SLOs Welcome Page', () => { describe('when loading is done and results are found', () => { beforeEach(() => { - useFetchSloListMock.mockReturnValue({ isLoading: false, sloList }); + useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList }); useGlobalDiagnosisMock.mockReturnValue({ data: { userPrivileges: { diff --git a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx index 5c4317cd4e138..16f4a75974453 100644 --- a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx +++ b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx @@ -40,8 +40,8 @@ export function SlosWelcomePage() { const { hasAtLeast } = useLicense(); const hasRightLicense = hasAtLeast('platinum'); - const { isLoading, sloList } = useFetchSloList(); - const { total } = sloList || { total: 0 }; + const { isLoading, data: sloList } = useFetchSloList(); + const { total } = sloList ?? { total: 0 }; const hasRequiredWritePrivileges = !!globalDiagnosis?.userPrivileges.write.has_all_requested; const hasRequiredReadPrivileges = !!globalDiagnosis?.userPrivileges.read.has_all_requested;