[8.x] [SLO]: Add filtering to SLO Management table, improve UX (#216040) (#218171)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[SLO]: Add filtering to SLO Management table, improve UX
(#216040)](https://github.com/elastic/kibana/pull/216040)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Bailey
Cash","email":"bailey.cash@elastic.co"},"sourceCommit":{"committedDate":"2025-03-27T22:36:16Z","message":"[SLO]:
Add filtering to SLO Management table, improve UX (#216040)\n\n##
Summary\n\nResolves #214258 \n\n- Updates search bar to utilize
UnifiedSearchBar\n- Adds ability to filter SLOs by tags (OR operator)\n-
Makes improvements to version display\n\n![Screenshot 2025-03-26 at 2
55\n01 PM](https://github.com/user-attachments/assets/cf8c19e4-7a9f-4f2e-bd5d-b820b8f9bf23)\n![Screenshot
2025-03-26 at 2
54\n20 PM](https://github.com/user-attachments/assets/46e968ff-352a-4f4e-b762-a96c727c08f4)\n\n---------\n\nCo-authored-by:
kdelemme <kdelemme@users.noreply.github.com>\nCo-authored-by: Elastic
Machine <elasticmachine@users.noreply.github.com>\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"40e95f00f1b2f3b2ba76f958727e9bea24424679","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:skip","Team:obs-ux-management","v9.1.0"],"title":"[SLO]:
Add filtering to SLO Management table, improve
UX","number":216040,"url":"https://github.com/elastic/kibana/pull/216040","mergeCommit":{"message":"[SLO]:
Add filtering to SLO Management table, improve UX (#216040)\n\n##
Summary\n\nResolves #214258 \n\n- Updates search bar to utilize
UnifiedSearchBar\n- Adds ability to filter SLOs by tags (OR operator)\n-
Makes improvements to version display\n\n![Screenshot 2025-03-26 at 2
55\n01 PM](https://github.com/user-attachments/assets/cf8c19e4-7a9f-4f2e-bd5d-b820b8f9bf23)\n![Screenshot
2025-03-26 at 2
54\n20 PM](https://github.com/user-attachments/assets/46e968ff-352a-4f4e-b762-a96c727c08f4)\n\n---------\n\nCo-authored-by:
kdelemme <kdelemme@users.noreply.github.com>\nCo-authored-by: Elastic
Machine <elasticmachine@users.noreply.github.com>\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"40e95f00f1b2f3b2ba76f958727e9bea24424679"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/216040","number":216040,"mergeCommit":{"message":"[SLO]:
Add filtering to SLO Management table, improve UX (#216040)\n\n##
Summary\n\nResolves #214258 \n\n- Updates search bar to utilize
UnifiedSearchBar\n- Adds ability to filter SLOs by tags (OR operator)\n-
Makes improvements to version display\n\n![Screenshot 2025-03-26 at 2
55\n01 PM](https://github.com/user-attachments/assets/cf8c19e4-7a9f-4f2e-bd5d-b820b8f9bf23)\n![Screenshot
2025-03-26 at 2
54\n20 PM](https://github.com/user-attachments/assets/46e968ff-352a-4f4e-b762-a96c727c08f4)\n\n---------\n\nCo-authored-by:
kdelemme <kdelemme@users.noreply.github.com>\nCo-authored-by: Elastic
Machine <elasticmachine@users.noreply.github.com>\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"40e95f00f1b2f3b2ba76f958727e9bea24424679"}}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
Bailey Cash 2025-04-15 03:10:26 -04:00 committed by GitHub
parent 1761809f29
commit de6ea00825
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 261 additions and 110 deletions

View file

@ -46071,6 +46071,11 @@ paths:
name: includeOutdatedOnly
schema:
type: boolean
- description: Specify which SLO tags to query by (comma-separated list)
in: query
name: tags
schema:
type: string
- description: Filters the SLOs by name
example: my service availability
in: query

View file

@ -12,6 +12,7 @@ const findSloDefinitionsParamsSchema = t.partial({
query: t.partial({
search: t.string,
includeOutdatedOnly: toBooleanRt,
tags: t.string,
page: t.string,
perPage: t.string,
}),

View file

@ -677,6 +677,113 @@
}
}
},
"/s/{spaceId}/internal/observability/slos/_definitions": {
"get": {
"summary": "Get the SLO definitions",
"operationId": "getDefinitionsOp",
"description": "You must have the `read` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.\n",
"tags": [
"slo"
],
"parameters": [
{
"$ref": "#/components/parameters/kbn_xsrf"
},
{
"$ref": "#/components/parameters/space_id"
},
{
"name": "includeOutdatedOnly",
"in": "query",
"description": "Indicates if the API returns only outdated SLO or all SLO definitions",
"schema": {
"type": "boolean"
},
"example": true
},
{
"name": "tags",
"in": "query",
"description": "Specify which SLO tags to query by (comma-separated values)",
"schema": {
"type": "string"
},
"example": true
},
{
"name": "search",
"in": "query",
"description": "Filters the SLOs by name",
"schema": {
"type": "string"
},
"example": "my service availability"
},
{
"name": "page",
"in": "query",
"description": "The page to use for pagination, must be greater or equal than 1",
"schema": {
"type": "number"
},
"example": 1
},
{
"name": "perPage",
"in": "query",
"description": "Number of SLOs returned by page",
"schema": {
"type": "integer",
"default": 100,
"maximum": 1000
},
"example": 100
}
],
"responses": {
"200": {
"description": "Successful request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/find_slo_definitions_response"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/400_response"
}
}
}
},
"401": {
"description": "Unauthorized response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/401_response"
}
}
}
},
"403": {
"description": "Unauthorized response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/403_response"
}
}
}
}
}
}
},
"/s/{spaceId}/api/observability/slos/_delete_instances": {
"post": {
"summary": "Batch delete rollup and summary data",

View file

@ -1346,7 +1346,7 @@ components:
example: Bad Request
message:
type: string
example: 'Invalid value ''foo'' supplied to: [...]'
example: "Invalid value 'foo' supplied to: [...]"
401_response:
title: Unauthorized
type: object

View file

@ -15,6 +15,11 @@ get:
schema:
type: boolean
example: true
- name: tags
in: query
description: Filters the SLOs by tag
schema:
type: string
- name: search
in: query
description: Filters the SLOs by name
@ -29,7 +34,7 @@ get:
example: 1
- name: perPage
in: query
description: Number of SLOs returned by page
description: Number of SLOs returned by page
schema:
type: integer
default: 100

View file

@ -57,6 +57,7 @@ export const sloKeys = {
page: number;
perPage: number;
includeOutdatedOnly: boolean;
validTags: string;
}) => [...sloKeys.allDefinitions(), params],
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
health: (list: Array<{ sloId: string; sloInstanceId: string }>) =>

View file

@ -21,6 +21,7 @@ export interface UseFetchSloDefinitionsResponse {
interface SLODefinitionParams {
name?: string;
includeOutdatedOnly?: boolean;
tags?: string[];
page?: number;
perPage?: number;
}
@ -28,14 +29,16 @@ interface SLODefinitionParams {
export function useFetchSloDefinitions({
name = '',
includeOutdatedOnly = false,
tags = [],
page = 1,
perPage = 100,
}: SLODefinitionParams): UseFetchSloDefinitionsResponse {
const { sloClient } = usePluginContext();
const search = name.endsWith('*') ? name : `${name}*`;
const validTags = tags.filter((tag) => !!tag).join();
const { isLoading, isError, isSuccess, data, refetch } = useQuery({
queryKey: sloKeys.definitions({ search, page, perPage, includeOutdatedOnly }),
queryKey: sloKeys.definitions({ search, page, perPage, includeOutdatedOnly, validTags }),
queryFn: async ({ signal }) => {
try {
return await sloClient.fetch('GET /api/observability/slos/_definitions 2023-10-31', {
@ -43,6 +46,7 @@ export function useFetchSloDefinitions({
query: {
...(search !== undefined && { search }),
...(includeOutdatedOnly !== undefined && { includeOutdatedOnly }),
...(validTags?.length && { tags: validTags }),
...(page !== undefined && { page: String(page) }),
...(perPage !== undefined && { perPage: String(perPage) }),
},

View file

@ -7,72 +7,85 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/eui';
import { EuiComboBox, EuiComboBoxOptionOption, EuiText } from '@elastic/eui';
import { observabilityAppId } from '@kbn/observability-shared-plugin/common';
import { useFetchSLOSuggestions } from '../../slo_edit/hooks/use_fetch_suggestions';
import { useKibana } from '../../../hooks/use_kibana';
interface Props {
initialSearch?: string;
filters: {
search: string;
tags: string[];
};
setFilters: Function;
onRefresh: () => void;
onSearch: (search: string) => void;
}
export function SloManagementSearchBar({ onSearch, onRefresh, initialSearch = '' }: Props) {
const [tempSearch, setTempSearch] = useState<string>(initialSearch);
const [search, setSearch] = useState<string>(initialSearch);
const refreshOrUpdateSearch = () => {
if (tempSearch !== search) {
setSearch(tempSearch);
onSearch(tempSearch);
} else {
onRefresh();
}
};
export function SloManagementSearchBar({ filters, setFilters, onRefresh }: Props) {
const {
unifiedSearch: {
ui: { SearchBar },
},
} = useKibana().services;
const handleClick = (event: React.ChangeEvent<HTMLInputElement>) =>
setTempSearch(event.target.value);
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
refreshOrUpdateSearch();
}
};
const { suggestions } = useFetchSLOSuggestions();
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
[]
);
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldSearch
data-test-subj="o11ySloDefinitionsFieldSearch"
fullWidth
value={tempSearch}
onChange={handleClick}
onKeyDown={handleKeyPress}
<SearchBar
appName={observabilityAppId}
placeholder={i18n.translate('xpack.slo.sloDefinitions.filterByName', {
defaultMessage: 'Filter by name',
})}
isAutoRefreshDisabled
disableQueryLanguageSwitcher
nonKqlMode="text"
showQueryMenu={false}
showDatePicker={false}
showSavedQueryControls={false}
showFilterBar={false}
query={{ query: filters.search, language: 'text' }}
onQuerySubmit={({ query: value }) => {
setFilters({ search: value?.query, tags: filters.tags });
}}
onRefresh={onRefresh}
renderQueryInputAppend={() => (
<EuiComboBox
aria-label={filterTagsLabel}
placeholder={filterTagsLabel}
delimiter=","
options={suggestions?.tags ? [existOption, ...suggestions?.tags] : []}
selectedOptions={selectedOptions}
onChange={(newOptions) => {
setSelectedOptions(newOptions);
setFilters({ search: filters.search, tags: newOptions.map((option) => option.value) });
}}
isClearable={true}
data-test-subj="filter-slos-by-tag"
/>
</EuiFlexItem>
<EuiFlexItem grow={0}>
{search === tempSearch && (
<EuiButton
data-test-subj="o11ySloDefinitionsRefreshButton"
iconType="refresh"
onClick={refreshOrUpdateSearch}
>
{i18n.translate('xpack.slo.sloDefinitions.refreshButtonLabel', {
defaultMessage: 'Refresh',
})}
</EuiButton>
)}
{search !== tempSearch && (
<EuiButton
data-test-subj="o11ySloDefinitionsUpdateButton"
iconType="kqlFunction"
color="success"
fill
onClick={refreshOrUpdateSearch}
>
{i18n.translate('xpack.slo.sloDefinitions.updateButtonLabel', {
defaultMessage: 'Update',
})}
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
)}
/>
);
}
const filterTagsLabel = i18n.translate('xpack.slo.sloDefinitions.filterByTag', {
defaultMessage: 'Filter tags',
});
const existOption = {
prepend: (
<EuiText size="s">
<strong>
<em>
{i18n.translate('xpack.slo.sloDefinitions.tagOptions.exists', {
defaultMessage: 'Exists',
})}
</em>
</strong>
</EuiText>
),
label: '',
value: '*',
};

View file

@ -13,12 +13,10 @@ import {
EuiBasicTableColumn,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
@ -39,10 +37,18 @@ import { SloEnableConfirmationModal } from '../../../components/slo/enable_confi
import { SloDisableConfirmationModal } from '../../../components/slo/disable_confirmation_modal/slo_disable_confirmation_modal';
import { SLO_MODEL_VERSION } from '../../../../common/constants';
interface SearchFilters {
search: string;
tags: string[];
}
export function SloManagementTable() {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState('');
const [filters, setFilters] = useState<SearchFilters>({
search: '',
tags: [],
});
const { services } = useKibana();
const {
@ -51,9 +57,10 @@ export function SloManagementTable() {
} = services;
const { isLoading, isError, data, refetch } = useFetchSloDefinitions({
name: search,
name: filters.search,
page: pageIndex + 1,
perPage: pageSize,
tags: filters.tags,
});
const { data: permissions } = usePermissions();
@ -230,31 +237,17 @@ export function SloManagementTable() {
}),
render: (item: SLODefinitionResponse['version']) => {
return item < SLO_MODEL_VERSION ? (
<EuiFlexGroup alignItems="center" direction="row" gutterSize="xs">
<EuiFlexItem grow={0}>
<EuiText size="s">{item}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={0}>
<EuiToolTip
title={
<EuiText>
{i18n.translate('xpack.slo.sloManagementTable.columns.outdatedTooltip', {
defaultMessage:
'This SLO is from a previous version and needs to either be reset to upgrade to the latest version OR deleted and removed from the system. When you reset the SLO, the transform will be updated to the latest version and the historical data will be regenerated from the source data.',
})}
</EuiText>
}
>
<EuiFlexGroup alignItems="center" direction="row" gutterSize="xs">
<EuiFlexItem grow={0}>
<EuiIcon type="warning" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiText size="s">
{i18n.translate('xpack.slo.sloManagementTable.version.outdated', {
defaultMessage: 'Outdated',
})}
</EuiText>
) : (
<EuiText size="s">{item}</EuiText>
<EuiText size="s">
{i18n.translate('xpack.slo.sloManagementTable.version.current', {
defaultMessage: 'Current',
})}
</EuiText>
);
},
},
@ -301,19 +294,24 @@ export function SloManagementTable() {
return (
<>
<EuiPanel hasBorder={true}>
<SloManagementSearchBar initialSearch={search} onRefresh={refetch} onSearch={setSearch} />
<SloManagementSearchBar filters={filters} setFilters={setFilters} onRefresh={refetch} />
<EuiSpacer size="m" />
{!isError && (
<EuiBasicTable<SLODefinitionResponse>
tableCaption={TABLE_CAPTION}
items={data?.results ?? []}
rowHeader="status"
columns={columns}
pagination={pagination}
onChange={onTableChange}
loading={isLoading}
/>
)}
<EuiBasicTable<SLODefinitionResponse>
tableCaption={TABLE_CAPTION}
error={
isError
? i18n.translate('xpack.slo.sloManagementTable.error', {
defaultMessage: 'An error occurred while retrieving SLO definitions',
})
: undefined
}
items={data?.results ?? []}
rowHeader="name"
columns={columns}
pagination={pagination}
onChange={onTableChange}
loading={isLoading}
/>
</EuiPanel>
{sloToDelete ? (
<SloDeleteModal

View file

@ -10,18 +10,18 @@ import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import React, { useEffect } from 'react';
import { paths } from '../../../common/locators/paths';
import { HeaderMenu } from '../../components/header_menu/header_menu';
import { useKibana } from '../../hooks/use_kibana';
import { useLicense } from '../../hooks/use_license';
import { usePermissions } from '../../hooks/use_permissions';
import { useLicense } from '../../hooks/use_license';
import { useKibana } from '../../hooks/use_kibana';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { SloManagementContent } from './components/slo_management_content';
import { useFetchSloDefinitions } from '../../hooks/use_fetch_slo_definitions';
export function SloManagementPage() {
const {
application: { navigateToUrl },
http: { basePath },
serverless,
application: { navigateToUrl },
} = useKibana().services;
const { ObservabilityPageTemplate } = usePluginContext();
const { data: permissions } = usePermissions();

View file

@ -22,8 +22,11 @@ export class FindSLODefinitions {
constructor(private repository: SLORepository) {}
public async execute(params: FindSLODefinitionsParams): Promise<FindSLODefinitionsResponse> {
const requestTags: string[] = params.tags?.split(',') ?? [];
const result = await this.repository.search(params.search ?? '', toPagination(params), {
includeOutdatedOnly: params.includeOutdatedOnly === true ? true : false,
includeOutdatedOnly: params.includeOutdatedOnly === true,
tags: requestTags,
});
return findSloDefinitionsResponseSchema.encode(result);
}

View file

@ -25,7 +25,10 @@ export interface SLORepository {
search(
search: string,
pagination: Pagination,
options?: { includeOutdatedOnly?: boolean }
options?: {
includeOutdatedOnly: boolean;
tags: string[];
}
): Promise<Paginated<SLODefinition>>;
}
@ -123,17 +126,28 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
async search(
search: string,
pagination: Pagination,
options: { includeOutdatedOnly?: boolean } = { includeOutdatedOnly: false }
options: {
includeOutdatedOnly: boolean;
tags: string[];
} = { includeOutdatedOnly: false, tags: [] }
): Promise<Paginated<SLODefinition>> {
const { includeOutdatedOnly, tags } = options;
const filter = [];
if (tags.length > 0) {
filter.push(`slo.attributes.tags: (${tags.join(' OR ')})`);
}
if (!!includeOutdatedOnly) {
filter.push(`slo.attributes.version < ${SLO_MODEL_VERSION}`);
}
const response = await this.soClient.find<StoredSLODefinition>({
type: SO_SLO_TYPE,
page: pagination.page,
perPage: pagination.perPage,
search,
searchFields: ['name'],
...(!!options.includeOutdatedOnly && {
filter: `slo.attributes.version < ${SLO_MODEL_VERSION}`,
}),
...(filter.length && { filter: filter.join(' AND ') }),
sortField: 'id',
sortOrder: 'asc',
});