mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SLO] Group filters in the Overview embeddable (#179620)
Fixes https://github.com/elastic/kibana/issues/179908 Fixes https://github.com/elastic/kibana/issues/1798952924e619
-9080-4c80-b5c3-7066a100026c ## UI changes ### SLO Overview configuration <img width="600" alt="Screenshot 2024-04-15 at 13 34 21" src="3635c9e7
-e5c1-454b-bb60-93b5d637083e"> ### Rendered panel with Group of SLOs <img width="1022" alt="Screenshot 2024-04-03 at 13 08 11" src="377c5cd0
-b6e0-4906-888b-93893a5f1208"> ### Updated SLO list group by page <img width="1316" alt="Screenshot 2024-03-28 at 23 56 12" src="d6b306a0
-77e8-44e5-b103-ce3537b0ceda"> ## Responsiveness This PR makes sure that the UI doesn't look broken in smaller screen resolutions and fixes following issues. If we want to display things differently in smaller screen resolutions, we should handle in a follow up PR (cc @maciejforcone) ### Before #### SLO list page in smaller screen resolution <img width="500" alt="Screenshot 2024-04-03 at 10 14 31" src="f2b11408
-3356-4f4b-9c4b-ec085ff494fa"> #### Rendered panel with Group of SLOs in smaller screen resolution <img width="500" alt="Screenshot 2024-04-03 at 10 13 18" src="6e37ad61
-5a0a-4027-8b90-4f8fad243637"> #### After #### SLO list page in smaller screen resolution <img width="500" alt="Screenshot 2024-04-03 at 11 04 35" src="fa875aa8
-bc54-4c6b-9159-4462e585a9cb"> #### Rendered panel with Group of SLOs in smaller screen resolution I applied a `min-width` of 100px to the SLO name, so that it doesn't look broken. What is not visible in the screenshot is a horizontal scrollbar, so user can still see the part with Worst performing that is not visible. <img width="500" alt="Screenshot 2024-04-03 at 10 13 47" src="d9566ca3
-42af-4d85-a3f7-6374cc27981d"> ## Acceptance criteria - Overview configuration - toggles between Single SLO or Grouped SLOs - Required field for the `Grouped SLOs` view is only the `Group by` option, the rest filters are optional - Users can optionally select the groups they want - Users can use the KQL bar to apply extra filters - Overview configuration opens in a `Flyout` (modal was causing some issues with z-index and kqlbar suggestions being displayed below the overlay) - Embeddable panel - The rendered panel shows the selected groups with summary data divided with a horizontal like and no border around it - Clicking on a group opens an accordion similar to the SLO list group by page - An Edit criteria link is visible only in the Grouped Embeddable panel - Clicking on the Edit criteria link opens the configuration modal, showing only the grouped tab and hiding the tabbed button group - The grouped embeddable should react to Dashboard's Refresh button - `Add to Dashboard` action should be hidden in Dashboard context --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com>
This commit is contained in:
parent
186d9cc5e7
commit
081f4e44ff
31 changed files with 1286 additions and 237 deletions
|
@ -123,6 +123,7 @@ const findSLOGroupsParamsSchema = t.partial({
|
|||
page: t.string,
|
||||
perPage: t.string,
|
||||
groupBy: groupBySchema,
|
||||
groupsFilter: t.union([t.array(t.string), t.string]),
|
||||
kqlQuery: t.string,
|
||||
filters: t.string,
|
||||
}),
|
||||
|
|
|
@ -25,8 +25,8 @@ export const paths = {
|
|||
`${SLOS_BASE_PATH}${SLOS_PATH}/edit/${encodeURIComponent(sloId)}?_a=${encodedParams}`,
|
||||
sloDetails: (sloId: string, instanceId?: string) =>
|
||||
!!instanceId
|
||||
? `${SLOS_BASE_PATH}${SLOS_PATH}/${encodeURIComponent(sloId)}?instanceId=${encodeURIComponent(
|
||||
? `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}?instanceId=${encodeURIComponent(
|
||||
instanceId
|
||||
)}`
|
||||
: `${SLOS_BASE_PATH}${SLOS_PATH}/${encodeURIComponent(sloId)}`,
|
||||
: `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}`,
|
||||
};
|
||||
|
|
|
@ -19,4 +19,10 @@ export interface PluginContextValue {
|
|||
experimentalFeatures?: ExperimentalFeatures;
|
||||
}
|
||||
|
||||
export const PluginContext = createContext({} as PluginContextValue);
|
||||
export interface OverviewEmbeddableContextValue {
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
|
||||
}
|
||||
|
||||
export const PluginContext = createContext(
|
||||
{} as PluginContextValue | OverviewEmbeddableContextValue
|
||||
);
|
||||
|
|
|
@ -8,3 +8,4 @@ import { AlertConsumers } from '@kbn/rule-data-utils';
|
|||
export const SLO_ALERTS_EMBEDDABLE = 'SLO_ALERTS_EMBEDDABLE';
|
||||
|
||||
export const SLO_ALERTS_TABLE_CONFIG_ID = `${AlertConsumers.SLO}-embeddable-alerts-table`;
|
||||
export const SLO_EMBEDDABLE = 'SLO_EMBEDDABLE';
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SLOView } from '../../../../pages/slos/components/toggle_slo_view';
|
||||
import { SloEmbeddableInput } from '../types';
|
||||
import { GroupView } from '../../../../pages/slos/components/grouped_slos/group_view';
|
||||
import { buildCombinedKqlQuery } from './helpers/build_kql_query';
|
||||
|
||||
interface Props {
|
||||
groupBy: string;
|
||||
groups?: string[];
|
||||
kqlQuery?: string;
|
||||
sloView: SLOView;
|
||||
sort?: string;
|
||||
filters?: Filter[];
|
||||
reloadGroupSubject: Subject<SloEmbeddableInput | undefined>;
|
||||
}
|
||||
|
||||
export function GroupSloView({
|
||||
sloView,
|
||||
groupBy: initialGroupBy = 'status',
|
||||
groups: initialGroups = [],
|
||||
kqlQuery: initialKqlQuery = '',
|
||||
filters: initialFilters = [],
|
||||
reloadGroupSubject,
|
||||
}: Props) {
|
||||
const [lastRefreshTime, setLastRefreshTime] = useState<number | undefined>(undefined);
|
||||
const [groupBy, setGroupBy] = useState(initialGroupBy);
|
||||
const [kqlQuery, setKqlQuery] = useState(initialKqlQuery);
|
||||
const [filters, setFilters] = useState(initialFilters);
|
||||
const [groups, setGroups] = useState(initialGroups);
|
||||
const combinedKqlQuery = buildCombinedKqlQuery({ groups, groupBy, kqlQuery });
|
||||
|
||||
useEffect(() => {
|
||||
const subs = reloadGroupSubject?.subscribe((input) => {
|
||||
if (input) {
|
||||
const nGroupBy = input?.groupFilters?.groupBy ?? groupBy;
|
||||
setGroupBy(nGroupBy);
|
||||
|
||||
const nKqlInput = input?.groupFilters?.kqlQuery ?? kqlQuery;
|
||||
setKqlQuery(nKqlInput);
|
||||
|
||||
const nFilters = input?.groupFilters?.filters ?? filters;
|
||||
setFilters(nFilters);
|
||||
|
||||
const nGroups = input?.groupFilters?.groups ?? groups;
|
||||
setGroups(nGroups);
|
||||
}
|
||||
setLastRefreshTime(Date.now());
|
||||
});
|
||||
return () => {
|
||||
subs?.unsubscribe();
|
||||
};
|
||||
}, [filters, groupBy, groups, kqlQuery, reloadGroupSubject]);
|
||||
return (
|
||||
<GroupView
|
||||
sloView={sloView}
|
||||
groupBy={groupBy}
|
||||
groupsFilter={groups}
|
||||
kqlQuery={combinedKqlQuery}
|
||||
filters={filters}
|
||||
lastRefreshTime={lastRefreshTime}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`buildCombinedKqlQuery should generate correct es query for no selected groups, no kql 1`] = `""`;
|
||||
|
||||
exports[`buildCombinedKqlQuery should generate correct es query for no selected groups, with kql 1`] = `"status:\\"VIOLATED\\""`;
|
||||
|
||||
exports[`buildCombinedKqlQuery should generate correct es query for selected groups, no kql 1`] = `"(slo.tags:\\"production\\" or slo.tags:\\"dev\\")"`;
|
||||
|
||||
exports[`buildCombinedKqlQuery should generate correct es query for selected groups, with kql 1`] = `"(slo.tags:\\"production\\" or slo.tags:\\"dev\\") and status:\\"VIOLATED\\""`;
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { buildCombinedKqlQuery } from './build_kql_query';
|
||||
|
||||
describe('buildCombinedKqlQuery', () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'no selected groups, with kql',
|
||||
props: {
|
||||
groups: [],
|
||||
groupBy: 'slo.tags',
|
||||
kqlQuery: 'status:"VIOLATED"',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'selected groups, with kql',
|
||||
props: {
|
||||
groups: ['production', 'dev'],
|
||||
groupBy: 'slo.tags',
|
||||
kqlQuery: 'status:"VIOLATED"',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'selected groups, no kql',
|
||||
props: {
|
||||
groups: ['production', 'dev'],
|
||||
groupBy: 'slo.tags',
|
||||
kqlQuery: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'no selected groups, no kql',
|
||||
props: {
|
||||
groups: [],
|
||||
groupBy: 'slo.tags',
|
||||
kqlQuery: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testData)('should generate correct es query for $title', ({ props }) => {
|
||||
expect(buildCombinedKqlQuery(props)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
kqlQuery: string;
|
||||
groups: string[];
|
||||
groupBy: string;
|
||||
}
|
||||
|
||||
export const buildCombinedKqlQuery = ({ groups, groupBy, kqlQuery }: Props) => {
|
||||
let groupsKqlQuery = '';
|
||||
if (groups.length > 0) {
|
||||
groupsKqlQuery += `(`;
|
||||
|
||||
groups.map((group, index) => {
|
||||
const shouldAddOr = index < groups.length - 1;
|
||||
groupsKqlQuery += `${groupBy}:"${group}"`;
|
||||
if (shouldAddOr) {
|
||||
groupsKqlQuery += ' or ';
|
||||
}
|
||||
});
|
||||
groupsKqlQuery += `)`;
|
||||
}
|
||||
|
||||
let combinedKqlQuery = '';
|
||||
if (kqlQuery && groupsKqlQuery) {
|
||||
combinedKqlQuery = `${groupsKqlQuery} and ${kqlQuery}`;
|
||||
} else if (groupsKqlQuery) {
|
||||
combinedKqlQuery = groupsKqlQuery;
|
||||
} else if (kqlQuery) {
|
||||
combinedKqlQuery = kqlQuery;
|
||||
}
|
||||
|
||||
return combinedKqlQuery;
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
.sloOverviewEmbeddable .uniSearchBar {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* 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 './slo_group_filters.scss';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { debounce } from 'lodash';
|
||||
import { EuiFormRow, EuiComboBox, EuiSelect, EuiComboBoxOptionOption, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useFetchSloGroups } from '../../../../hooks/use_fetch_slo_groups';
|
||||
import { SLI_OPTIONS } from '../../../../pages/slo_edit/constants';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { useCreateDataView } from '../../../../hooks/use_create_data_view';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_NAME } from '../../../../../common/constants';
|
||||
import { sloAppId } from '../../../../../common';
|
||||
import type { GroupFilters, GroupBy } from '../types';
|
||||
interface Option {
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const groupByOptions: Option[] = [
|
||||
{
|
||||
text: i18n.translate('xpack.slo.sloGroupConfiguration.groupBy.tags', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
value: 'slo.tags',
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.slo.sloGroupConfiguration.groupBy.status', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
value: 'status',
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.slo.sloGroupConfiguration.groupBy.sliType', {
|
||||
defaultMessage: 'SLI type',
|
||||
}),
|
||||
value: 'slo.indicator.type',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
onSelected: (prop: string, value: string | Array<string | undefined> | Filter[]) => void;
|
||||
selectedFilters: GroupFilters;
|
||||
}
|
||||
|
||||
const mapGroupsToOptions = (groups: string[] | undefined, selectedGroupBy: string) =>
|
||||
groups?.map((group) => {
|
||||
let label = group;
|
||||
if (selectedGroupBy === 'status') {
|
||||
label = group.toLowerCase();
|
||||
} else if (selectedGroupBy === 'slo.indicator.type') {
|
||||
label = SLI_OPTIONS.find((option) => option.value === group)!.text ?? group;
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value: group,
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
export function SloGroupFilters({ selectedFilters, onSelected }: Props) {
|
||||
const {
|
||||
unifiedSearch: {
|
||||
ui: { SearchBar },
|
||||
},
|
||||
} = useKibana().services;
|
||||
const { dataView } = useCreateDataView({
|
||||
indexPatternString: SLO_SUMMARY_DESTINATION_INDEX_NAME,
|
||||
});
|
||||
const [selectedGroupBy, setSelectedGroupBy] =
|
||||
useState<GroupBy>(selectedFilters.groupBy) ?? 'status';
|
||||
const [filters, setFilters] = useState(selectedFilters.filters) ?? [];
|
||||
const [kqlQuery, setkqlQuery] = useState(selectedFilters.kqlQuery);
|
||||
const [selectedGroupByLabel, setSelectedGroupByLabel] = useState('Status');
|
||||
const [groupOptions, setGroupOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
|
||||
const [selectedGroupOptions, setSelectedGroupOptions] = useState<
|
||||
Array<EuiComboBoxOptionOption<string>>
|
||||
>(mapGroupsToOptions(selectedFilters.groups, selectedGroupBy));
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const sliTypeSearch = SLI_OPTIONS.find((option) =>
|
||||
option.text.toLowerCase().includes(searchValue.toLowerCase())
|
||||
)?.value;
|
||||
const query = `${searchValue}*`;
|
||||
|
||||
const { data, isLoading } = useFetchSloGroups({
|
||||
perPage: 100,
|
||||
groupBy: selectedGroupBy,
|
||||
kqlQuery: `slo.tags: (${query}) or status: (${query.toUpperCase()}) or slo.indicator.type: (${sliTypeSearch})`,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const isLoadedWithData = !isLoading && data?.results !== undefined;
|
||||
const opts: Array<EuiComboBoxOptionOption<string>> = isLoadedWithData
|
||||
? mapGroupsToOptions(
|
||||
data?.results.map((item) => item.group),
|
||||
selectedGroupBy
|
||||
)
|
||||
: [];
|
||||
setGroupOptions(opts);
|
||||
|
||||
if (selectedGroupBy === 'slo.tags') {
|
||||
setSelectedGroupByLabel(
|
||||
i18n.translate('xpack.slo.sloGroupConfiguration.tagsLabel', {
|
||||
defaultMessage: 'Tags',
|
||||
})
|
||||
);
|
||||
} else if (selectedGroupBy === 'status') {
|
||||
setSelectedGroupByLabel(
|
||||
i18n.translate('xpack.slo.sloGroupConfiguration.statusLabel', {
|
||||
defaultMessage: 'Status',
|
||||
})
|
||||
);
|
||||
} else if (selectedGroupBy === 'slo.indicator.type') {
|
||||
setSelectedGroupByLabel(
|
||||
i18n.translate('xpack.slo.sloGroupConfiguration.sliTypeLabel', {
|
||||
defaultMessage: 'SLI type',
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [isLoading, data, selectedGroupBy]);
|
||||
|
||||
const onChange = (opts: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setSelectedGroupOptions(opts);
|
||||
const selectedGroups = opts.length >= 1 ? opts.map((opt) => opt.value) : [];
|
||||
onSelected('groups', selectedGroups);
|
||||
};
|
||||
|
||||
const onSearchChange = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.slo.sloGroupConfiguration.groupTypeLabel', {
|
||||
defaultMessage: 'Group by',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="o11ySloGroupConfigurationSelect"
|
||||
options={groupByOptions}
|
||||
value={selectedGroupBy}
|
||||
onChange={(e) => {
|
||||
setSelectedGroupBy(e.target.value as GroupBy);
|
||||
onSelected('groupBy', e.target.value);
|
||||
setSelectedGroupOptions([]);
|
||||
onSelected('groups', []);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.slo.sloGroupConfiguration.groupByLabel', {
|
||||
defaultMessage: '{ selectedGroupByLabel }',
|
||||
values: { selectedGroupByLabel },
|
||||
})}
|
||||
labelAppend={
|
||||
<EuiText color="subdued" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloGroupConfiguration.groupsOptional"
|
||||
defaultMessage="Optional"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
aria-label={i18n.translate('xpack.slo.sloGroupConfiguration.sloSelector.ariaLabel', {
|
||||
defaultMessage: '{ selectedGroupByLabel }',
|
||||
values: { selectedGroupByLabel },
|
||||
})}
|
||||
placeholder={i18n.translate('xpack.slo.sloGroupConfiguration.sloSelector.placeholder', {
|
||||
defaultMessage: 'Select a {selectedGroupByLabel}',
|
||||
values: { selectedGroupByLabel },
|
||||
})}
|
||||
data-test-subj="sloGroup"
|
||||
options={groupOptions}
|
||||
selectedOptions={selectedGroupOptions}
|
||||
async
|
||||
onChange={onChange}
|
||||
onSearchChange={onSearchChange}
|
||||
fullWidth
|
||||
singleSelection={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.slo.sloGroupConfiguration.customFiltersLabel', {
|
||||
defaultMessage: 'Custom filter',
|
||||
})}
|
||||
labelAppend={
|
||||
<EuiText color="subdued" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloGroupConfiguration.customFiltersOptional"
|
||||
defaultMessage="Optional"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<SearchBar
|
||||
appName={sloAppId}
|
||||
placeholder={PLACEHOLDER}
|
||||
indexPatterns={dataView ? [dataView] : []}
|
||||
showSubmitButton={false}
|
||||
showFilterBar={true}
|
||||
filters={filters}
|
||||
onFiltersUpdated={(newFilters) => {
|
||||
setFilters(newFilters);
|
||||
onSelected('filters', newFilters);
|
||||
}}
|
||||
onQuerySubmit={({ query: value }) => {
|
||||
setkqlQuery(String(value?.query));
|
||||
onSelected('kqlQuery', String(value?.query));
|
||||
}}
|
||||
onQueryChange={({ query: value }) => {
|
||||
setkqlQuery(String(value?.query));
|
||||
onSelected('kqlQuery', String(value?.query));
|
||||
}}
|
||||
query={{ query: String(kqlQuery), language: 'kuery' }}
|
||||
showDatePicker={false}
|
||||
disableQueryLanguageSwitcher={true}
|
||||
saveQueryMenuVisibility="globally_managed"
|
||||
onClearSavedQuery={() => {}}
|
||||
showQueryInput={true}
|
||||
onSavedQueryUpdated={(savedQuery) => {
|
||||
setFilters(savedQuery.attributes.filters);
|
||||
setkqlQuery(String(savedQuery.attributes.query.query));
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PLACEHOLDER = i18n.translate('xpack.slo.sloGroupConfiguration.customFilterText', {
|
||||
defaultMessage: 'Custom filter',
|
||||
});
|
|
@ -24,7 +24,7 @@ export async function resolveEmbeddableSloUserInput(
|
|||
const queryClient = new QueryClient();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const modalSession = overlays.openModal(
|
||||
const flyoutSession = overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
|
@ -34,12 +34,13 @@ export async function resolveEmbeddableSloUserInput(
|
|||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SloConfiguration
|
||||
initialInput={input}
|
||||
onCreate={(update: EmbeddableSloProps) => {
|
||||
modalSession.close();
|
||||
flyoutSession.close();
|
||||
resolve(update);
|
||||
}}
|
||||
onCancel={() => {
|
||||
modalSession.close();
|
||||
flyoutSession.close();
|
||||
reject();
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButtonGroup, type EuiButtonGroupOptionProps } from '@elastic/eui';
|
||||
import { OverviewMode } from './types';
|
||||
|
||||
const overviewModeOptions: EuiButtonGroupOptionProps[] = [
|
||||
{
|
||||
id: `single`,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="xpack.slo.overviewEmbeddable.typeSelector.singleSLOLabel"
|
||||
defaultMessage="Single SLO"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: `groups`,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="xpack.slo.overviewEmbeddable.typeSelector.groupSLOLabel"
|
||||
defaultMessage="Grouped SLOs"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export interface OverviewModeSelectorProps {
|
||||
value: string;
|
||||
onChange: (update: OverviewMode) => void;
|
||||
}
|
||||
|
||||
export function OverviewModeSelector({ value, onChange }: OverviewModeSelectorProps) {
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
legend="This is a basic group"
|
||||
options={overviewModeOptions}
|
||||
idSelected={value}
|
||||
onChange={onChange as (id: string) => void}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -7,100 +7,232 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSwitch,
|
||||
EuiSpacer,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SloSelector } from '../alerts/slo_selector';
|
||||
import type { EmbeddableSloProps } from './types';
|
||||
|
||||
import type {
|
||||
SingleSloProps,
|
||||
GroupSloProps,
|
||||
SloEmbeddableInput,
|
||||
GroupFilters,
|
||||
OverviewMode,
|
||||
} from './types';
|
||||
import { SloGroupFilters } from './group_view/slo_group_filters';
|
||||
import { OverviewModeSelector } from './overview_mode_selector';
|
||||
|
||||
interface SloConfigurationProps {
|
||||
onCreate: (props: EmbeddableSloProps) => void;
|
||||
initialInput?: Partial<SloEmbeddableInput> | undefined;
|
||||
onCreate: (props: SingleSloProps | GroupSloProps) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function SloConfiguration({ onCreate, onCancel }: SloConfigurationProps) {
|
||||
const [selectedSlo, setSelectedSlo] = useState<EmbeddableSloProps>();
|
||||
interface SingleConfigurationProps {
|
||||
onCreate: (props: SingleSloProps) => void;
|
||||
onCancel: () => void;
|
||||
overviewMode: OverviewMode;
|
||||
}
|
||||
|
||||
interface GroupConfigurationProps {
|
||||
onCreate: (props: GroupSloProps) => void;
|
||||
onCancel: () => void;
|
||||
overviewMode: OverviewMode;
|
||||
initialInput?: GroupSloProps;
|
||||
}
|
||||
|
||||
function SingleSloConfiguration({ overviewMode, onCreate, onCancel }: SingleConfigurationProps) {
|
||||
const [selectedSlo, setSelectedSlo] = useState<SingleSloProps>();
|
||||
const [showAllGroupByInstances, setShowAllGroupByInstances] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const hasGroupBy = selectedSlo && selectedSlo.sloInstanceId !== ALL_VALUE;
|
||||
|
||||
const onConfirmClick = () =>
|
||||
onCreate({
|
||||
showAllGroupByInstances,
|
||||
sloId: selectedSlo?.sloId,
|
||||
sloInstanceId: selectedSlo?.sloInstanceId,
|
||||
overviewMode,
|
||||
});
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onCancel} style={{ minWidth: 550 }}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('xpack.slo.sloEmbeddable.config.sloSelector.headerTitle', {
|
||||
defaultMessage: 'SLO configuration',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow>
|
||||
<SloSelector
|
||||
singleSelection={true}
|
||||
hasError={hasError}
|
||||
onSelected={(slo) => {
|
||||
setHasError(slo === undefined);
|
||||
if (slo && 'id' in slo) {
|
||||
setSelectedSlo({ sloId: slo.id, sloInstanceId: slo.instanceId });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<SloSelector
|
||||
singleSelection={true}
|
||||
hasError={hasError}
|
||||
onSelected={(slo) => {
|
||||
setHasError(slo === undefined);
|
||||
if (slo && 'id' in slo) {
|
||||
setSelectedSlo({ sloId: slo.id, sloInstanceId: slo.instanceId });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{hasGroupBy && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.slo.sloConfiguration.euiSwitch.showAllGroupByLabel',
|
||||
{
|
||||
defaultMessage: 'Show all related group-by instances',
|
||||
}
|
||||
)}
|
||||
checked={showAllGroupByInstances}
|
||||
onChange={(e) => {
|
||||
setShowAllGroupByInstances(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{selectedSlo?.sloInstanceId !== ALL_VALUE && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.slo.sloConfiguration.euiSwitch.showAllGroupByLabel', {
|
||||
defaultMessage: 'Show all related group-by instances',
|
||||
})}
|
||||
checked={showAllGroupByInstances}
|
||||
onChange={(e) => {
|
||||
setShowAllGroupByInstances(e.target.checked);
|
||||
}}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiButtonEmpty onClick={onCancel} data-test-subj="sloCancelButton">
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloEmbeddable.config.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel} data-test-subj="sloCancelButton">
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloEmbeddable.config.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="sloConfirmButton"
|
||||
isDisabled={!selectedSlo || hasError}
|
||||
onClick={onConfirmClick}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.embeddableSlo.config.confirmButtonLabel"
|
||||
defaultMessage="Confirm configurations"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
<EuiButton
|
||||
data-test-subj="sloConfirmButton"
|
||||
isDisabled={!selectedSlo || hasError}
|
||||
onClick={onConfirmClick}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.overviewEmbeddableSlo.config.confirmButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupSloConfiguration({
|
||||
overviewMode,
|
||||
onCreate,
|
||||
onCancel,
|
||||
initialInput,
|
||||
}: GroupConfigurationProps) {
|
||||
const [selectedGroupFilters, setSelectedGroupFilters] = useState<GroupFilters>({
|
||||
groupBy: initialInput?.groupFilters.groupBy ?? 'status',
|
||||
filters: initialInput?.groupFilters.filters ?? [],
|
||||
kqlQuery: initialInput?.groupFilters.kqlQuery ?? '',
|
||||
groups: initialInput?.groupFilters.groups ?? [],
|
||||
});
|
||||
|
||||
const onConfirmClick = () =>
|
||||
onCreate({
|
||||
groupFilters: selectedGroupFilters,
|
||||
overviewMode,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutBody className="sloOverviewEmbeddable">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<SloGroupFilters
|
||||
selectedFilters={selectedGroupFilters}
|
||||
onSelected={(prop, value) => {
|
||||
setSelectedGroupFilters((prevState) => ({ ...prevState, [prop]: value }));
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiButtonEmpty onClick={onCancel} data-test-subj="sloCancelButton">
|
||||
<FormattedMessage
|
||||
id="xpack.slo.sloEmbeddable.config.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton data-test-subj="sloConfirmButton" onClick={onConfirmClick} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.slo.overviewEmbeddableSlo.config.confirmButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SloConfiguration({ initialInput, onCreate, onCancel }: SloConfigurationProps) {
|
||||
const [overviewMode, setOverviewMode] = useState<OverviewMode>(
|
||||
initialInput?.overviewMode ?? 'single'
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onCancel}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.slo.sloEmbeddable.config.sloSelector.headerTitle', {
|
||||
defaultMessage: 'SLO configuration',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{initialInput?.overviewMode === undefined && (
|
||||
<EuiFlexItem>
|
||||
<OverviewModeSelector
|
||||
value={overviewMode}
|
||||
onChange={(mode) => setOverviewMode(mode)}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
{overviewMode === 'groups' ? (
|
||||
<GroupSloConfiguration
|
||||
initialInput={initialInput as GroupSloProps}
|
||||
overviewMode={overviewMode}
|
||||
onCreate={onCreate}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : (
|
||||
<SingleSloConfiguration
|
||||
overviewMode={overviewMode}
|
||||
onCreate={onCreate}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* 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 { nextTick } from '@kbn/test-jest-helpers';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { SLOEmbeddable, SloEmbeddableDeps } from './slo_embeddable';
|
||||
import type { SloEmbeddableInput, OverviewMode, GroupFilters } from './types';
|
||||
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import { applicationServiceMock } from '@kbn/core/public/mocks';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '@kbn/observability-plugin/public';
|
||||
import { SloOverview } from './slo_overview';
|
||||
import { useFetchSloDetails } from '../../../hooks/use_fetch_slo_details';
|
||||
import { useFetchActiveAlerts } from '../../../hooks/use_fetch_active_alerts';
|
||||
import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary';
|
||||
import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo';
|
||||
|
||||
import { ActiveAlerts } from '../../../hooks/active_alerts';
|
||||
import { buildSlo } from '../../../data/slo/slo';
|
||||
import { historicalSummaryData } from '../../../data/slo/historical_summary_data';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
|
||||
import { GroupListView } from '../../../pages/slos/components/grouped_slos/group_list_view';
|
||||
import { render } from 'react-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
let mockWrapper: ReactWrapper;
|
||||
|
||||
jest.mock('react-dom', () => {
|
||||
const { mount } = jest.requireActual('enzyme');
|
||||
return {
|
||||
...jest.requireActual('react-dom'),
|
||||
render: jest.fn((component: ReactElement) => {
|
||||
mockWrapper = mount(component);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../hooks/use_fetch_slo_details');
|
||||
jest.mock('../../../hooks/use_fetch_active_alerts');
|
||||
jest.mock('../../../hooks/use_fetch_historical_summary');
|
||||
jest.mock('../../../hooks/use_fetch_rules_for_slo');
|
||||
|
||||
const useFetchSloDetailsMock = useFetchSloDetails as jest.Mock;
|
||||
const useFetchActiveAlertsMock = useFetchActiveAlerts as jest.Mock;
|
||||
const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock;
|
||||
const useFetchRulesForSloMock = useFetchRulesForSlo as jest.Mock;
|
||||
|
||||
function createSloEmbeddableDepsMock(): SloEmbeddableDeps {
|
||||
return {
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
uiSettings: uiSettingsServiceMock.createSetupContract(),
|
||||
i18n: i18nServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
notifications: notificationServiceMock.createStartContract(),
|
||||
http: httpServiceMock.createStartContract(),
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
observability: {
|
||||
config: {
|
||||
unsafe: {
|
||||
alertDetails: {
|
||||
observability: { enabled: true },
|
||||
metrics: { enabled: false },
|
||||
uptime: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
useRulesLink: () => ({ href: 'newRuleLink' }),
|
||||
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const waitOneTick = () => act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
||||
|
||||
describe('SLO Overview embeddable', () => {
|
||||
let mountpoint: HTMLDivElement;
|
||||
let depsMock: jest.Mocked<SloEmbeddableDeps>;
|
||||
const createEmbeddable = ({
|
||||
overviewMode,
|
||||
sloId,
|
||||
sloInstanceId,
|
||||
groupFilters,
|
||||
}: {
|
||||
overviewMode: OverviewMode;
|
||||
sloId?: string;
|
||||
sloInstanceId?: string;
|
||||
groupFilters?: GroupFilters;
|
||||
}) => {
|
||||
const baseInput: SloEmbeddableInput = {
|
||||
id: 'mock-embeddable-id',
|
||||
overviewMode,
|
||||
};
|
||||
let initialInput = baseInput;
|
||||
initialInput =
|
||||
overviewMode === 'single'
|
||||
? {
|
||||
...baseInput,
|
||||
sloId,
|
||||
sloInstanceId,
|
||||
}
|
||||
: {
|
||||
...baseInput,
|
||||
groupFilters,
|
||||
};
|
||||
return new SLOEmbeddable(depsMock, initialInput);
|
||||
};
|
||||
beforeEach(() => {
|
||||
mountpoint = document.createElement('div');
|
||||
depsMock = createSloEmbeddableDepsMock() as unknown as jest.Mocked<SloEmbeddableDeps>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mountpoint.remove();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should render single Overview', async () => {
|
||||
const slo = buildSlo();
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, data: slo, refetch: () => {} });
|
||||
useFetchActiveAlertsMock.mockReturnValue({ isLoading: false, data: new ActiveAlerts() });
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: historicalSummaryData,
|
||||
});
|
||||
useFetchRulesForSloMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: [],
|
||||
});
|
||||
const embeddable = createEmbeddable({
|
||||
overviewMode: 'single',
|
||||
sloId: 'sloId',
|
||||
sloInstanceId: 'sloInstanceId',
|
||||
});
|
||||
await waitOneTick();
|
||||
expect(render).toHaveBeenCalledTimes(0);
|
||||
embeddable.render(mountpoint);
|
||||
expect(render).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockWrapper.find(SloOverview).exists()).toBe(true);
|
||||
expect(mockWrapper.find(GroupListView).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show SLOs grouped by tags', async () => {
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
mockWrapper.unmount();
|
||||
mockWrapper.update();
|
||||
});
|
||||
const embeddable = createEmbeddable({
|
||||
overviewMode: 'groups',
|
||||
groupFilters: {
|
||||
groupBy: 'slo.tags',
|
||||
groups: ['production'],
|
||||
},
|
||||
});
|
||||
await waitOneTick();
|
||||
expect(render).toHaveBeenCalledTimes(0);
|
||||
|
||||
embeddable.render(mountpoint);
|
||||
mockWrapper.update();
|
||||
expect(render).toHaveBeenCalledTimes(1);
|
||||
expect(mockWrapper.find(SloOverview).exists()).toBe(false);
|
||||
});
|
||||
});
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { EuiFlexItem, EuiLink, EuiFlexGroup } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import {
|
||||
|
@ -22,29 +24,40 @@ import {
|
|||
ApplicationStart,
|
||||
NotificationsStart,
|
||||
} from '@kbn/core/public';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { ObservabilityPublicStart } from '@kbn/observability-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { PluginContext } from '../../../context/plugin_context';
|
||||
import { SloCardChartList } from './slo_overview_grid';
|
||||
import { SloOverview } from './slo_overview';
|
||||
import { GroupSloView } from './group_view/group_view';
|
||||
import type { SloEmbeddableInput } from './types';
|
||||
import { EDIT_SLO_OVERVIEW_ACTION } from '../../../ui_actions/edit_slo_overview_panel';
|
||||
|
||||
export const SLO_EMBEDDABLE = 'SLO_EMBEDDABLE';
|
||||
|
||||
interface SloEmbeddableDeps {
|
||||
export interface SloEmbeddableDeps {
|
||||
uiSettings: IUiSettingsClient;
|
||||
http: CoreStart['http'];
|
||||
i18n: CoreStart['i18n'];
|
||||
theme: CoreStart['theme'];
|
||||
application: ApplicationStart;
|
||||
notifications: NotificationsStart;
|
||||
observability: ObservabilityPublicStart;
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export class SLOEmbeddable extends AbstractEmbeddable<SloEmbeddableInput, EmbeddableOutput> {
|
||||
public readonly type = SLO_EMBEDDABLE;
|
||||
private node?: HTMLElement;
|
||||
private reloadSubject: Subject<boolean>;
|
||||
private reloadGroupSubject: Subject<SloEmbeddableInput | undefined>;
|
||||
private subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly deps: SloEmbeddableDeps,
|
||||
|
@ -53,13 +66,17 @@ export class SLOEmbeddable extends AbstractEmbeddable<SloEmbeddableInput, Embedd
|
|||
) {
|
||||
super(initialInput, {}, parent);
|
||||
this.reloadSubject = new Subject<boolean>();
|
||||
|
||||
this.reloadGroupSubject = new Subject<SloEmbeddableInput | undefined>();
|
||||
this.setTitle(
|
||||
this.input.title ||
|
||||
i18n.translate('xpack.slo.sloEmbeddable.displayTitle', {
|
||||
defaultMessage: 'SLO Overview',
|
||||
})
|
||||
);
|
||||
|
||||
this.subscription = this.getInput$().subscribe((input) => {
|
||||
this.reloadGroupSubject.next(input);
|
||||
});
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
|
@ -75,30 +92,82 @@ export class SLOEmbeddable extends AbstractEmbeddable<SloEmbeddableInput, Embedd
|
|||
this.node = node;
|
||||
// required for the export feature to work
|
||||
this.node.setAttribute('data-shared-item', '');
|
||||
|
||||
const { sloId, sloInstanceId, showAllGroupByInstances } = this.getInput();
|
||||
const { sloId, sloInstanceId, showAllGroupByInstances, overviewMode, groupFilters } =
|
||||
this.getInput();
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const { observabilityRuleTypeRegistry } = this.deps.observability;
|
||||
const I18nContext = this.deps.i18n.Context;
|
||||
const renderOverview = () => {
|
||||
if (overviewMode === 'groups') {
|
||||
const groupBy = groupFilters?.groupBy ?? 'status';
|
||||
const kqlQuery = groupFilters?.kqlQuery ?? '';
|
||||
const groups = groupFilters?.groups ?? [];
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
wrap
|
||||
css={`
|
||||
margin-bottom: 20px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
const trigger = this.deps.uiActions.getTrigger(CONTEXT_MENU_TRIGGER);
|
||||
this.deps.uiActions.getAction(EDIT_SLO_OVERVIEW_ACTION).execute({
|
||||
trigger,
|
||||
embeddable: this,
|
||||
} as ActionExecutionContext);
|
||||
}}
|
||||
data-test-subj="o11ySloAlertsWrapperSlOsIncludedLink"
|
||||
>
|
||||
{i18n.translate('xpack.slo.overviewEmbeddable.editCriteriaLabel', {
|
||||
defaultMessage: 'Edit criteria',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<GroupSloView
|
||||
sloView="cardView"
|
||||
groupBy={groupBy}
|
||||
groups={groups}
|
||||
kqlQuery={kqlQuery}
|
||||
filters={groupFilters?.filters}
|
||||
reloadGroupSubject={this.reloadGroupSubject}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</Wrapper>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SloOverview
|
||||
onRenderComplete={() => this.onRenderComplete()}
|
||||
sloId={sloId}
|
||||
sloInstanceId={sloInstanceId}
|
||||
reloadSubject={this.reloadSubject}
|
||||
showAllGroupByInstances={showAllGroupByInstances}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<Router history={createBrowserHistory()}>
|
||||
<EuiThemeProvider darkMode={true}>
|
||||
<KibanaThemeProvider theme={this.deps.theme}>
|
||||
<KibanaContextProvider services={this.deps}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{showAllGroupByInstances ? (
|
||||
<SloCardChartList sloId={sloId!} />
|
||||
) : (
|
||||
<SloOverview
|
||||
onRenderComplete={() => this.onRenderComplete()}
|
||||
sloId={sloId}
|
||||
sloInstanceId={sloInstanceId}
|
||||
reloadSubject={this.reloadSubject}
|
||||
showAllGroupByInstances={showAllGroupByInstances}
|
||||
/>
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
<PluginContext.Provider value={{ observabilityRuleTypeRegistry }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{showAllGroupByInstances ? (
|
||||
<SloCardChartList sloId={sloId!} />
|
||||
) : (
|
||||
renderOverview()
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</PluginContext.Provider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
</EuiThemeProvider>
|
||||
|
@ -108,14 +177,34 @@ export class SLOEmbeddable extends AbstractEmbeddable<SloEmbeddableInput, Embedd
|
|||
);
|
||||
}
|
||||
|
||||
public getSloOverviewConfig() {
|
||||
return this.getInput();
|
||||
}
|
||||
|
||||
public updateSloOverviewConfig(next: SloEmbeddableInput) {
|
||||
this.updateInput(next);
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.reloadSubject.next(true);
|
||||
this.reloadGroupSubject?.next(undefined);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.subscription.unsubscribe();
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
padding: 5px 15px;
|
||||
overflow: scroll;
|
||||
|
||||
.euiAccordion__buttonContent {
|
||||
min-width: 100px;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -57,7 +57,7 @@ export class SloOverviewEmbeddableFactoryDefinition
|
|||
SloEmbeddableInput,
|
||||
unknown
|
||||
>['getPanelPlacementSettings'] = (input) => {
|
||||
if (input.showAllGroupByInstances) {
|
||||
if (input.showAllGroupByInstances || input.groupFilters) {
|
||||
return { width: 24, height: 8 };
|
||||
}
|
||||
return { width: 12, height: 8 };
|
||||
|
|
|
@ -19,14 +19,14 @@ import { SloCardItemBadges } from '../../../pages/slos/components/card_view/slo_
|
|||
import { SloCardChart } from '../../../pages/slos/components/card_view/slo_card_item';
|
||||
import { useFetchSloDetails } from '../../../hooks/use_fetch_slo_details';
|
||||
|
||||
import { EmbeddableSloProps } from './types';
|
||||
import { SingleSloProps } from './types';
|
||||
|
||||
export function SloOverview({
|
||||
sloId,
|
||||
sloInstanceId,
|
||||
onRenderComplete,
|
||||
reloadSubject,
|
||||
}: EmbeddableSloProps) {
|
||||
}: SingleSloProps) {
|
||||
const [lastRefreshTime, setLastRefreshTime] = useState<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -6,13 +6,44 @@
|
|||
*/
|
||||
import { EmbeddableInput } from '@kbn/embeddable-plugin/public';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
export interface EmbeddableSloProps {
|
||||
sloId: string | undefined;
|
||||
sloInstanceId: string | undefined;
|
||||
reloadSubject?: Subject<boolean>;
|
||||
onRenderComplete?: () => void;
|
||||
showAllGroupByInstances?: boolean;
|
||||
export type OverviewMode = 'single' | 'groups';
|
||||
export type GroupBy = 'slo.tags' | 'status' | 'slo.indicator.type';
|
||||
export interface GroupFilters {
|
||||
groupBy: GroupBy;
|
||||
groups?: string[];
|
||||
filters?: Filter[];
|
||||
kqlQuery?: string;
|
||||
}
|
||||
|
||||
export type SloEmbeddableInput = EmbeddableInput & EmbeddableSloProps;
|
||||
export type SingleSloProps = EmbeddableSloProps & {
|
||||
sloId: string | undefined;
|
||||
sloInstanceId: string | undefined;
|
||||
showAllGroupByInstances?: boolean;
|
||||
};
|
||||
|
||||
export type GroupSloProps = EmbeddableSloProps & {
|
||||
groupFilters: GroupFilters;
|
||||
};
|
||||
|
||||
export interface EmbeddableSloProps {
|
||||
reloadSubject?: Subject<boolean>;
|
||||
onRenderComplete?: () => void;
|
||||
overviewMode?: OverviewMode;
|
||||
}
|
||||
|
||||
export type SloEmbeddableInput = EmbeddableInput & Partial<GroupSloProps> & Partial<SingleSloProps>;
|
||||
|
||||
export interface HasSloOverviewConfig {
|
||||
getSloOverviewConfig: () => SloEmbeddableInput;
|
||||
updateSloOverviewConfig: (next: SloEmbeddableInput) => void;
|
||||
}
|
||||
|
||||
export const apiHasSloOverviewConfig = (api: unknown | null): api is HasSloOverviewConfig => {
|
||||
return Boolean(
|
||||
api &&
|
||||
typeof (api as HasSloOverviewConfig).getSloOverviewConfig === 'function' &&
|
||||
typeof (api as HasSloOverviewConfig).updateSloOverviewConfig === 'function'
|
||||
);
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ interface SloGroupListFilter {
|
|||
kqlQuery: string;
|
||||
filters: string;
|
||||
lastRefresh?: number;
|
||||
groupsFilter?: string[];
|
||||
}
|
||||
|
||||
export const sloKeys = {
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
useQuery,
|
||||
RefetchOptions,
|
||||
QueryObserverResult,
|
||||
RefetchQueryFilters,
|
||||
} from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { buildQueryFromFilters, Filter } from '@kbn/es-query';
|
||||
import { useMemo } from 'react';
|
||||
|
@ -20,6 +25,7 @@ interface SLOGroupsParams {
|
|||
page?: number;
|
||||
perPage?: number;
|
||||
groupBy?: string;
|
||||
groupsFilter?: string[];
|
||||
kqlQuery?: string;
|
||||
tagsFilter?: SearchState['tagsFilter'];
|
||||
statusFilter?: SearchState['statusFilter'];
|
||||
|
@ -33,12 +39,16 @@ interface UseFetchSloGroupsResponse {
|
|||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: FindSLOGroupsResponse | undefined;
|
||||
refetch: <TPageData>(
|
||||
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
|
||||
) => Promise<QueryObserverResult<FindSLOGroupsResponse | undefined, unknown>>;
|
||||
}
|
||||
|
||||
export function useFetchSloGroups({
|
||||
page = 1,
|
||||
perPage = DEFAULT_SLO_GROUPS_PAGE_SIZE,
|
||||
groupBy = 'ungrouped',
|
||||
groupsFilter = [],
|
||||
kqlQuery = '',
|
||||
tagsFilter,
|
||||
statusFilter,
|
||||
|
@ -73,9 +83,16 @@ export function useFetchSloGroups({
|
|||
return '';
|
||||
}
|
||||
}, [filterDSL, tagsFilter, statusFilter, dataView]);
|
||||
|
||||
const { data, isLoading, isSuccess, isError, isRefetching } = useQuery({
|
||||
queryKey: sloKeys.group({ page, perPage, groupBy, kqlQuery, filters, lastRefresh }),
|
||||
const { data, isLoading, isSuccess, isError, isRefetching, refetch } = useQuery({
|
||||
queryKey: sloKeys.group({
|
||||
page,
|
||||
perPage,
|
||||
groupBy,
|
||||
groupsFilter,
|
||||
kqlQuery,
|
||||
filters,
|
||||
lastRefresh,
|
||||
}),
|
||||
queryFn: async ({ signal }) => {
|
||||
const response = await http.get<FindSLOGroupsResponse>(
|
||||
'/internal/api/observability/slos/_groups',
|
||||
|
@ -84,6 +101,7 @@ export function useFetchSloGroups({
|
|||
...(page && { page }),
|
||||
...(perPage && { perPage }),
|
||||
...(groupBy && { groupBy }),
|
||||
...(groupsFilter && { groupsFilter }),
|
||||
...(kqlQuery && { kqlQuery }),
|
||||
...(filters && { filters }),
|
||||
},
|
||||
|
@ -115,5 +133,6 @@ export function useFetchSloGroups({
|
|||
isSuccess,
|
||||
isError,
|
||||
isRefetching,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import { useContext } from 'react';
|
||||
import { PluginContext } from '../context/plugin_context';
|
||||
import type { PluginContextValue } from '../context/plugin_context';
|
||||
|
||||
export function usePluginContext() {
|
||||
return useContext(PluginContext);
|
||||
return useContext(PluginContext) as PluginContextValue;
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ const mockKibana = () => {
|
|||
},
|
||||
executionContext: {
|
||||
get: () => ({
|
||||
name: 'observability-overview',
|
||||
name: 'slo',
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiTablePagination,
|
||||
EuiText,
|
||||
|
@ -35,13 +35,13 @@ import { SLOView } from '../toggle_slo_view';
|
|||
|
||||
interface Props {
|
||||
group: string;
|
||||
kqlQuery: string;
|
||||
kqlQuery?: string;
|
||||
sloView: SLOView;
|
||||
sort: string;
|
||||
direction: SortDirection;
|
||||
sort?: string;
|
||||
direction?: SortDirection;
|
||||
groupBy: string;
|
||||
summary: GroupSummary;
|
||||
filters: Filter[];
|
||||
summary?: GroupSummary;
|
||||
filters?: Filter[];
|
||||
}
|
||||
|
||||
export function GroupListView({
|
||||
|
@ -93,121 +93,119 @@ export function GroupListView({
|
|||
setPage(pageNumber);
|
||||
};
|
||||
|
||||
const worstSLI = useSloFormattedSLIValue(summary.worst.sliValue);
|
||||
const worstSLI = useSloFormattedSLIValue(summary?.worst.sliValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel hasBorder={true} data-test-subj="sloGroupViewPanel">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<MemoEuiAccordion
|
||||
forceState={accordionState}
|
||||
onToggle={onToggle}
|
||||
buttonContent={
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexGroup data-test-subj="sloGroupViewPanel">
|
||||
<EuiFlexItem>
|
||||
<MemoEuiAccordion
|
||||
forceState={accordionState}
|
||||
onToggle={onToggle}
|
||||
buttonContent={
|
||||
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{groupName}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued">({summary?.total})</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
extraAction={
|
||||
<EuiFlexGroup responsive={false} alignItems="center">
|
||||
{summary!.violated > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{groupName}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued">({summary.total})</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
extraAction={
|
||||
<EuiFlexGroup alignItems="center">
|
||||
{summary.violated > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="danger">
|
||||
{i18n.translate('xpack.slo.group.totalViolated', {
|
||||
defaultMessage: '{total} Violated',
|
||||
values: {
|
||||
total: summary.violated,
|
||||
},
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color={'success'}>
|
||||
{i18n.translate('xpack.slo.group.totalHealthy', {
|
||||
defaultMessage: '{total} Healthy',
|
||||
<EuiBadge color="danger">
|
||||
{i18n.translate('xpack.slo.group.totalViolated', {
|
||||
defaultMessage: '{total} Violated',
|
||||
values: {
|
||||
total: summary.healthy,
|
||||
total: summary?.violated,
|
||||
},
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<>
|
||||
<EuiText size="s">
|
||||
{i18n.translate('xpack.slo.group.totalSloViolatedTooltip', {
|
||||
defaultMessage: 'SLO: {name}',
|
||||
values: {
|
||||
name: summary.worst.slo?.name,
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiText size="s">
|
||||
{i18n.translate('xpack.slo.group.totalSloViolatedTooltip.instance', {
|
||||
defaultMessage: 'Instance: {instance}',
|
||||
values: {
|
||||
instance: summary.worst.slo?.instanceId,
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color={'success'}>
|
||||
{i18n.translate('xpack.slo.group.totalHealthy', {
|
||||
defaultMessage: '{total} Healthy',
|
||||
values: {
|
||||
total: summary?.healthy,
|
||||
},
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<>
|
||||
<EuiText size="s">
|
||||
{i18n.translate('xpack.slo.group.totalSloViolatedTooltip', {
|
||||
defaultMessage: 'SLO: {name}',
|
||||
values: {
|
||||
name: summary?.worst.slo?.name,
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiText size="s">
|
||||
{i18n.translate('xpack.slo.group.totalSloViolatedTooltip.instance', {
|
||||
defaultMessage: 'Instance: {instance}',
|
||||
values: {
|
||||
instance: summary?.worst.slo?.instanceId,
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiLink
|
||||
data-test-subj="o11yGroupListViewLink"
|
||||
href={basePath.prepend(
|
||||
paths.sloDetails(summary!.worst.slo?.id, summary!.worst.slo?.instanceId)
|
||||
)}
|
||||
>
|
||||
<EuiLink
|
||||
data-test-subj="o11yGroupListViewLink"
|
||||
href={basePath.prepend(
|
||||
paths.sloDetails(summary.worst.slo?.id, summary.worst.slo?.instanceId)
|
||||
)}
|
||||
{i18n.translate('xpack.slo.group.worstPerforming', {
|
||||
defaultMessage: 'Worst performing: ',
|
||||
})}
|
||||
<EuiTextColor
|
||||
color={summary?.worst.status !== 'HEALTHY' ? 'danger' : undefined}
|
||||
>
|
||||
{i18n.translate('xpack.slo.group.worstPerforming', {
|
||||
defaultMessage: 'Worst performing: ',
|
||||
})}
|
||||
<EuiTextColor
|
||||
color={summary.worst.status !== 'HEALTHY' ? 'danger' : undefined}
|
||||
>
|
||||
<strong>{worstSLI}</strong>
|
||||
</EuiTextColor>
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
id={group}
|
||||
initialIsOpen={false}
|
||||
>
|
||||
{isAccordionOpen && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<SlosView
|
||||
sloList={results}
|
||||
loading={isLoading || isRefetching}
|
||||
error={isError}
|
||||
sloView={sloView}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTablePagination
|
||||
pageCount={Math.ceil(total / itemsPerPage)}
|
||||
activePage={page}
|
||||
onChangePage={handlePageClick}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onChangeItemsPerPage={(perPage) => setItemsPerPage(perPage)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MemoEuiAccordion>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<strong>{worstSLI}</strong>
|
||||
</EuiTextColor>
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
id={group}
|
||||
initialIsOpen={false}
|
||||
>
|
||||
{isAccordionOpen && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<SlosView
|
||||
sloList={results}
|
||||
loading={isLoading || isRefetching}
|
||||
error={isError}
|
||||
sloView={sloView}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTablePagination
|
||||
pageCount={Math.ceil(total / itemsPerPage)}
|
||||
activePage={page}
|
||||
onChangePage={handlePageClick}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onChangeItemsPerPage={(perPage) => setItemsPerPage(perPage)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MemoEuiAccordion>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ describe('Group View', () => {
|
|||
useFetchSloGroupsMock.mockReturnValue({
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
<GroupView groupBy="slo.tags" kqlQuery="" sloView="cardView" sort="status" direction="desc" />
|
||||
|
@ -65,6 +67,7 @@ describe('Group View', () => {
|
|||
total: 0,
|
||||
results: [],
|
||||
},
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
|
@ -78,6 +81,7 @@ describe('Group View', () => {
|
|||
it('should show loading indicator', async () => {
|
||||
useFetchSloGroupsMock.mockReturnValue({
|
||||
isLoading: true,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
|
@ -115,6 +119,7 @@ describe('Group View', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
const { queryAllByTestId, getByTestId } = render(
|
||||
<GroupView
|
||||
|
@ -163,6 +168,7 @@ describe('Group View', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const { queryAllByTestId } = render(
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { EuiEmptyPrompt, EuiFlexItem, EuiLoadingSpinner, EuiTablePagination } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { useFetchSloGroups } from '../../../../hooks/use_fetch_slo_groups';
|
||||
import { useUrlSearchState } from '../../hooks/use_url_search_state';
|
||||
import type { SortDirection } from '../slo_list_search_bar';
|
||||
|
@ -16,17 +17,28 @@ import { GroupListView } from './group_list_view';
|
|||
|
||||
interface Props {
|
||||
groupBy: string;
|
||||
kqlQuery: string;
|
||||
kqlQuery?: string;
|
||||
sloView: SLOView;
|
||||
sort: string;
|
||||
direction: SortDirection;
|
||||
sort?: string;
|
||||
direction?: SortDirection;
|
||||
filters?: Filter[];
|
||||
lastRefreshTime?: number;
|
||||
groupsFilter?: string[];
|
||||
}
|
||||
|
||||
export function GroupView({ kqlQuery, sloView, sort, direction, groupBy }: Props) {
|
||||
export function GroupView({
|
||||
kqlQuery,
|
||||
sloView,
|
||||
sort,
|
||||
direction,
|
||||
groupBy,
|
||||
groupsFilter,
|
||||
filters,
|
||||
lastRefreshTime,
|
||||
}: Props) {
|
||||
const { state, onStateChange } = useUrlSearchState();
|
||||
const { tagsFilter, statusFilter, filters, page, perPage, lastRefresh } = state;
|
||||
|
||||
const { data, isLoading, isError } = useFetchSloGroups({
|
||||
const { tagsFilter, statusFilter, page, perPage, lastRefresh } = state;
|
||||
const { data, isLoading, isError, isRefetching, refetch } = useFetchSloGroups({
|
||||
perPage,
|
||||
page: page + 1,
|
||||
groupBy,
|
||||
|
@ -35,13 +47,19 @@ export function GroupView({ kqlQuery, sloView, sort, direction, groupBy }: Props
|
|||
statusFilter,
|
||||
filters,
|
||||
lastRefresh,
|
||||
groupsFilter,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [lastRefreshTime, refetch]);
|
||||
|
||||
const { results = [], total = 0 } = data ?? {};
|
||||
const handlePageClick = (pageNumber: number) => {
|
||||
onStateChange({ page: pageNumber });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || isRefetching) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="sloGroupListLoading"
|
||||
|
|
|
@ -75,7 +75,10 @@ export function SloItemActions({
|
|||
share: {
|
||||
url: { locators },
|
||||
},
|
||||
executionContext,
|
||||
} = useKibana().services;
|
||||
const executionContextName = executionContext.get().name;
|
||||
const isDashboardContext = executionContextName === 'dashboards';
|
||||
const { hasWriteCapabilities } = useCapabilities();
|
||||
|
||||
const sloDetailsUrl = basePath.prepend(
|
||||
|
@ -220,17 +223,22 @@ export function SloItemActions({
|
|||
>
|
||||
{i18n.translate('xpack.slo.item.actions.delete', { defaultMessage: 'Delete' })}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
icon="dashboardApp"
|
||||
key="addToDashboard"
|
||||
onClick={handleAddToDashboard}
|
||||
data-test-subj="sloActionsAddToDashboard"
|
||||
>
|
||||
{i18n.translate('xpack.slo.item.actions.addToDashboard', {
|
||||
defaultMessage: 'Add to Dashboard',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
].concat(
|
||||
!isDashboardContext ? (
|
||||
<EuiContextMenuItem
|
||||
icon="dashboardApp"
|
||||
key="addToDashboard"
|
||||
onClick={handleAddToDashboard}
|
||||
data-test-subj="sloActionsAddToDashboard"
|
||||
>
|
||||
{i18n.translate('xpack.slo.item.actions.addToDashboard', {
|
||||
defaultMessage: 'Add to Dashboard',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
) : (
|
||||
[]
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
|
|
|
@ -124,6 +124,11 @@ const mockKibana = () => {
|
|||
hasQuerySuggestions: () => {},
|
||||
},
|
||||
},
|
||||
executionContext: {
|
||||
get: () => ({
|
||||
name: 'slo',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
||||
import type { CoreSetup } from '@kbn/core/public';
|
||||
import {
|
||||
apiCanAccessViewMode,
|
||||
apiHasType,
|
||||
apiIsOfType,
|
||||
EmbeddableApiContext,
|
||||
getInheritedViewMode,
|
||||
CanAccessViewMode,
|
||||
HasType,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { createAction } from '@kbn/ui-actions-plugin/public';
|
||||
import type { SLOEmbeddable } from '../embeddable/slo/overview/slo_embeddable';
|
||||
import { SLO_EMBEDDABLE } from '../embeddable/slo/constants';
|
||||
import { SloPublicPluginsStart, SloPublicStart } from '..';
|
||||
import { HasSloOverviewConfig } from '../embeddable/slo/overview/types';
|
||||
|
||||
export const EDIT_SLO_OVERVIEW_ACTION = 'editSloOverviewPanelAction';
|
||||
type EditSloOverviewPanelApi = CanAccessViewMode & HasType & HasSloOverviewConfig;
|
||||
const isEditSloOverviewPanelApi = (api: unknown): api is EditSloOverviewPanelApi =>
|
||||
Boolean(
|
||||
apiHasType(api) &&
|
||||
apiIsOfType(api, SLO_EMBEDDABLE) &&
|
||||
apiCanAccessViewMode(api) &&
|
||||
getInheritedViewMode(api) === ViewMode.EDIT
|
||||
);
|
||||
|
||||
export function createEditSloOverviewPanelAction(
|
||||
getStartServices: CoreSetup<SloPublicPluginsStart, SloPublicStart>['getStartServices']
|
||||
) {
|
||||
return createAction<EmbeddableApiContext>({
|
||||
id: EDIT_SLO_OVERVIEW_ACTION,
|
||||
type: EDIT_SLO_OVERVIEW_ACTION,
|
||||
getIconType(): string {
|
||||
return 'pencil';
|
||||
},
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.slo.actions.editSloOverviewEmbeddableTitle', {
|
||||
defaultMessage: 'Edit criteria',
|
||||
}),
|
||||
async execute({ embeddable }: EmbeddableApiContext) {
|
||||
if (!embeddable) {
|
||||
throw new Error('Not possible to execute an action without the embeddable context');
|
||||
}
|
||||
|
||||
const [coreStart, pluginStart] = await getStartServices();
|
||||
|
||||
try {
|
||||
const { resolveEmbeddableSloUserInput } = await import(
|
||||
'../embeddable/slo/overview/handle_explicit_input'
|
||||
);
|
||||
|
||||
const result = await resolveEmbeddableSloUserInput(
|
||||
coreStart,
|
||||
pluginStart,
|
||||
(embeddable as SLOEmbeddable).getSloOverviewConfig()
|
||||
);
|
||||
(embeddable as SLOEmbeddable).updateInput(result);
|
||||
} catch (e) {
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
isCompatible: async ({ embeddable }: EmbeddableApiContext) =>
|
||||
isEditSloOverviewPanelApi(embeddable) &&
|
||||
embeddable.getSloOverviewConfig().overviewMode === 'groups',
|
||||
});
|
||||
}
|
|
@ -9,6 +9,7 @@ import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
|
|||
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import type { CoreSetup } from '@kbn/core/public';
|
||||
import { createEditSloAlertsPanelAction } from './edit_slo_alerts_panel';
|
||||
import { createEditSloOverviewPanelAction } from './edit_slo_overview_panel';
|
||||
import { SloPublicPluginsStart, SloPublicStart } from '..';
|
||||
|
||||
export function registerSloAlertsUiActions(
|
||||
|
@ -17,6 +18,8 @@ export function registerSloAlertsUiActions(
|
|||
) {
|
||||
// Initialize actions
|
||||
const editSloAlertsPanelAction = createEditSloAlertsPanelAction(core.getStartServices);
|
||||
const editSloOverviewPanelAction = createEditSloOverviewPanelAction(core.getStartServices);
|
||||
// Assign triggers
|
||||
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editSloAlertsPanelAction);
|
||||
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editSloOverviewPanelAction);
|
||||
}
|
||||
|
|
|
@ -49,16 +49,18 @@ export class FindSLOGroups {
|
|||
public async execute(params: FindSLOGroupsParams): Promise<FindSLOGroupsResponse> {
|
||||
const pagination = toPagination(params);
|
||||
const groupBy = params.groupBy;
|
||||
const groupsFilter = [params.groupsFilter ?? []].flat();
|
||||
const kqlQuery = params.kqlQuery ?? '';
|
||||
const filters = params.filters ?? '';
|
||||
let parsedFilters: any = {};
|
||||
|
||||
try {
|
||||
parsedFilters = JSON.parse(filters);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to parse filters: ${e.message}`);
|
||||
}
|
||||
|
||||
const hasSelectedTags = groupBy === 'slo.tags' && groupsFilter.length > 0;
|
||||
|
||||
const response = await typedSearch(this.esClient, {
|
||||
index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
size: 0,
|
||||
|
@ -77,6 +79,7 @@ export class FindSLOGroups {
|
|||
terms: {
|
||||
field: groupBy,
|
||||
size: 10000,
|
||||
...(hasSelectedTags && { include: groupsFilter }),
|
||||
},
|
||||
aggs: {
|
||||
worst: {
|
||||
|
|
|
@ -85,6 +85,12 @@
|
|||
"@kbn/discover-plugin",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/presentation-publishing"
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/core-ui-settings-browser-mocks",
|
||||
"@kbn/core-i18n-browser-mocks",
|
||||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/core-notifications-browser-mocks",
|
||||
"@kbn/core-http-browser-mocks"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue