feat(slo): store search state in url (#168528)

This commit is contained in:
Kevin Delemme 2023-10-17 08:45:16 -04:00 committed by GitHub
parent a94fd4f9e0
commit 9cc7b8cda5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 174 additions and 114 deletions

View file

@ -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,
});

View file

@ -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,
});

View file

@ -26,7 +26,11 @@ export function SloSelector({ initialSlo, onSelected, hasError }: Props) {
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>();
const [searchValue, setSearchValue] = useState<string>('');
const { isInitialLoading, isLoading, sloList } = useFetchSloList({
const {
isInitialLoading,
isLoading,
data: sloList,
} = useFetchSloList({
kqlQuery: `slo.name: ${searchValue.replaceAll(' ', '*')}*`,
});

View file

@ -15,7 +15,7 @@ export const useFetchSloList = (): UseFetchSloListResponse => {
isRefetching: false,
isError: false,
isSuccess: true,
sloList,
data: sloList,
refetch: function () {} as UseFetchSloListResponse['refetch'],
};
};

View file

@ -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: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<GetSLOResponse | undefined, unknown>>;
@ -65,7 +65,7 @@ export function useFetchSloDetails({
);
return {
slo: data,
data,
isLoading,
isInitialLoading,
isRefetching,

View file

@ -33,7 +33,7 @@ export interface UseFetchSloListResponse {
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
sloList: FindSLOResponse | undefined;
data: FindSLOResponse | undefined;
refetch: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<FindSLOResponse | undefined, unknown>>;
@ -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<number | undefined>(
SHORT_REFETCH_INTERVAL
);
const [stateRefetchInterval, setStateRefetchInterval] = useState<number>(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,

View file

@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);
@ -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(<SloDetailsPage />);

View file

@ -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,

View file

@ -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,

View file

@ -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([
{

View file

@ -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<SortField | undefined>('status');
const { state, store: storeState } = useUrlSearchState();
const [page, setPage] = useState(state.page);
const [query, setQuery] = useState(state.kqlQuery);
const [sort, setSort] = useState<SortField>(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 (
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="sloList">
<EuiFlexItem grow>
<SloListSearchFilterSortBar
<SloListSearchBar
loading={isLoading || isCreatingSlo || isCloningSlo || isUpdatingSlo || isDeletingSlo}
onChangeQuery={handleChangeQuery}
onChangeSort={handleChangeSort}
initialState={state}
/>
</EuiFlexItem>
<EuiFlexItem>
<SloListItems sloList={results} loading={isLoading || isRefetching} error={isError} />
</EuiFlexItem>
{results.length ? (
{total > 0 ? (
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s" alignItems="flexEnd">
<EuiFlexItem>
<EuiPagination
pageCount={Math.ceil(total / perPage)}
activePage={activePage}
activePage={page}
onPageClick={handlePageClick}
/>
</EuiFlexItem>

View file

@ -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<typeof Component> = (props: SloListSearchFilterSortBarProps) => (
<Component {...props} />
);
const Template: ComponentStory<typeof Component> = (props: Props) => <Component {...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;

View file

@ -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<Item<SortField>> = [
defaultMessage: 'SLO status',
}),
type: 'status',
checked: 'on',
},
{
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', {
@ -65,26 +66,26 @@ const SORT_OPTIONS: Array<Item<SortField>> = [
},
];
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<Array<Item<SortField>>>(
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<Item<SortField>>) => {
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() ?? '' },
})}
</EuiFilterButton>
}
@ -149,7 +150,7 @@ export function SloListSearchFilterSortBar({
})}
</EuiPopoverTitle>
<EuiSelectable<Item<SortField>>
singleSelection
singleSelection="always"
options={sortOptions}
onChange={handleChangeSort}
isLoading={loading}

View file

@ -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<SearchState>) => Promise<string | undefined>;
} {
const history = useHistory();
const urlStateStorage = createKbnUrlStateStorage({
history,
useHash: false,
useHashQuery: false,
});
const searchState = urlStateStorage.get<SearchState>('search') ?? DEFAULT_STATE;
return {
state: deepmerge(DEFAULT_STATE, searchState),
store: (state: Partial<SearchState>) =>
urlStateStorage.set('search', deepmerge(searchState, state), { replace: true }),
};
}

View file

@ -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 { useCloneSlo } from '../../hooks/slo/use_clone_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 { useLicense } from '../../hooks/use_license';
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';
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 { 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';
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,

View file

@ -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<boolean>(getAutoRefreshState());

View file

@ -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: {

View file

@ -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;