mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
feat(slo): store search state in url (#168528)
This commit is contained in:
parent
a94fd4f9e0
commit
9cc7b8cda5
18 changed files with 174 additions and 114 deletions
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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(' ', '*')}*`,
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export const useFetchSloList = (): UseFetchSloListResponse => {
|
|||
isRefetching: false,
|
||||
isError: false,
|
||||
isSuccess: true,
|
||||
sloList,
|
||||
data: sloList,
|
||||
refetch: function () {} as UseFetchSloListResponse['refetch'],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 />);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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([
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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}
|
|
@ -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 }),
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue