mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[SLOs] Unified Search (#174054)
## Summary Fixes https://github.com/elastic/kibana/issues/173601 Implement Unified Search in SLO List view 1. SLO title has been removed to make it more consistent with other kibana apps 2. Feedback button is moved up 3. Auto refresh is removed <img width="1720" alt="image" src="0ff6fd83
-98ac-4737-bf4f-aa087739f110"> <img width="1728" alt="image" src="aa71e2e7
-3bc8-4be7-afb3-f4b7efffc953"> Filters <img width="1727" alt="image" src="d3bbf3d3
-d2b3-4574-ae7f-bb8258016930"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
615c16ec8a
commit
f1057ca3e6
32 changed files with 479 additions and 379 deletions
|
@ -14,7 +14,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import { buildExistsFilter } from '@kbn/es-query';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { EuiButton, EuiComboBox } from '@elastic/eui';
|
||||
import { SearchBar, SearchBarProps } from '../search_bar';
|
||||
import { setIndexPatterns } from '../services';
|
||||
|
||||
|
@ -695,4 +695,20 @@ storiesOf('SearchBar', module)
|
|||
},
|
||||
submitButtonStyle: 'full',
|
||||
})
|
||||
)
|
||||
|
||||
.add('with renderQueryInputAppend prop', () =>
|
||||
wrapSearchBarInContext({
|
||||
dataViewPickerComponentProps: {
|
||||
currentDataViewId: '1234',
|
||||
trigger: {
|
||||
'data-test-subj': 'dataView-switch-link',
|
||||
label: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
},
|
||||
onChangeDataView: action('onChangeDataView'),
|
||||
},
|
||||
submitButtonStyle: 'full',
|
||||
renderQueryInputAppend: () => <EuiButton onClick={() => {}}>Append</EuiButton>,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -70,6 +70,7 @@ export interface QueryBarMenuProps extends WithCloseFilterEditorConfirmModalProp
|
|||
buttonProps?: Partial<EuiButtonIconProps>;
|
||||
isDisabled?: boolean;
|
||||
suggestionsAbstraction?: SuggestionsAbstraction;
|
||||
renderQueryInputAppend?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
function QueryBarMenuComponent({
|
||||
|
|
|
@ -163,6 +163,7 @@ export interface QueryBarTopRowProps<QT extends Query | AggregateQuery = Query>
|
|||
onTextLangQuerySubmit: (query?: Query | AggregateQuery) => void;
|
||||
onTextLangQueryChange: (query: AggregateQuery) => void;
|
||||
submitOnBlur?: boolean;
|
||||
renderQueryInputAppend?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
export const SharingMetaFields = React.memo(function SharingMetaFields({
|
||||
|
@ -702,6 +703,7 @@ export const QueryBarTopRow = React.memo(
|
|||
? renderTextLangEditor()
|
||||
: null}
|
||||
</EuiFlexItem>
|
||||
{props.renderQueryInputAppend?.()}
|
||||
{shouldShowDatePickerAsBadge() && props.filterBar}
|
||||
{renderUpdateButton()}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -120,6 +120,8 @@ export interface SearchBarOwnProps<QT extends AggregateQuery | Query = Query> {
|
|||
isDisabled?: boolean;
|
||||
|
||||
submitOnBlur?: boolean;
|
||||
|
||||
renderQueryInputAppend?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
export type SearchBarProps<QT extends Query | AggregateQuery = Query> = SearchBarOwnProps<QT> &
|
||||
|
@ -524,6 +526,7 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
|
|||
: undefined
|
||||
}
|
||||
suggestionsAbstraction={this.props.suggestionsAbstraction}
|
||||
renderQueryInputAppend={this.props.renderQueryInputAppend}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
|
@ -610,6 +613,7 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
|
|||
onTextLangQueryChange={this.onTextLangQueryChange}
|
||||
submitOnBlur={this.props.submitOnBlur}
|
||||
suggestionsAbstraction={this.props.suggestionsAbstraction}
|
||||
renderQueryInputAppend={this.props.renderQueryInputAppend}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -87,6 +87,7 @@ const sortBySchema = t.union([
|
|||
|
||||
const findSLOParamsSchema = t.partial({
|
||||
query: t.partial({
|
||||
filters: t.string,
|
||||
kqlQuery: t.string,
|
||||
page: t.string,
|
||||
perPage: t.string,
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const SLO_FEEDBACK_LINK = 'https://ela.st/slo-feedback';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function FeedbackButton({ disabled }: Props) {
|
||||
return (
|
||||
<EuiButton
|
||||
data-test-subj="sloFeedbackButton"
|
||||
isDisabled={disabled}
|
||||
href={SLO_FEEDBACK_LINK}
|
||||
target="_blank"
|
||||
color="warning"
|
||||
iconType="editorComment"
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.feedbackButtonLabel', {
|
||||
defaultMessage: 'Tell us what you think!',
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -25,37 +25,40 @@ export function SloOutdatedCallout() {
|
|||
const { isLoading, data } = useFetchSloDefinitions({ includeOutdatedOnly: true });
|
||||
if (!isLoading && data && data.total > 0) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
title={i18n.translate('xpack.observability.slo.outdatedSloCallout.title', {
|
||||
defaultMessage: '{total} Outdated SLOs Detected',
|
||||
values: {
|
||||
total: data.total,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.slo.outdatedSloCallout.message"
|
||||
defaultMessage="We've noticed that you have {total} outdated SLO definitions, these SLOs will not be running or alerting until you've reset them. Please click the button below to review the SLO definitions; you can choose to either reset the SLO definition or remove it."
|
||||
values={{ total: data.total }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiButton
|
||||
color="warning"
|
||||
data-test-subj="o11ySloOutdatedCalloutViewOutdatedSloDefinitionsButton"
|
||||
fill
|
||||
onClick={handleClick}
|
||||
>
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
title={i18n.translate('xpack.observability.slo.outdatedSloCallout.title', {
|
||||
defaultMessage: '{total} Outdated SLOs Detected',
|
||||
values: {
|
||||
total: data.total,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.outdatedSloCallout.buttonLabel"
|
||||
defaultMessage="Review Outdated SLO Definitions"
|
||||
id="xpack.observability.slo.outdatedSloCallout.message"
|
||||
defaultMessage="We've noticed that you have {total} outdated SLO definitions, these SLOs will not be running or alerting until you've reset them. Please click the button below to review the SLO definitions; you can choose to either reset the SLO definition or remove it."
|
||||
values={{ total: data.total }}
|
||||
/>
|
||||
</EuiButton>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</p>
|
||||
<p>
|
||||
<EuiButton
|
||||
color="warning"
|
||||
data-test-subj="o11ySloOutdatedCalloutViewOutdatedSloDefinitionsButton"
|
||||
fill
|
||||
onClick={handleClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.outdatedSloCallout.buttonLabel"
|
||||
defaultMessage="Review Outdated SLO Definitions"
|
||||
/>
|
||||
</EuiButton>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -13,6 +13,8 @@ interface SloListFilter {
|
|||
perPage: number;
|
||||
sortBy: string;
|
||||
sortDirection: string;
|
||||
filters: string;
|
||||
lastRefresh?: number;
|
||||
}
|
||||
|
||||
export const sloKeys = {
|
||||
|
|
|
@ -8,9 +8,13 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FindSLOResponse } from '@kbn/slo-schema';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { DEFAULT_SLO_PAGE_SIZE } from '../../../common/slo/constants';
|
||||
import { SLO_LONG_REFETCH_INTERVAL, SLO_SHORT_REFETCH_INTERVAL } from '../../constants';
|
||||
import { useMemo } from 'react';
|
||||
import { buildQueryFromFilters, Filter } from '@kbn/es-query';
|
||||
import { useCreateDataView } from '../use_create_data_view';
|
||||
import {
|
||||
DEFAULT_SLO_PAGE_SIZE,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
} from '../../../common/slo/constants';
|
||||
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { sloKeys } from './query_key_factory';
|
||||
|
@ -20,8 +24,9 @@ interface SLOListParams {
|
|||
page?: number;
|
||||
sortBy?: string;
|
||||
sortDirection?: 'asc' | 'desc';
|
||||
shouldRefetch?: boolean;
|
||||
perPage?: number;
|
||||
filters?: Filter[];
|
||||
lastRefresh?: number;
|
||||
}
|
||||
|
||||
export interface UseFetchSloListResponse {
|
||||
|
@ -38,37 +43,57 @@ export function useFetchSloList({
|
|||
page = 1,
|
||||
sortBy = 'status',
|
||||
sortDirection = 'desc',
|
||||
shouldRefetch,
|
||||
perPage = DEFAULT_SLO_PAGE_SIZE,
|
||||
filters: filterDSL = [],
|
||||
lastRefresh,
|
||||
}: SLOListParams = {}): UseFetchSloListResponse {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const queryClient = useQueryClient();
|
||||
const [stateRefetchInterval, setStateRefetchInterval] = useState<number>(
|
||||
SLO_SHORT_REFETCH_INTERVAL
|
||||
);
|
||||
|
||||
const { dataView } = useCreateDataView({
|
||||
indexPatternString: SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
});
|
||||
|
||||
const filters = useMemo(() => {
|
||||
try {
|
||||
return JSON.stringify(
|
||||
buildQueryFromFilters(filterDSL, dataView, {
|
||||
ignoreFilterIfFieldNotInIndex: true,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}, [filterDSL, dataView]);
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: sloKeys.list({ kqlQuery, page, perPage, sortBy, sortDirection }),
|
||||
queryKey: sloKeys.list({
|
||||
kqlQuery,
|
||||
page,
|
||||
perPage,
|
||||
sortBy,
|
||||
sortDirection,
|
||||
filters,
|
||||
lastRefresh,
|
||||
}),
|
||||
queryFn: async ({ signal }) => {
|
||||
const response = await http.get<FindSLOResponse>(`/api/observability/slos`, {
|
||||
return await http.get<FindSLOResponse>(`/api/observability/slos`, {
|
||||
query: {
|
||||
...(kqlQuery && { kqlQuery }),
|
||||
...(sortBy && { sortBy }),
|
||||
...(sortDirection && { sortDirection }),
|
||||
...(page && { page }),
|
||||
...(perPage && { perPage }),
|
||||
...(filters && { filters }),
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
cacheTime: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchInterval: shouldRefetch ? stateRefetchInterval : undefined,
|
||||
retry: (failureCount, error) => {
|
||||
if (String(error) === 'Error: Forbidden') {
|
||||
return false;
|
||||
|
@ -79,16 +104,6 @@ export function useFetchSloList({
|
|||
queryClient.invalidateQueries({ queryKey: sloKeys.historicalSummaries(), exact: false });
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.activeAlerts(), exact: false });
|
||||
queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false });
|
||||
|
||||
if (!shouldRefetch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.find((slo) => slo.summary.status === 'NO_DATA' || !slo.summary)) {
|
||||
setStateRefetchInterval(SLO_SHORT_REFETCH_INTERVAL);
|
||||
} else {
|
||||
setStateRefetchInterval(SLO_LONG_REFETCH_INTERVAL);
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('SloListLocator', () => {
|
|||
const location = await locator.getLocation({});
|
||||
expect(location.app).toEqual('observability');
|
||||
expect(location.path).toMatchInlineSnapshot(
|
||||
`"/slos?search=(compact:!t,kqlQuery:'',page:0,perPage:25,sort:(by:status,direction:desc),view:cardView)"`
|
||||
`"/slos?search=(compact:!t,filters:!(),kqlQuery:'',lastRefresh:0,page:0,perPage:25,sort:(by:status,direction:desc),view:cardView)"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -24,7 +24,7 @@ describe('SloListLocator', () => {
|
|||
});
|
||||
expect(location.app).toEqual('observability');
|
||||
expect(location.path).toMatchInlineSnapshot(
|
||||
`"/slos?search=(compact:!t,kqlQuery:'slo.name:%20%22Service%20Availability%22%20and%20slo.indicator.type%20:%20%22sli.kql.custom%22',page:0,perPage:25,sort:(by:status,direction:desc),view:cardView)"`
|
||||
`"/slos?search=(compact:!t,filters:!(),kqlQuery:'slo.name:%20%22Service%20Availability%22%20and%20slo.indicator.type%20:%20%22sli.kql.custom%22',lastRefresh:0,page:0,perPage:25,sort:(by:status,direction:desc),view:cardView)"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import React from 'react';
|
|||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../../../hooks/use_plugin_context';
|
||||
import HeaderMenuPortal from './header_menu_portal';
|
||||
const SLO_FEEDBACK_LINK = 'https://ela.st/slo-feedback';
|
||||
|
||||
export function HeaderMenu(): React.ReactElement | null {
|
||||
const {
|
||||
|
@ -27,6 +28,16 @@ export function HeaderMenu(): React.ReactElement | null {
|
|||
return (
|
||||
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme.theme$}>
|
||||
<EuiHeaderLinks>
|
||||
<EuiHeaderLink
|
||||
data-test-subj="sloFeedbackButton"
|
||||
color="warning"
|
||||
href={SLO_FEEDBACK_LINK}
|
||||
iconType="popout"
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.giveFeedback', {
|
||||
defaultMessage: 'Give feedback',
|
||||
})}
|
||||
</EuiHeaderLink>
|
||||
<EuiHeaderLink
|
||||
color="primary"
|
||||
href={http.basePath.prepend('/app/integrations/browse')}
|
||||
|
|
|
@ -26,7 +26,6 @@ import { HeaderControl } from './components/header_control';
|
|||
import { paths } from '../../../common/locators/paths';
|
||||
import type { SloDetailsPathParams } from './types';
|
||||
import { AutoRefreshButton } from '../../components/slo/auto_refresh_button';
|
||||
import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button';
|
||||
import { useGetInstanceIdQueryParam } from './hooks/use_get_instance_id_query_param';
|
||||
import { useAutoRefreshStorage } from '../../components/slo/auto_refresh_button/hooks/use_auto_refresh_storage';
|
||||
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
|
||||
|
@ -81,7 +80,6 @@ export function SloDetailsPage() {
|
|||
isAutoRefreshing={isAutoRefreshing}
|
||||
onClick={handleToggleAutoRefresh}
|
||||
/>,
|
||||
<FeedbackButton disabled={isPerformingAction} />,
|
||||
],
|
||||
bottomBorder: false,
|
||||
}}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
|
||||
|
||||
import { createHtmlPortalNode, OutPortal } from 'react-reverse-portal';
|
||||
import { FeedbackButton } from '../alert_details/components/feedback_button';
|
||||
import { paths } from '../../../common/locators/paths';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
|
@ -18,7 +19,6 @@ import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
|||
import { useLicense } from '../../hooks/use_license';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis';
|
||||
import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button';
|
||||
import { SloEditForm } from './components/slo_edit_form';
|
||||
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { paths } from '../../../../../common/locators/paths';
|
||||
import { useCapabilities } from '../../../../hooks/slo/use_capabilities';
|
||||
|
||||
export function CreateSloBtn() {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
|
||||
const { hasWriteCapabilities } = useCapabilities();
|
||||
|
||||
const handleClickCreateSlo = () => {
|
||||
navigateToUrl(basePath.prepend(paths.observability.sloCreate));
|
||||
};
|
||||
return (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="slosPageCreateNewSloButton"
|
||||
disabled={!hasWriteCapabilities}
|
||||
fill
|
||||
onClick={handleClickCreateSlo}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.sloList.pageHeader.create', {
|
||||
defaultMessage: 'Create SLO',
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiSelectable,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { SearchState } from '../../hooks/use_url_search_state';
|
||||
import { Item, SortField } from '../slo_list_search_bar';
|
||||
|
||||
interface Props {
|
||||
initialState: SearchState;
|
||||
loading: boolean;
|
||||
onStateChange: (newState: Partial<SearchState>) => void;
|
||||
}
|
||||
|
||||
export function SortBySelect({ initialState, onStateChange, loading }: Props) {
|
||||
const [isSortPopoverOpen, setSortPopoverOpen] = useState(false);
|
||||
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 handleChangeSort = (newOptions: Array<Item<SortField>>) => {
|
||||
setSortOptions(newOptions);
|
||||
setSortPopoverOpen(false);
|
||||
onStateChange({
|
||||
page: 0,
|
||||
sort: { by: newOptions.find((o) => o.checked)!.type, direction: initialState.sort.direction },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFilterGroup>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiFilterButton
|
||||
disabled={loading}
|
||||
iconType="arrowDown"
|
||||
onClick={handleToggleSortButton}
|
||||
isSelected={isSortPopoverOpen}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.list.sortByType', {
|
||||
defaultMessage: 'Sort by {type}',
|
||||
values: { type: selectedSort?.label.toLowerCase() ?? '' },
|
||||
})}
|
||||
</EuiFilterButton>
|
||||
}
|
||||
isOpen={isSortPopoverOpen}
|
||||
closePopover={handleToggleSortButton}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downCenter"
|
||||
>
|
||||
<div style={{ width: 250 }}>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{i18n.translate('xpack.observability.slo.list.sortBy', {
|
||||
defaultMessage: 'Sort by',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiSelectable<Item<SortField>>
|
||||
singleSelection="always"
|
||||
options={sortOptions}
|
||||
onChange={handleChangeSort}
|
||||
isLoading={loading}
|
||||
>
|
||||
{(list) => list}
|
||||
</EuiSelectable>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const SORT_OPTIONS: Array<Item<SortField>> = [
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.sliValue', {
|
||||
defaultMessage: 'SLI value',
|
||||
}),
|
||||
type: 'sli_value',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.sloStatus', {
|
||||
defaultMessage: 'SLO status',
|
||||
}),
|
||||
type: 'status',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', {
|
||||
defaultMessage: 'Error budget consumed',
|
||||
}),
|
||||
type: 'error_budget_consumed',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetRemaining', {
|
||||
defaultMessage: 'Error budget remaining',
|
||||
}),
|
||||
type: 'error_budget_remaining',
|
||||
},
|
||||
];
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { SloList as Component, Props } from './slo_list';
|
||||
import { SloList as Component } from './slo_list';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
|
@ -18,11 +18,9 @@ export default {
|
|||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Component> = (props: Props) => <Component {...props} />;
|
||||
const Template: ComponentStory<typeof Component> = () => <Component />;
|
||||
|
||||
const defaultProps = {
|
||||
autoRefresh: true,
|
||||
};
|
||||
const defaultProps = {};
|
||||
|
||||
export const SloList = Template.bind({});
|
||||
SloList.args = defaultProps;
|
||||
|
|
|
@ -7,26 +7,17 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTablePagination } from '@elastic/eui';
|
||||
import { useIsMutating } from '@tanstack/react-query';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { CreateSloBtn } from './common/create_slo_btn';
|
||||
import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list';
|
||||
import { useUrlSearchState } from '../hooks/use_url_search_state';
|
||||
import { SearchState, useUrlSearchState } from '../hooks/use_url_search_state';
|
||||
import { SlosView } from './slos_view';
|
||||
import { SloListSearchBar, SortDirection, SortField } from './slo_list_search_bar';
|
||||
import { SLOView, ToggleSLOView } from './toggle_slo_view';
|
||||
import { SloListSearchBar } from './slo_list_search_bar';
|
||||
import { ToggleSLOView } from './toggle_slo_view';
|
||||
|
||||
export interface Props {
|
||||
autoRefresh: boolean;
|
||||
}
|
||||
|
||||
export function SloList({ autoRefresh }: Props) {
|
||||
export function SloList() {
|
||||
const { state, store: storeState } = useUrlSearchState();
|
||||
const [page, setPage] = useState(state.page);
|
||||
const [perPage, setPerPage] = useState(state.perPage);
|
||||
const [query, setQuery] = useState(state.kqlQuery);
|
||||
const [sort, setSort] = useState<SortField>(state.sort.by);
|
||||
const [direction] = useState<SortDirection>(state.sort.direction);
|
||||
const [view, setView] = useState<SLOView>(state.view);
|
||||
const [isCompact, setCompact] = useState<boolean>(state.compact);
|
||||
const { view, page, perPage, kqlQuery, filters, compact: isCompact } = state;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
|
@ -35,11 +26,12 @@ export function SloList({ autoRefresh }: Props) {
|
|||
data: sloList,
|
||||
} = useFetchSloList({
|
||||
perPage,
|
||||
filters,
|
||||
page: page + 1,
|
||||
kqlQuery: query,
|
||||
sortBy: sort,
|
||||
sortDirection: direction,
|
||||
shouldRefetch: autoRefresh,
|
||||
kqlQuery,
|
||||
sortBy: state.sort.by,
|
||||
sortDirection: state.sort.direction,
|
||||
lastRefresh: state.lastRefresh,
|
||||
});
|
||||
|
||||
const { results = [], total = 0 } = sloList ?? {};
|
||||
|
@ -49,49 +41,34 @@ export function SloList({ autoRefresh }: Props) {
|
|||
const isUpdatingSlo = Boolean(useIsMutating(['updatingSlo']));
|
||||
const isDeletingSlo = Boolean(useIsMutating(['deleteSlo']));
|
||||
|
||||
const handlePageClick = (pageNumber: number) => {
|
||||
setPage(pageNumber);
|
||||
storeState({ page: pageNumber });
|
||||
};
|
||||
|
||||
const handleChangeQuery = (newQuery: string) => {
|
||||
setPage(0);
|
||||
setQuery(newQuery);
|
||||
storeState({ page: 0, kqlQuery: newQuery });
|
||||
};
|
||||
|
||||
const handleChangeSort = (newSort: SortField) => {
|
||||
setPage(0);
|
||||
setSort(newSort);
|
||||
storeState({ page: 0, sort: { by: newSort, direction: state.sort.direction } });
|
||||
};
|
||||
|
||||
const handleChangeView = (newView: SLOView) => {
|
||||
setView(newView);
|
||||
storeState({ view: newView });
|
||||
};
|
||||
|
||||
const handleToggleCompactView = () => {
|
||||
const newCompact = !isCompact;
|
||||
setCompact(newCompact);
|
||||
storeState({ compact: newCompact });
|
||||
const onStateChange = (newState: Partial<SearchState>) => {
|
||||
storeState({ page: 0, ...newState });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="sloList">
|
||||
<EuiFlexItem grow>
|
||||
<SloListSearchBar
|
||||
loading={isLoading || isCreatingSlo || isCloningSlo || isUpdatingSlo || isDeletingSlo}
|
||||
onChangeQuery={handleChangeQuery}
|
||||
onChangeSort={handleChangeSort}
|
||||
initialState={state}
|
||||
/>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={true}>
|
||||
<SloListSearchBar
|
||||
query={kqlQuery}
|
||||
filters={filters}
|
||||
loading={isLoading || isCreatingSlo || isCloningSlo || isUpdatingSlo || isDeletingSlo}
|
||||
onStateChange={onStateChange}
|
||||
initialState={state}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CreateSloBtn />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ToggleSLOView
|
||||
sloList={sloList}
|
||||
sloView={view}
|
||||
onChangeView={handleChangeView}
|
||||
onToggleCompactView={handleToggleCompactView}
|
||||
onChangeView={(newView) => onStateChange({ view: newView })}
|
||||
onToggleCompactView={() => onStateChange({ compact: !isCompact })}
|
||||
isCompact={isCompact}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -108,12 +85,13 @@ export function SloList({ autoRefresh }: Props) {
|
|||
<EuiTablePagination
|
||||
pageCount={Math.ceil(total / perPage)}
|
||||
activePage={page}
|
||||
onChangePage={handlePageClick}
|
||||
onChangePage={(newPage) => {
|
||||
onStateChange({ page: newPage });
|
||||
}}
|
||||
itemsPerPage={perPage}
|
||||
itemsPerPageOptions={[10, 25, 50, 100]}
|
||||
onChangeItemsPerPage={(newPerPage) => {
|
||||
setPerPage(newPerPage);
|
||||
storeState({ perPage: newPerPage });
|
||||
storeState({ perPage: newPerPage, page: 0 });
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -22,8 +22,7 @@ const Template: ComponentStory<typeof Component> = (props: Props) => <Component
|
|||
|
||||
const defaultProps: Props = {
|
||||
loading: false,
|
||||
onChangeQuery: () => {},
|
||||
onChangeSort: () => {},
|
||||
onStateChange: () => {},
|
||||
initialState: DEFAULT_STATE,
|
||||
};
|
||||
|
||||
|
|
|
@ -5,29 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiSelectable,
|
||||
EuiSelectableOption,
|
||||
} from '@elastic/eui';
|
||||
import { EuiSelectableOption } from '@elastic/eui';
|
||||
import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { useCreateDataView } from '../../../hooks/use_create_data_view';
|
||||
import React from 'react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import styled from 'styled-components';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { ObservabilityPublicPluginsStart } from '../../..';
|
||||
import { SortBySelect } from './common/sort_by_select';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_NAME } from '../../../../common/slo/constants';
|
||||
import { useCreateDataView } from '../../../hooks/use_create_data_view';
|
||||
import { SearchState } from '../hooks/use_url_search_state';
|
||||
|
||||
export interface Props {
|
||||
query?: string;
|
||||
filters?: Filter[];
|
||||
loading: boolean;
|
||||
initialState: SearchState;
|
||||
onChangeQuery: (query: string) => void;
|
||||
onChangeSort: (sort: SortField) => void;
|
||||
onStateChange: (newState: Partial<SearchState>) => void;
|
||||
}
|
||||
|
||||
export type SortField = 'sli_value' | 'error_budget_consumed' | 'error_budget_remaining' | 'status';
|
||||
|
@ -39,127 +35,54 @@ export type Item<T> = EuiSelectableOption & {
|
|||
checked?: EuiSelectableOptionCheckedType;
|
||||
};
|
||||
|
||||
const SORT_OPTIONS: Array<Item<SortField>> = [
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.sliValue', {
|
||||
defaultMessage: 'SLI value',
|
||||
}),
|
||||
type: 'sli_value',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.sloStatus', {
|
||||
defaultMessage: 'SLO status',
|
||||
}),
|
||||
type: 'status',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetConsumed', {
|
||||
defaultMessage: 'Error budget consumed',
|
||||
}),
|
||||
type: 'error_budget_consumed',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.observability.slo.list.sortBy.errorBudgetRemaining', {
|
||||
defaultMessage: 'Error budget remaining',
|
||||
}),
|
||||
type: 'error_budget_remaining',
|
||||
},
|
||||
];
|
||||
|
||||
export type ViewMode = 'default' | 'compact';
|
||||
|
||||
export function SloListSearchBar({ loading, onChangeQuery, onChangeSort, initialState }: Props) {
|
||||
export function SloListSearchBar({ query, filters, loading, initialState, onStateChange }: Props) {
|
||||
const { dataView } = useCreateDataView({
|
||||
indexPatternString: SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
});
|
||||
|
||||
const {
|
||||
unifiedSearch: {
|
||||
ui: { QueryStringInput },
|
||||
ui: { SearchBar },
|
||||
},
|
||||
} = useKibana().services;
|
||||
const { dataView } = useCreateDataView({ indexPatternString: '.slo-observability.summary-*' });
|
||||
|
||||
const [query, setQuery] = useState(initialState.kqlQuery);
|
||||
const [isSortPopoverOpen, setSortPopoverOpen] = useState(false);
|
||||
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 handleChangeSort = (newOptions: Array<Item<SortField>>) => {
|
||||
setSortOptions(newOptions);
|
||||
setSortPopoverOpen(false);
|
||||
onChangeSort(newOptions.find((o) => o.checked)!.type);
|
||||
};
|
||||
} = useKibana<ObservabilityPublicPluginsStart>().services;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s" responsive>
|
||||
<EuiFlexItem grow>
|
||||
<QueryStringInput
|
||||
appName="Observability"
|
||||
bubbleSubmitEvent={false}
|
||||
disableAutoFocus
|
||||
onSubmit={(value: Query) => {
|
||||
setQuery(String(value.query));
|
||||
onChangeQuery(String(value.query));
|
||||
}}
|
||||
disableLanguageSwitcher
|
||||
isDisabled={loading}
|
||||
autoSubmit
|
||||
indexPatterns={dataView ? [dataView] : []}
|
||||
placeholder={i18n.translate('xpack.observability.slo.list.search', {
|
||||
defaultMessage: 'Search your SLOs...',
|
||||
})}
|
||||
query={{ query: String(query), language: 'kuery' }}
|
||||
size="s"
|
||||
onChange={(value) => setQuery(String(value.query))}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" responsive>
|
||||
<EuiFlexItem style={{ maxWidth: 250 }}>
|
||||
<EuiFilterGroup>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiFilterButton
|
||||
disabled={loading}
|
||||
iconType="arrowDown"
|
||||
onClick={handleToggleSortButton}
|
||||
isSelected={isSortPopoverOpen}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.list.sortByType', {
|
||||
defaultMessage: 'Sort by {type}',
|
||||
values: { type: selectedSort?.label.toLowerCase() ?? '' },
|
||||
})}
|
||||
</EuiFilterButton>
|
||||
}
|
||||
isOpen={isSortPopoverOpen}
|
||||
closePopover={handleToggleSortButton}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downCenter"
|
||||
>
|
||||
<div style={{ width: 250 }}>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{i18n.translate('xpack.observability.slo.list.sortBy', {
|
||||
defaultMessage: 'Sort by',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiSelectable<Item<SortField>>
|
||||
singleSelection="always"
|
||||
options={sortOptions}
|
||||
onChange={handleChangeSort}
|
||||
isLoading={loading}
|
||||
>
|
||||
{(list) => list}
|
||||
</EuiSelectable>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<Container>
|
||||
<SearchBar
|
||||
appName="observability"
|
||||
placeholder={i18n.translate('xpack.observability.slo.list.search', {
|
||||
defaultMessage: 'Search your SLOs...',
|
||||
})}
|
||||
indexPatterns={dataView ? [dataView] : []}
|
||||
isDisabled={loading}
|
||||
renderQueryInputAppend={() => (
|
||||
<SortBySelect
|
||||
initialState={initialState}
|
||||
loading={loading}
|
||||
onStateChange={onStateChange}
|
||||
/>
|
||||
)}
|
||||
filters={filters}
|
||||
onFiltersUpdated={(newFilters) => {
|
||||
onStateChange({ filters: newFilters });
|
||||
}}
|
||||
onQuerySubmit={({ query: value }) => {
|
||||
onStateChange({ kqlQuery: String(value?.query), lastRefresh: Date.now() });
|
||||
}}
|
||||
query={{ query: String(query), language: 'kuery' }}
|
||||
showSubmitButton={true}
|
||||
showDatePicker={false}
|
||||
showQueryInput={true}
|
||||
disableQueryLanguageSwitcher={true}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
.uniSearchBar {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FindSLOResponse } from '@kbn/slo-schema';
|
||||
import { SLOViewSettings } from './slo_view_settings';
|
||||
|
||||
export type SLOView = 'cardView' | 'listView';
|
||||
|
@ -17,6 +19,7 @@ interface Props {
|
|||
onChangeView: (view: SLOView) => void;
|
||||
isCompact: boolean;
|
||||
sloView: SLOView;
|
||||
sloList?: FindSLOResponse;
|
||||
}
|
||||
|
||||
const toggleButtonsIcons = [
|
||||
|
@ -39,10 +42,38 @@ export function ToggleSLOView({
|
|||
onChangeView,
|
||||
onToggleCompactView,
|
||||
isCompact = true,
|
||||
sloList,
|
||||
}: Props) {
|
||||
const total = sloList?.total ?? 0;
|
||||
const pageSize = sloList?.perPage ?? 0;
|
||||
const pageIndex = sloList?.page ?? 1;
|
||||
|
||||
const rangeStart = (total === 0 ? 0 : pageSize * (pageIndex - 1)) + 1;
|
||||
const rangeEnd = Math.min(total, pageSize * (pageIndex - 1) + pageSize);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.observability.overview.pagination.description"
|
||||
defaultMessage="Showing {currentCount} of {total} {slos}"
|
||||
values={{
|
||||
currentCount: <strong>{`${rangeStart}-${rangeEnd}`}</strong>,
|
||||
total,
|
||||
slos: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.overview.slos.label"
|
||||
defaultMessage="SLOs"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.observability.toggleSLOView.euiButtonGroup.sloView', {
|
||||
defaultMessage: 'SLO View',
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import deepmerge from 'deepmerge';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { DEFAULT_SLO_PAGE_SIZE } from '../../../../common/slo/constants';
|
||||
import type { SortField, SortDirection } from '../components/slo_list_search_bar';
|
||||
import type { SLOView } from '../components/toggle_slo_view';
|
||||
|
@ -24,6 +26,8 @@ export interface SearchState {
|
|||
};
|
||||
view: SLOView;
|
||||
compact: boolean;
|
||||
filters: Filter[];
|
||||
lastRefresh?: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_STATE = {
|
||||
|
@ -33,27 +37,51 @@ export const DEFAULT_STATE = {
|
|||
sort: { by: 'status' as const, direction: 'desc' as const },
|
||||
view: 'cardView' as const,
|
||||
compact: true,
|
||||
filters: [],
|
||||
lastRefresh: 0,
|
||||
};
|
||||
|
||||
export function useUrlSearchState(): {
|
||||
state: SearchState;
|
||||
store: (state: Partial<SearchState>) => Promise<string | undefined>;
|
||||
} {
|
||||
const [state, setState] = useState<SearchState>(DEFAULT_STATE);
|
||||
const history = useHistory();
|
||||
const urlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
useHashQuery: false,
|
||||
});
|
||||
const urlStateStorage = useRef(
|
||||
createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
useHashQuery: false,
|
||||
})
|
||||
);
|
||||
|
||||
const searchState =
|
||||
urlStateStorage.get<SearchState>(SLO_LIST_SEARCH_URL_STORAGE_KEY) ?? DEFAULT_STATE;
|
||||
useEffect(() => {
|
||||
const sub = urlStateStorage.current
|
||||
?.change$<SearchState>(SLO_LIST_SEARCH_URL_STORAGE_KEY)
|
||||
.subscribe((newSearchState) => {
|
||||
if (newSearchState) {
|
||||
setState(newSearchState);
|
||||
}
|
||||
});
|
||||
|
||||
setState(
|
||||
urlStateStorage.current?.get<SearchState>(SLO_LIST_SEARCH_URL_STORAGE_KEY) ?? DEFAULT_STATE
|
||||
);
|
||||
|
||||
return () => {
|
||||
sub?.unsubscribe();
|
||||
};
|
||||
}, [urlStateStorage]);
|
||||
|
||||
return {
|
||||
state: deepmerge(DEFAULT_STATE, searchState),
|
||||
store: (state: Partial<SearchState>) =>
|
||||
urlStateStorage.set(SLO_LIST_SEARCH_URL_STORAGE_KEY, deepmerge(searchState, state), {
|
||||
replace: true,
|
||||
}),
|
||||
state: deepmerge(DEFAULT_STATE, state),
|
||||
store: (newState: Partial<SearchState>) =>
|
||||
urlStateStorage.current?.set(
|
||||
SLO_LIST_SEARCH_URL_STORAGE_KEY,
|
||||
{ ...state, ...newState },
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -111,6 +111,7 @@ const mockKibana = () => {
|
|||
},
|
||||
unifiedSearch: {
|
||||
ui: {
|
||||
SearchBar: () => <div>SearchBar</div>,
|
||||
QueryStringInput: () => <div>Query String Input</div>,
|
||||
},
|
||||
autocomplete: {
|
||||
|
@ -181,22 +182,7 @@ describe('SLOs Page', () => {
|
|||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Create new SLO')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have an Auto Refresh button', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, data: sloList });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: historicalSummaryData,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('autoRefreshButton')).toBeTruthy();
|
||||
expect(screen.getByText('Create SLO')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when API has returned results', () => {
|
||||
|
@ -218,7 +204,7 @@ describe('SLOs Page', () => {
|
|||
expect(screen.queryByTestId('slosPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloList')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('sloItem')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('sloItem').length).toBe(sloList.results.length);
|
||||
expect((await screen.findAllByTestId('sloItem')).length).toBe(sloList.results.length);
|
||||
});
|
||||
|
||||
it('allows editing an SLO', async () => {
|
||||
|
|
|
@ -5,22 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
|
||||
import { SloList } from './components/slo_list';
|
||||
import { AutoRefreshButton } from '../../components/slo/auto_refresh_button';
|
||||
import { HeaderTitle } from './components/header_title';
|
||||
import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button';
|
||||
import { paths } from '../../../common/locators/paths';
|
||||
import { useAutoRefreshStorage } from '../../components/slo/auto_refresh_button/hooks/use_auto_refresh_storage';
|
||||
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
|
||||
import { SloOutdatedCallout } from '../../components/slo/slo_outdated_callout';
|
||||
|
||||
|
@ -30,15 +24,11 @@ export function SlosPage() {
|
|||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const { hasWriteCapabilities } = useCapabilities();
|
||||
const { hasAtLeast } = useLicense();
|
||||
|
||||
const { isLoading, isError, data: sloList } = useFetchSloList();
|
||||
const { total } = sloList ?? { total: 0 };
|
||||
|
||||
const { storeAutoRefreshState, getAutoRefreshState } = useAutoRefreshStorage();
|
||||
const [isAutoRefreshing, setIsAutoRefreshing] = useState<boolean>(getAutoRefreshState());
|
||||
|
||||
useBreadcrumbs([
|
||||
{
|
||||
href: basePath.prepend(paths.observability.slos),
|
||||
|
@ -55,45 +45,11 @@ export function SlosPage() {
|
|||
}
|
||||
}, [basePath, hasAtLeast, isError, isLoading, navigateToUrl, total]);
|
||||
|
||||
const handleClickCreateSlo = () => {
|
||||
navigateToUrl(basePath.prepend(paths.observability.sloCreate));
|
||||
};
|
||||
|
||||
const handleToggleAutoRefresh = () => {
|
||||
setIsAutoRefreshing(!isAutoRefreshing);
|
||||
storeAutoRefreshState(!isAutoRefreshing);
|
||||
};
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: <HeaderTitle />,
|
||||
rightSideItems: [
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="slosPageCreateNewSloButton"
|
||||
disabled={!hasWriteCapabilities}
|
||||
fill
|
||||
onClick={handleClickCreateSlo}
|
||||
>
|
||||
{i18n.translate('xpack.observability.slo.sloList.pageHeader.createNewButtonLabel', {
|
||||
defaultMessage: 'Create new SLO',
|
||||
})}
|
||||
</EuiButton>,
|
||||
<AutoRefreshButton
|
||||
isAutoRefreshing={isAutoRefreshing}
|
||||
onClick={handleToggleAutoRefresh}
|
||||
/>,
|
||||
<FeedbackButton />,
|
||||
],
|
||||
bottomBorder: false,
|
||||
}}
|
||||
data-test-subj="slosPage"
|
||||
>
|
||||
<ObservabilityPageTemplate data-test-subj="slosPage">
|
||||
<HeaderMenu />
|
||||
<SloOutdatedCallout />
|
||||
<EuiSpacer size="l" />
|
||||
<SloList autoRefresh={isAutoRefreshing} />
|
||||
<SloList />
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ describe('FindSLO', () => {
|
|||
|
||||
expect(mockSummarySearchClient.search.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"",
|
||||
"",
|
||||
Object {
|
||||
"direction": "asc",
|
||||
|
@ -128,6 +129,7 @@ describe('FindSLO', () => {
|
|||
expect(mockSummarySearchClient.search.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"slo.name:'Service*' and slo.indicator.type:'sli.kql.custom'",
|
||||
"",
|
||||
Object {
|
||||
"direction": "asc",
|
||||
"field": "error_budget_consumed",
|
||||
|
|
|
@ -24,6 +24,7 @@ export class FindSLO {
|
|||
public async execute(params: FindSLOParams): Promise<FindSLOResponse> {
|
||||
const sloSummaryList = await this.summarySearchClient.search(
|
||||
params.kqlQuery ?? '',
|
||||
params.filters ?? '',
|
||||
toSort(params),
|
||||
toPagination(params)
|
||||
);
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('Summary Search Client', () => {
|
|||
it('returns an empty response on error', async () => {
|
||||
esClientMock.count.mockRejectedValue(new Error('Cannot reach es'));
|
||||
|
||||
await expect(service.search('', defaultSort, defaultPagination)).resolves
|
||||
await expect(service.search('', '', defaultSort, defaultPagination)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
|
@ -53,7 +53,7 @@ describe('Summary Search Client', () => {
|
|||
_shards: { failed: 0, successful: 1, total: 1 },
|
||||
});
|
||||
|
||||
await expect(service.search('', defaultSort, defaultPagination)).resolves
|
||||
await expect(service.search('', '', defaultSort, defaultPagination)).resolves
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
|
@ -99,7 +99,7 @@ describe('Summary Search Client', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const results = await service.search('', defaultSort, defaultPagination);
|
||||
const results = await service.search('', '', defaultSort, defaultPagination);
|
||||
|
||||
expect(esClientMock.deleteByQuery).toHaveBeenCalled();
|
||||
expect(esClientMock.deleteByQuery.mock.calls[0]).toMatchSnapshot();
|
||||
|
|
|
@ -44,7 +44,12 @@ export interface Sort {
|
|||
}
|
||||
|
||||
export interface SummarySearchClient {
|
||||
search(kqlQuery: string, sort: Sort, pagination: Pagination): Promise<Paginated<SLOSummary>>;
|
||||
search(
|
||||
kqlQuery: string,
|
||||
filters: string,
|
||||
sort: Sort,
|
||||
pagination: Pagination
|
||||
): Promise<Paginated<SLOSummary>>;
|
||||
}
|
||||
|
||||
export class DefaultSummarySearchClient implements SummarySearchClient {
|
||||
|
@ -56,16 +61,29 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
|
||||
async search(
|
||||
kqlQuery: string,
|
||||
filters: string,
|
||||
sort: Sort,
|
||||
pagination: Pagination
|
||||
): Promise<Paginated<SLOSummary>> {
|
||||
let parsedFilters: any = {};
|
||||
|
||||
try {
|
||||
parsedFilters = JSON.parse(filters);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to parse filters: ${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const summarySearch = await this.esClient.search<EsSummaryDocument>({
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
track_total_hits: true,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ term: { spaceId: this.spaceId } }, getElastichsearchQueryOrThrow(kqlQuery)],
|
||||
filter: [
|
||||
{ term: { spaceId: this.spaceId } },
|
||||
getElastichsearchQueryOrThrow(kqlQuery),
|
||||
...(parsedFilters.filter ?? []),
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ApplicationStart, ChromeBreadcrumb, ChromeStart } from '@kbn/core/public';
|
||||
import { MouseEvent, useEffect } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ChromeBreadcrumbsAppendExtension } from '@kbn/core-chrome-browser';
|
||||
import { useQueryParams } from './use_query_params';
|
||||
|
||||
function addClickHandlers(
|
||||
|
@ -36,13 +37,14 @@ function getTitleFromBreadCrumbs(breadcrumbs: ChromeBreadcrumb[]) {
|
|||
|
||||
export const useBreadcrumbs = (
|
||||
extraCrumbs: ChromeBreadcrumb[],
|
||||
app?: { id: string; label: string }
|
||||
app?: { id: string; label: string },
|
||||
breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension
|
||||
) => {
|
||||
const params = useQueryParams();
|
||||
|
||||
const {
|
||||
services: {
|
||||
chrome: { docTitle, setBreadcrumbs },
|
||||
chrome: { docTitle, setBreadcrumbs, setBreadcrumbsAppendExtension },
|
||||
application: { getUrlForApp, navigateToUrl },
|
||||
},
|
||||
} = useKibana<{
|
||||
|
@ -52,6 +54,17 @@ export const useBreadcrumbs = (
|
|||
const setTitle = docTitle.change;
|
||||
const appPath = getUrlForApp(app?.id ?? 'observability-overview') ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
if (breadcrumbsAppendExtension) {
|
||||
setBreadcrumbsAppendExtension(breadcrumbsAppendExtension);
|
||||
}
|
||||
return () => {
|
||||
if (breadcrumbsAppendExtension) {
|
||||
setBreadcrumbsAppendExtension(undefined);
|
||||
}
|
||||
};
|
||||
}, [breadcrumbsAppendExtension, setBreadcrumbsAppendExtension]);
|
||||
|
||||
useEffect(() => {
|
||||
const breadcrumbs = [
|
||||
{
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
"@kbn/shared-ux-error-boundary",
|
||||
"@kbn/management-settings-field-definition",
|
||||
"@kbn/management-settings-types",
|
||||
"@kbn/management-settings-utilities"
|
||||
"@kbn/management-settings-utilities",
|
||||
"@kbn/core-chrome-browser"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -29019,7 +29019,6 @@
|
|||
"xpack.observability.slo.duration.minutely": "Par minute",
|
||||
"xpack.observability.slo.duration.monthly": "Mensuel",
|
||||
"xpack.observability.slo.duration.weekly": "Hebdomadaire",
|
||||
"xpack.observability.slo.feedbackButtonLabel": "Dites-nous ce que vous pensez !",
|
||||
"xpack.observability.slo.globalDiagnosis.errorNotification": "Vous ne disposez pas des autorisations nécessaires pour utiliser cette fonctionnalité.",
|
||||
"xpack.observability.slo.indicators.apmAvailability": "Disponibilité APM",
|
||||
"xpack.observability.slo.indicators.apmLatency": "Latence APM",
|
||||
|
@ -29199,7 +29198,6 @@
|
|||
"xpack.observability.slo.sloEdit.timeWindowDuration.tooltip": "La durée de la fenêtre temporelle utilisée pour calculer le SLO.",
|
||||
"xpack.observability.slo.sloEdit.timeWindowType.label": "Fenêtre temporelle",
|
||||
"xpack.observability.slo.sloEdit.timeWindowType.tooltip": "Choisissez entre une fenêtre glissante ou alignée sur le calendrier.",
|
||||
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "Créer un nouveau SLO",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "Créer un SLO",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "Pour commencer, créez votre premier SLO.",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.learnMore": "Envie d'en savoir plus ?",
|
||||
|
|
|
@ -29020,7 +29020,6 @@
|
|||
"xpack.observability.slo.duration.minutely": "毎分",
|
||||
"xpack.observability.slo.duration.monthly": "月ごと",
|
||||
"xpack.observability.slo.duration.weekly": "週ごと",
|
||||
"xpack.observability.slo.feedbackButtonLabel": "ご意見をお聞かせください。",
|
||||
"xpack.observability.slo.globalDiagnosis.errorNotification": "この機能を使用する権限がありません。",
|
||||
"xpack.observability.slo.indicators.apmAvailability": "APM可用性",
|
||||
"xpack.observability.slo.indicators.apmLatency": "APMレイテンシ",
|
||||
|
@ -29200,7 +29199,6 @@
|
|||
"xpack.observability.slo.sloEdit.timeWindowDuration.tooltip": "SLOを計算するために使用される時間枠期間。",
|
||||
"xpack.observability.slo.sloEdit.timeWindowType.label": "時間枠",
|
||||
"xpack.observability.slo.sloEdit.timeWindowType.tooltip": "ローリング時間枠とカレンダー時間枠のどちらかを選択します。",
|
||||
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "新規SLOを作成",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "SLOの作成",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "開始するには、まずSLOを作成します。",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.learnMore": "詳細について",
|
||||
|
|
|
@ -29004,7 +29004,6 @@
|
|||
"xpack.observability.slo.duration.minutely": "每分钟",
|
||||
"xpack.observability.slo.duration.monthly": "每月",
|
||||
"xpack.observability.slo.duration.weekly": "每周",
|
||||
"xpack.observability.slo.feedbackButtonLabel": "告诉我们您的看法!",
|
||||
"xpack.observability.slo.globalDiagnosis.errorNotification": "您没有适当权限,无法使用此功能。",
|
||||
"xpack.observability.slo.indicators.apmAvailability": "APM 可用性",
|
||||
"xpack.observability.slo.indicators.apmLatency": "APM 延迟",
|
||||
|
@ -29184,7 +29183,6 @@
|
|||
"xpack.observability.slo.sloEdit.timeWindowDuration.tooltip": "用于在其间计算 SLO 的时间窗口持续时间。",
|
||||
"xpack.observability.slo.sloEdit.timeWindowType.label": "时间窗口",
|
||||
"xpack.observability.slo.sloEdit.timeWindowType.tooltip": "选择滚动或日历对齐窗口。",
|
||||
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "创建新 SLO",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "创建 SLO",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "要开始使用,请创建您的首个 SLO。",
|
||||
"xpack.observability.slo.sloList.welcomePrompt.learnMore": "希望了解详情?",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue