[Infrastructure UI] Add strict payload validation to inventory_views endpoint (#160852)

closes [#157505](https://github.com/elastic/kibana/issues/157505)
fixes [#157740](https://github.com/elastic/kibana/issues/157740)
## Summary

This PR adds strict payload validation to `inventory_views` endpoint. I
tried to make the types consistent between sever and frontend code and
because of that more changes had to be made. I also refactored the
`toolbar_control` component, decoupling them from Inventory View and
Metrics Explorer View types


### How to test

- Call the endpoint below trying to use invalid values. see
[here](https://github.com/elastic/kibana/pull/160852/files#diff-058b21e249ebbe2795d450d07025d8904a58cfb07a97979e85975f87e931ffceR133-R143).

```bash
[POST|PUT] kbn:/api/infra/inventory_views
{
  "attributes": {
    "metric": {
        "type": [TYPE]
    },
    "sort": {
        "by": "name"
        "direction": "desc"
    },
    "groupBy": [],
    "nodeType": "host",
    "view": "map",
    "customOptions": [],
    "customMetrics": [],
    "boundsOverride": {
        "max": 1,
        "min": 0
    },
    "autoBounds": true,
    "accountId": "",
    "region": "",
    "autoReload": false,
    "filterQuery": {
        "expression": "",
        "kind": "kuery"
    },
    "legend": {
        "palette": "cool",
        "steps": 10,
        "reverseColors": false
    },
    "timelineOpen": false,
    "name": "test-uptime"
  }
}
```

- Set up a local Kibana instance
- Navigate to `Infrastructure`
- In the UI, use the Saved View feature and try different field
combinations

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Carlos Crespo 2023-07-05 08:52:24 +02:00 committed by GitHub
parent 74c3658dd6
commit d79e69a1ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 496 additions and 344 deletions

View file

@ -16,11 +16,15 @@ export interface InRangeBrand {
export type InRange = rt.Branded<number, InRangeBrand>;
export const inRangeRt = (start: number, end: number) =>
rt.brand(
rt.number, // codec
(n): n is InRange => n >= start && n <= end,
// refinement of the number type
'InRange' // name of this codec
new rt.Type<number, number>(
'InRange',
(input: unknown): input is number =>
typeof input === 'number' && input >= start && input <= end,
(input: unknown, context: rt.Context) =>
typeof input === 'number' && input >= start && input <= end
? rt.success(input)
: rt.failure(input, context),
rt.identity
);
export const inRangeFromStringRt = (start: number, end: number) => {

View file

@ -4,9 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { either } from 'fp-ts/Either';
import { inventoryViewRT } from '../../../inventory_views';
export const INVENTORY_VIEW_URL = '/api/infra/inventory_views';
export const INVENTORY_VIEW_URL_ENTITY = `${INVENTORY_VIEW_URL}/{inventoryViewId}`;
@ -33,30 +34,8 @@ export const inventoryViewRequestQueryRT = rt.partial({
sourceId: rt.string,
});
export type InventoryViewRequestQuery = rt.TypeOf<typeof inventoryViewRequestQueryRT>;
const inventoryViewAttributesResponseRT = rt.intersection([
rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
}),
rt.UnknownRecord,
]);
const inventoryViewResponseRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: inventoryViewAttributesResponseRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
export const inventoryViewResponsePayloadRT = rt.type({
data: inventoryViewResponseRT,
data: inventoryViewRT,
});
export type InventoryViewRequestQuery = rt.TypeOf<typeof inventoryViewRequestQueryRT>;

View file

@ -5,16 +5,18 @@
* 2.0.
*/
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { inventoryViewAttributesRT, inventoryViewRT } from '../../../inventory_views';
export const createInventoryViewAttributesRequestPayloadRT = rt.intersection([
rt.type({
name: nonEmptyStringRt,
}),
rt.UnknownRecord,
rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })),
]);
export const createInventoryViewAttributesRequestPayloadRT = rt.exact(
rt.intersection([
inventoryViewAttributesRT,
rt.partial({
isDefault: rt.undefined,
isStatic: rt.undefined,
}),
])
);
export type CreateInventoryViewAttributesRequestPayload = rt.TypeOf<
typeof createInventoryViewAttributesRequestPayloadRT
@ -23,3 +25,5 @@ export type CreateInventoryViewAttributesRequestPayload = rt.TypeOf<
export const createInventoryViewRequestPayloadRT = rt.type({
attributes: createInventoryViewAttributesRequestPayloadRT,
});
export type CreateInventoryViewResponsePayload = rt.TypeOf<typeof inventoryViewRT>;

View file

@ -5,28 +5,11 @@
* 2.0.
*/
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const findInventoryViewAttributesResponseRT = rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
});
const findInventoryViewResponseRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: findInventoryViewAttributesResponseRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
import { singleInventoryViewRT } from '../../../inventory_views';
export const findInventoryViewResponsePayloadRT = rt.type({
data: rt.array(findInventoryViewResponseRT),
data: rt.array(singleInventoryViewRT),
});
export type FindInventoryViewResponsePayload = rt.TypeOf<typeof findInventoryViewResponsePayloadRT>;

View file

@ -6,7 +6,10 @@
*/
import * as rt from 'io-ts';
import { inventoryViewRT } from '../../../inventory_views';
export const getInventoryViewRequestParamsRT = rt.type({
inventoryViewId: rt.string,
});
export type GetInventoryViewResposePayload = rt.TypeOf<typeof inventoryViewRT>;

View file

@ -5,16 +5,18 @@
* 2.0.
*/
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { inventoryViewAttributesRT, inventoryViewRT } from '../../../inventory_views';
export const updateInventoryViewAttributesRequestPayloadRT = rt.intersection([
rt.type({
name: nonEmptyStringRt,
}),
rt.UnknownRecord,
rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })),
]);
export const updateInventoryViewAttributesRequestPayloadRT = rt.exact(
rt.intersection([
inventoryViewAttributesRT,
rt.partial({
isDefault: rt.undefined,
isStatic: rt.undefined,
}),
])
);
export type UpdateInventoryViewAttributesRequestPayload = rt.TypeOf<
typeof updateInventoryViewAttributesRequestPayloadRT
@ -23,3 +25,5 @@ export type UpdateInventoryViewAttributesRequestPayload = rt.TypeOf<
export const updateInventoryViewRequestPayloadRT = rt.type({
attributes: updateInventoryViewAttributesRequestPayloadRT,
});
export type UpdateInventoryViewResponsePayload = rt.TypeOf<typeof inventoryViewRT>;

View file

@ -5,19 +5,89 @@
* 2.0.
*/
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import { isoToEpochRt, nonEmptyStringRt, inRangeRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import {
SnapshotCustomMetricInputRT,
SnapshotGroupByRT,
SnapshotMetricInputRT,
} from '../http_api/snapshot_api';
import { ItemTypeRT } from '../inventory_models/types';
export const inventoryViewAttributesRT = rt.intersection([
rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
export const inventoryColorPaletteRT = rt.keyof({
status: null,
temperature: null,
cool: null,
warm: null,
positive: null,
negative: null,
});
const inventoryLegendOptionsRT = rt.type({
palette: inventoryColorPaletteRT,
steps: inRangeRt(2, 18),
reverseColors: rt.boolean,
});
export const inventorySortOptionRT = rt.type({
by: rt.keyof({ name: null, value: null }),
direction: rt.keyof({ asc: null, desc: null }),
});
export const inventoryViewOptionsRT = rt.keyof({ table: null, map: null });
export const inventoryMapBoundsRT = rt.type({
min: inRangeRt(0, 1),
max: inRangeRt(0, 1),
});
export const inventoryFiltersStateRT = rt.type({
kind: rt.literal('kuery'),
expression: rt.string,
});
export const inventoryOptionsStateRT = rt.intersection([
rt.type({
accountId: rt.string,
autoBounds: rt.boolean,
boundsOverride: inventoryMapBoundsRT,
customMetrics: rt.array(SnapshotCustomMetricInputRT),
customOptions: rt.array(
rt.type({
text: rt.string,
field: rt.string,
})
),
groupBy: SnapshotGroupByRT,
metric: SnapshotMetricInputRT,
nodeType: ItemTypeRT,
region: rt.string,
sort: inventorySortOptionRT,
view: inventoryViewOptionsRT,
}),
rt.UnknownRecord,
rt.partial({ legend: inventoryLegendOptionsRT, source: rt.string, timelineOpen: rt.boolean }),
]);
export type InventoryViewAttributes = rt.TypeOf<typeof inventoryViewAttributesRT>;
export const inventoryViewBasicAttributesRT = rt.type({
name: nonEmptyStringRt,
});
const inventoryViewFlagsRT = rt.partial({ isDefault: rt.boolean, isStatic: rt.boolean });
export const inventoryViewAttributesRT = rt.intersection([
inventoryOptionsStateRT,
inventoryViewBasicAttributesRT,
inventoryViewFlagsRT,
rt.type({
autoReload: rt.boolean,
filterQuery: inventoryFiltersStateRT,
}),
rt.partial({ time: rt.number }),
]);
const singleInventoryViewAttributesRT = rt.exact(
rt.intersection([inventoryViewBasicAttributesRT, inventoryViewFlagsRT])
);
export const inventoryViewRT = rt.exact(
rt.intersection([
@ -26,10 +96,31 @@ export const inventoryViewRT = rt.exact(
attributes: inventoryViewAttributesRT,
}),
rt.partial({
updatedAt: rt.number,
updatedAt: isoToEpochRt,
version: rt.string,
}),
])
);
export const singleInventoryViewRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: singleInventoryViewAttributesRT,
}),
rt.partial({
updatedAt: isoToEpochRt,
version: rt.string,
}),
])
);
export type InventoryColorPalette = rt.TypeOf<typeof inventoryColorPaletteRT>;
export type InventoryFiltersState = rt.TypeOf<typeof inventoryFiltersStateRT>;
export type InventoryLegendOptions = rt.TypeOf<typeof inventoryLegendOptionsRT>;
export type InventoryMapBounds = rt.TypeOf<typeof inventoryMapBoundsRT>;
export type InventoryOptionsState = rt.TypeOf<typeof inventoryOptionsStateRT>;
export type InventorySortOption = rt.TypeOf<typeof inventorySortOptionRT>;
export type InventoryView = rt.TypeOf<typeof inventoryViewRT>;
export type InventoryViewAttributes = rt.TypeOf<typeof inventoryViewAttributesRT>;
export type InventoryViewOptions = rt.TypeOf<typeof inventoryViewOptionsRT>;

View file

@ -9,7 +9,7 @@ import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
export const metricsExplorerViewAttributesRT = rt.intersection([
rt.strict({
rt.type({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './types';

View file

@ -0,0 +1,70 @@
/*
* 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 {
QueryObserverBaseResult,
UseMutateAsyncFunction,
UseMutateFunction,
} from '@tanstack/react-query';
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
export type ServerError = IHttpFetchError<ResponseErrorBody>;
export interface SavedViewState<TView> {
views?: SavedViewItem[];
currentView?: TView | null;
isCreatingView: boolean;
isFetchingCurrentView: boolean;
isFetchingViews: boolean;
isUpdatingView: boolean;
}
export interface SavedViewOperations<
TView extends { id: TView['id'] },
TId extends TView['id'] = TView['id'],
TPayload = any,
TConfig = any
> {
createView: UseMutateAsyncFunction<TView, ServerError, TPayload>;
deleteViewById: UseMutateFunction<null, ServerError, string, MutationContext<TView>>;
fetchViews: QueryObserverBaseResult<SavedViewItem[]>['refetch'];
updateViewById: UseMutateAsyncFunction<TView, ServerError, UpdateViewParams<TPayload>>;
switchViewById: (id: TId) => void;
setDefaultViewById: UseMutateFunction<TConfig, ServerError, string, MutationContext<TView>>;
}
export interface SavedViewResult<
TView extends {
id: TView['id'];
},
TId extends string = '',
TPayload = any,
TConfig = any
> extends SavedViewState<TView>,
SavedViewOperations<TView, TId, TPayload, TConfig> {}
export interface UpdateViewParams<TRequestPayload> {
id: string;
attributes: TRequestPayload;
}
export interface MutationContext<TView> {
id?: string;
previousViews?: TView[];
}
export interface BasicAttributes {
name?: string;
time?: number;
isDefault?: boolean;
isStatic?: boolean;
}
export interface SavedViewItem {
id: string;
attributes: BasicAttributes;
}

View file

@ -24,21 +24,15 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiBasicTableColumn } from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { MetricsExplorerView } from '../../../common/metrics_explorer_views';
import type { InventoryView } from '../../../common/inventory_views';
import { UseInventoryViewsResult } from '../../hooks/use_inventory_views';
import { UseMetricsExplorerViewsResult } from '../../hooks/use_metrics_explorer_views';
import { SavedViewOperations, SavedViewItem } from '../../../common/saved_views';
type View = InventoryView | MetricsExplorerView;
type UseViewResult = UseInventoryViewsResult | UseMetricsExplorerViewsResult;
export interface ManageViewsFlyoutProps {
views: UseViewResult['views'];
export interface ManageViewsFlyoutProps<TSavedViewState extends SavedViewItem> {
views?: SavedViewItem[];
loading: boolean;
onClose(): void;
onMakeDefaultView: UseViewResult['setDefaultViewById'];
onSwitchView: UseViewResult['switchViewById'];
onDeleteView: UseViewResult['deleteViewById'];
onMakeDefaultView: SavedViewOperations<TSavedViewState>['setDefaultViewById'];
onSwitchView: SavedViewOperations<TSavedViewState>['switchViewById'];
onDeleteView: SavedViewOperations<TSavedViewState>['deleteViewById'];
}
interface DeleteConfimationProps {
@ -50,18 +44,18 @@ const searchConfig = {
box: { incremental: true },
};
export function ManageViewsFlyout({
export function ManageViewsFlyout<TSavedViewState extends SavedViewItem>({
onClose,
views = [],
onSwitchView,
onMakeDefaultView,
onDeleteView,
loading,
}: ManageViewsFlyoutProps) {
}: ManageViewsFlyoutProps<TSavedViewState>) {
// Add name as top level property to allow in memory search
const namedViews = useMemo(() => views.map(addOwnName), [views]);
const renderName = (name: string, item: View) => (
const renderName = (name: string, item: SavedViewItem) => (
<EuiButtonEmpty
key={item.id}
data-test-subj="infraRenderNameButton"
@ -74,7 +68,7 @@ export function ManageViewsFlyout({
</EuiButtonEmpty>
);
const renderDeleteAction = (item: View) => {
const renderDeleteAction = (item: SavedViewItem) => {
return (
<DeleteConfimation
key={item.id}
@ -86,7 +80,7 @@ export function ManageViewsFlyout({
);
};
const renderMakeDefaultAction = (item: View) => {
const renderMakeDefaultAction = (item: SavedViewItem) => {
return (
<EuiButtonIcon
key={item.id}
@ -100,7 +94,7 @@ export function ManageViewsFlyout({
);
};
const columns: Array<EuiBasicTableColumn<View>> = [
const columns: Array<EuiBasicTableColumn<SavedViewItem>> = [
{
field: 'name',
name: i18n.translate('xpack.infra.openView.columnNames.name', { defaultMessage: 'Name' }),
@ -193,4 +187,7 @@ const DeleteConfimation = ({ isDisabled, onConfirm }: DeleteConfimationProps) =>
/**
* Helpers
*/
const addOwnName = (view: View) => ({ ...view, name: view.attributes.name });
const addOwnName = <TSavedViewState extends SavedViewItem>(view: TSavedViewState) => ({
...view,
name: view.attributes.name,
});

View file

@ -10,35 +10,30 @@ import { i18n } from '@kbn/i18n';
import { EuiButton, EuiPopover, EuiListGroup, EuiListGroupItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NonEmptyString } from '@kbn/io-ts-utils';
import {
SavedViewState,
SavedViewOperations,
SavedViewItem,
BasicAttributes,
} from '../../../common/saved_views';
import { ManageViewsFlyout } from './manage_views_flyout';
import { useBoolean } from '../../hooks/use_boolean';
import { UpsertViewModal } from './upsert_modal';
import { UseInventoryViewsResult } from '../../hooks/use_inventory_views';
import { UseMetricsExplorerViewsResult } from '../../hooks/use_metrics_explorer_views';
type UseViewProps =
| 'currentView'
| 'views'
| 'isFetchingViews'
| 'isFetchingCurrentView'
| 'isCreatingView'
| 'isUpdatingView';
type UseViewResult = UseInventoryViewsResult | UseMetricsExplorerViewsResult;
type InventoryViewsResult = Pick<UseInventoryViewsResult, UseViewProps>;
type MetricsExplorerViewsResult = Pick<UseMetricsExplorerViewsResult, UseViewProps>;
interface Props<ViewState> extends InventoryViewsResult, MetricsExplorerViewsResult {
viewState: ViewState & { time?: number };
onCreateView: UseViewResult['createView'];
onDeleteView: UseViewResult['deleteViewById'];
onUpdateView: UseViewResult['updateViewById'];
onLoadViews: UseViewResult['fetchViews'];
onSetDefaultView: UseViewResult['setDefaultViewById'];
onSwitchView: UseViewResult['switchViewById'];
interface Props<TSingleSavedViewState extends SavedViewItem, TViewState>
extends SavedViewState<TSingleSavedViewState> {
viewState: TViewState & BasicAttributes;
onCreateView: SavedViewOperations<TSingleSavedViewState>['createView'];
onDeleteView: SavedViewOperations<TSingleSavedViewState>['deleteViewById'];
onUpdateView: SavedViewOperations<TSingleSavedViewState>['updateViewById'];
onLoadViews: SavedViewOperations<TSingleSavedViewState>['fetchViews'];
onSetDefaultView: SavedViewOperations<TSingleSavedViewState>['setDefaultViewById'];
onSwitchView: SavedViewOperations<TSingleSavedViewState>['switchViewById'];
}
export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
export function SavedViewsToolbarControls<TSingleSavedViewState extends SavedViewItem, TViewState>(
props: Props<TSingleSavedViewState, TViewState>
) {
const {
currentView,
views,

View file

@ -9,63 +9,29 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import {
QueryObserverBaseResult,
UseMutateAsyncFunction,
UseMutateFunction,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
import { MetricsSourceConfigurationResponse } from '../../common/metrics_sources';
import {
CreateInventoryViewAttributesRequestPayload,
UpdateInventoryViewAttributesRequestPayload,
} from '../../common/http_api/latest';
MutationContext,
SavedViewResult,
ServerError,
UpdateViewParams,
} from '../../common/saved_views';
import { MetricsSourceConfigurationResponse } from '../../common/metrics_sources';
import { CreateInventoryViewAttributesRequestPayload } from '../../common/http_api/latest';
import type { InventoryView } from '../../common/inventory_views';
import { useKibanaContextForPlugin } from './use_kibana';
import { useUrlState } from '../utils/use_url_state';
import { useSavedViewsNotifier } from './use_saved_views_notifier';
import { useSourceContext } from '../containers/metrics_source';
interface UpdateViewParams {
id: string;
attributes: UpdateInventoryViewAttributesRequestPayload;
}
export interface UseInventoryViewsResult {
views?: InventoryView[];
currentView?: InventoryView | null;
createView: UseMutateAsyncFunction<
InventoryView,
ServerError,
CreateInventoryViewAttributesRequestPayload
>;
deleteViewById: UseMutateFunction<null, ServerError, string, MutationContext>;
fetchViews: QueryObserverBaseResult<InventoryView[]>['refetch'];
updateViewById: UseMutateAsyncFunction<InventoryView, ServerError, UpdateViewParams>;
switchViewById: (id: InventoryViewId) => void;
setDefaultViewById: UseMutateFunction<
MetricsSourceConfigurationResponse,
ServerError,
string,
MutationContext
>;
isCreatingView: boolean;
isFetchingCurrentView: boolean;
isFetchingViews: boolean;
isUpdatingView: boolean;
}
type ServerError = IHttpFetchError<ResponseErrorBody>;
interface MutationContext {
id?: string;
previousViews?: InventoryView[];
}
export type UseInventoryViewsResult = SavedViewResult<
InventoryView,
InventoryViewId,
CreateInventoryViewAttributesRequestPayload,
MetricsSourceConfigurationResponse
>;
const queryKeys = {
find: ['inventory-views-find'] as const,
@ -122,7 +88,7 @@ export const useInventoryViews = (): UseInventoryViewsResult => {
MetricsSourceConfigurationResponse,
ServerError,
string,
MutationContext
MutationContext<InventoryView>
>({
mutationFn: (id) => updateSourceConfiguration({ inventoryDefaultView: id }),
/**
@ -167,7 +133,7 @@ export const useInventoryViews = (): UseInventoryViewsResult => {
const { mutateAsync: updateViewById, isLoading: isUpdatingView } = useMutation<
InventoryView,
ServerError,
UpdateViewParams
UpdateViewParams<CreateInventoryViewAttributesRequestPayload>
>({
mutationFn: ({ id, attributes }) => inventoryViews.client.updateInventoryView(id, attributes),
onError: (error) => {
@ -178,7 +144,12 @@ export const useInventoryViews = (): UseInventoryViewsResult => {
},
});
const { mutate: deleteViewById } = useMutation<null, ServerError, string, MutationContext>({
const { mutate: deleteViewById } = useMutation<
null,
ServerError,
string,
MutationContext<InventoryView>
>({
mutationFn: (id: string) => inventoryViews.client.deleteInventoryView(id),
/**
* To provide a quick feedback, we perform an optimistic update on the list

View file

@ -9,63 +9,29 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import {
QueryObserverBaseResult,
UseMutateAsyncFunction,
UseMutateFunction,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
import { MetricsSourceConfigurationResponse } from '../../common/metrics_sources';
import {
CreateMetricsExplorerViewAttributesRequestPayload,
UpdateMetricsExplorerViewAttributesRequestPayload,
} from '../../common/http_api/latest';
MutationContext,
SavedViewResult,
ServerError,
UpdateViewParams,
} from '../../common/saved_views';
import { MetricsSourceConfigurationResponse } from '../../common/metrics_sources';
import { CreateMetricsExplorerViewAttributesRequestPayload } from '../../common/http_api/latest';
import { MetricsExplorerView } from '../../common/metrics_explorer_views';
import { useKibanaContextForPlugin } from './use_kibana';
import { useUrlState } from '../utils/use_url_state';
import { useSavedViewsNotifier } from './use_saved_views_notifier';
import { useSourceContext } from '../containers/metrics_source';
interface UpdateViewParams {
id: string;
attributes: UpdateMetricsExplorerViewAttributesRequestPayload;
}
export interface UseMetricsExplorerViewsResult {
views?: MetricsExplorerView[];
currentView?: MetricsExplorerView | null;
createView: UseMutateAsyncFunction<
MetricsExplorerView,
ServerError,
CreateMetricsExplorerViewAttributesRequestPayload
>;
deleteViewById: UseMutateFunction<null, ServerError, string, MutationContext>;
fetchViews: QueryObserverBaseResult<MetricsExplorerView[]>['refetch'];
updateViewById: UseMutateAsyncFunction<MetricsExplorerView, ServerError, UpdateViewParams>;
switchViewById: (id: MetricsExplorerViewId) => void;
setDefaultViewById: UseMutateFunction<
MetricsSourceConfigurationResponse,
ServerError,
string,
MutationContext
>;
isCreatingView: boolean;
isFetchingCurrentView: boolean;
isFetchingViews: boolean;
isUpdatingView: boolean;
}
type ServerError = IHttpFetchError<ResponseErrorBody>;
interface MutationContext {
id?: string;
previousViews?: MetricsExplorerView[];
}
export type UseMetricsExplorerViewsResult = SavedViewResult<
MetricsExplorerView,
MetricsExplorerViewId,
CreateMetricsExplorerViewAttributesRequestPayload,
MetricsSourceConfigurationResponse
>;
const queryKeys = {
find: ['metrics-explorer-views-find'] as const,
@ -122,7 +88,7 @@ export const useMetricsExplorerViews = (): UseMetricsExplorerViewsResult => {
MetricsSourceConfigurationResponse,
ServerError,
string,
MutationContext
MutationContext<MetricsExplorerView>
>({
mutationFn: (id) => updateSourceConfiguration({ metricsExplorerDefaultView: id }),
/**
@ -167,7 +133,7 @@ export const useMetricsExplorerViews = (): UseMetricsExplorerViewsResult => {
const { mutateAsync: updateViewById, isLoading: isUpdatingView } = useMutation<
MetricsExplorerView,
ServerError,
UpdateViewParams
UpdateViewParams<CreateMetricsExplorerViewAttributesRequestPayload>
>({
mutationFn: ({ id, attributes }) =>
metricsExplorerViews.client.updateMetricsExplorerView(id, attributes),
@ -179,7 +145,12 @@ export const useMetricsExplorerViews = (): UseMetricsExplorerViewsResult => {
},
});
const { mutate: deleteViewById } = useMutation<null, ServerError, string, MutationContext>({
const { mutate: deleteViewById } = useMutation<
null,
ServerError,
string,
MutationContext<MetricsExplorerView>
>({
mutationFn: (id: string) => metricsExplorerViews.client.deleteMetricsExplorerView(id),
/**
* To provide a quick feedback, we perform an optimistic update on the list

View file

@ -7,14 +7,16 @@
import { i18n } from '@kbn/i18n';
import * as rt from 'io-ts';
import {
import type { InventoryMapBounds } from '../../common/inventory_views';
import type {
InfraTimerangeInput,
SnapshotGroupBy,
SnapshotMetricInput,
SnapshotNodeMetric,
SnapshotNodePath,
} from '../../common/http_api/snapshot_api';
import { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options';
import type { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options';
export type { InventoryColorPalette } from '../../common/inventory_views';
export interface InfraWaffleMapNode {
pathId: string;
@ -72,9 +74,6 @@ export const PALETTES = {
}),
};
export const InventoryColorPaletteRT = rt.keyof(PALETTES);
export type InventoryColorPalette = rt.TypeOf<typeof InventoryColorPaletteRT>;
export const StepRuleRT = rt.intersection([
rt.type({
value: rt.number,
@ -136,10 +135,7 @@ export interface InfraOptions {
wafflemap: InfraWaffleMapOptions;
}
export interface InfraWaffleMapBounds {
min: number;
max: number;
}
export type InfraWaffleMapBounds = InventoryMapBounds;
export type InfraFormatter = (value: string | number) => string;
export enum InfraFormatterType {

View file

@ -8,7 +8,7 @@
import React from 'react';
import { useInventoryViews } from '../../../../hooks/use_inventory_views';
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
import { useWaffleViewState, WaffleViewState } from '../hooks/use_waffle_view_state';
import { useWaffleViewState } from '../hooks/use_waffle_view_state';
export const SavedViews = () => {
const { viewState } = useWaffleViewState();
@ -28,7 +28,7 @@ export const SavedViews = () => {
} = useInventoryViews();
return (
<SavedViewsToolbarControls<WaffleViewState>
<SavedViewsToolbarControls
currentView={currentView}
views={views}
isFetchingViews={isFetchingViews}

View file

@ -27,8 +27,12 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import React, { SyntheticEvent, useState, useCallback, useEffect } from 'react';
import { first, last } from 'lodash';
import { InfraWaffleMapBounds, InventoryColorPalette, PALETTES } from '../../../../../lib/lib';
import { WaffleLegendOptions } from '../../hooks/use_waffle_options';
import type { WaffleLegendOptions } from '../../hooks/use_waffle_options';
import {
type InfraWaffleMapBounds,
type InventoryColorPalette,
PALETTES,
} from '../../../../../lib/lib';
import { getColorPalette } from '../../lib/get_color_palette';
import { convertBoundsToPercents } from '../../lib/convert_bounds_to_percents';
import { SwatchLabel } from './swatch_label';

View file

@ -7,7 +7,7 @@
import React from 'react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { InventoryColorPalette } from '../../../../../lib/lib';
import type { InventoryColorPalette } from '../../../../../lib/lib';
import { getColorPalette } from '../../lib/get_color_palette';
interface Props {

View file

@ -7,11 +7,15 @@
import { fromKueryExpression } from '@kbn/es-query';
import { useState, useMemo, useCallback, useEffect } from 'react';
import * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import createContainter from 'constate';
import {
type InventoryFiltersState,
inventoryFiltersStateRT,
} from '../../../../../common/inventory_views';
import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill';
import { useUrlState } from '../../../../utils/use_url_state';
import { useSourceContext } from '../../../../containers/metrics_source';
@ -26,20 +30,23 @@ const validateKuery = (expression: string) => {
return true;
};
export const DEFAULT_WAFFLE_FILTERS_STATE: WaffleFiltersState = { kind: 'kuery', expression: '' };
export const DEFAULT_WAFFLE_FILTERS_STATE: InventoryFiltersState = {
kind: 'kuery',
expression: '',
};
export const useWaffleFilters = () => {
const { createDerivedIndexPattern } = useSourceContext();
const indexPattern = createDerivedIndexPattern();
const [urlState, setUrlState] = useUrlState<WaffleFiltersState>({
const [urlState, setUrlState] = useUrlState<InventoryFiltersState>({
defaultState: DEFAULT_WAFFLE_FILTERS_STATE,
decodeUrlState,
encodeUrlState,
urlStateKey: 'waffleFilter',
});
const [state, setState] = useState<WaffleFiltersState>(urlState);
const [state, setState] = useState<InventoryFiltersState>(urlState);
useEffect(() => setUrlState(state), [setUrlState, state]);
@ -61,7 +68,7 @@ export const useWaffleFilters = () => {
[setState]
);
const applyFilterQuery = useCallback((filterQuery: WaffleFiltersState) => {
const applyFilterQuery = useCallback((filterQuery: InventoryFiltersState) => {
setState(filterQuery);
setFilterQueryDraft(filterQuery.expression);
}, []);
@ -87,14 +94,10 @@ export const useWaffleFilters = () => {
};
};
export const WaffleFiltersStateRT = rt.type({
kind: rt.literal('kuery'),
expression: rt.string,
});
export type WaffleFiltersState = rt.TypeOf<typeof WaffleFiltersStateRT>;
const encodeUrlState = WaffleFiltersStateRT.encode;
// temporary
export type WaffleFiltersState = InventoryFiltersState;
const encodeUrlState = inventoryFiltersStateRT.encode;
const decodeUrlState = (value: unknown) =>
pipe(WaffleFiltersStateRT.decode(value), fold(constant(undefined), identity));
pipe(inventoryFiltersStateRT.decode(value), fold(constant(undefined), identity));
export const WaffleFilters = createContainter(useWaffleFilters);
export const [WaffleFiltersProvider, useWaffleFiltersContext] = WaffleFilters;

View file

@ -6,23 +6,25 @@
*/
import { useCallback, useState, useEffect } from 'react';
import * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import createContainer from 'constate';
import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill';
import { InventoryColorPaletteRT } from '../../../../lib/lib';
import { InventoryViewOptions } from '../../../../../common/inventory_views/types';
import {
type InventoryLegendOptions,
type InventoryOptionsState,
type InventorySortOption,
inventoryOptionsStateRT,
} from '../../../../../common/inventory_views';
import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill';
import type {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInput,
SnapshotMetricInputRT,
SnapshotGroupByRT,
SnapshotCustomMetricInputRT,
} from '../../../../../common/http_api/snapshot_api';
import { useUrlState } from '../../../../utils/use_url_state';
import { InventoryItemType, ItemTypeRT } from '../../../../../common/inventory_models/types';
import type { InventoryItemType } from '../../../../../common/inventory_models/types';
export const DEFAULT_LEGEND: WaffleLegendOptions = {
palette: 'cool',
@ -75,7 +77,7 @@ export const useWaffleOptions = () => {
);
const changeView = useCallback(
(view: string) => setState((previous) => ({ ...previous, view })),
(view: string) => setState((previous) => ({ ...previous, view: view as InventoryViewOptions })),
[setState]
);
@ -160,51 +162,15 @@ export const useWaffleOptions = () => {
};
};
const WaffleLegendOptionsRT = rt.type({
palette: InventoryColorPaletteRT,
steps: rt.number,
reverseColors: rt.boolean,
});
export type WaffleLegendOptions = InventoryLegendOptions;
export type WaffleSortOption = InventorySortOption;
export type WaffleOptionsState = InventoryOptionsState;
export type WaffleLegendOptions = rt.TypeOf<typeof WaffleLegendOptionsRT>;
export const WaffleSortOptionRT = rt.type({
by: rt.keyof({ name: null, value: null }),
direction: rt.keyof({ asc: null, desc: null }),
});
export const WaffleOptionsStateRT = rt.intersection([
rt.type({
metric: SnapshotMetricInputRT,
groupBy: SnapshotGroupByRT,
nodeType: ItemTypeRT,
view: rt.string,
customOptions: rt.array(
rt.type({
text: rt.string,
field: rt.string,
})
),
boundsOverride: rt.type({
min: rt.number,
max: rt.number,
}),
autoBounds: rt.boolean,
accountId: rt.string,
region: rt.string,
customMetrics: rt.array(SnapshotCustomMetricInputRT),
sort: WaffleSortOptionRT,
}),
rt.partial({ source: rt.string, legend: WaffleLegendOptionsRT, timelineOpen: rt.boolean }),
]);
export type WaffleSortOption = rt.TypeOf<typeof WaffleSortOptionRT>;
export type WaffleOptionsState = rt.TypeOf<typeof WaffleOptionsStateRT>;
const encodeUrlState = (state: WaffleOptionsState) => {
return WaffleOptionsStateRT.encode(state);
const encodeUrlState = (state: InventoryOptionsState) => {
return inventoryOptionsStateRT.encode(state);
};
const decodeUrlState = (value: unknown) => {
const state = pipe(WaffleOptionsStateRT.decode(value), fold(constant(undefined), identity));
const state = pipe(inventoryOptionsStateRT.decode(value), fold(constant(undefined), identity));
if (state) {
state.source = 'url';
}

View file

@ -6,17 +6,10 @@
*/
import { useCallback } from 'react';
import {
useWaffleOptionsContext,
DEFAULT_WAFFLE_OPTIONS_STATE,
WaffleOptionsState,
} from './use_waffle_options';
import { InventoryViewAttributes } from '../../../../../common/inventory_views';
import { useWaffleOptionsContext, DEFAULT_WAFFLE_OPTIONS_STATE } from './use_waffle_options';
import { useWaffleTimeContext, DEFAULT_WAFFLE_TIME_STATE } from './use_waffle_time';
import {
useWaffleFiltersContext,
DEFAULT_WAFFLE_FILTERS_STATE,
WaffleFiltersState,
} from './use_waffle_filters';
import { useWaffleFiltersContext, DEFAULT_WAFFLE_FILTERS_STATE } from './use_waffle_filters';
export const DEFAULT_WAFFLE_VIEW_STATE: WaffleViewState = {
...DEFAULT_WAFFLE_OPTIONS_STATE,
@ -65,8 +58,8 @@ export const useWaffleViewState = () => {
};
const onViewChange = useCallback(
(newState) => {
const attributes = newState.attributes as WaffleViewState;
(newState: { attributes: WaffleViewState }) => {
const attributes = newState.attributes;
setWaffleOptionsState({
sort: attributes.sort,
@ -102,8 +95,7 @@ export const useWaffleViewState = () => {
};
};
export type WaffleViewState = WaffleOptionsState & {
time: number;
autoReload: boolean;
filterQuery: WaffleFiltersState;
};
export type WaffleViewState = Omit<
InventoryViewAttributes,
'name' | 'isDefault' | 'isStatic' | 'source'
>;

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { InventoryColorPalette, InfraWaffleMapSteppedGradientLegend } from '../../../../lib/lib';
import type {
InventoryColorPalette,
InfraWaffleMapSteppedGradientLegend,
} from '../../../../lib/lib';
import { getColorPalette } from './get_color_palette';
export const createLegend = (

View file

@ -31,7 +31,7 @@ export const SavedViews = ({ viewState }: Props) => {
} = useMetricsExplorerViews();
return (
<SavedViewsToolbarControls<MetricExplorerViewState>
<SavedViewsToolbarControls<any, MetricExplorerViewState>
currentView={currentView}
views={views}
isFetchingViews={isFetchingViews}

View file

@ -9,15 +9,18 @@ import { HttpStart } from '@kbn/core/public';
import {
CreateInventoryViewAttributesRequestPayload,
createInventoryViewRequestPayloadRT,
CreateInventoryViewResponsePayload,
FindInventoryViewResponsePayload,
findInventoryViewResponsePayloadRT,
GetInventoryViewResposePayload,
getInventoryViewUrl,
inventoryViewResponsePayloadRT,
UpdateInventoryViewAttributesRequestPayload,
UpdateInventoryViewResponsePayload,
} from '../../../common/http_api/latest';
import {
DeleteInventoryViewError,
FetchInventoryViewError,
InventoryView,
UpsertInventoryViewError,
} from '../../../common/inventory_views';
import { decodeOrThrow } from '../../../common/runtime_types';
@ -26,7 +29,7 @@ import { IInventoryViewsClient } from './types';
export class InventoryViewsClient implements IInventoryViewsClient {
constructor(private readonly http: HttpStart) {}
async findInventoryViews(): Promise<InventoryView[]> {
async findInventoryViews(): Promise<FindInventoryViewResponsePayload['data']> {
const response = await this.http.get(getInventoryViewUrl()).catch((error) => {
throw new FetchInventoryViewError(`Failed to fetch inventory views: ${error}`);
});
@ -40,7 +43,7 @@ export class InventoryViewsClient implements IInventoryViewsClient {
return data;
}
async getInventoryView(inventoryViewId: string): Promise<InventoryView> {
async getInventoryView(inventoryViewId: string): Promise<GetInventoryViewResposePayload> {
const response = await this.http.get(getInventoryViewUrl(inventoryViewId)).catch((error) => {
throw new FetchInventoryViewError(
`Failed to fetch inventory view "${inventoryViewId}": ${error}`
@ -60,7 +63,7 @@ export class InventoryViewsClient implements IInventoryViewsClient {
async createInventoryView(
inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload
): Promise<InventoryView> {
): Promise<CreateInventoryViewResponsePayload> {
const response = await this.http
.post(getInventoryViewUrl(), {
body: JSON.stringify(
@ -85,7 +88,7 @@ export class InventoryViewsClient implements IInventoryViewsClient {
async updateInventoryView(
inventoryViewId: string,
inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload
): Promise<InventoryView> {
): Promise<UpdateInventoryViewResponsePayload> {
const response = await this.http
.put(getInventoryViewUrl(inventoryViewId), {
body: JSON.stringify(

View file

@ -8,9 +8,12 @@
import { HttpStart } from '@kbn/core/public';
import {
CreateInventoryViewAttributesRequestPayload,
CreateInventoryViewResponsePayload,
FindInventoryViewResponsePayload,
GetInventoryViewResposePayload,
UpdateInventoryViewAttributesRequestPayload,
UpdateInventoryViewResponsePayload,
} from '../../../common/http_api/latest';
import type { InventoryView } from '../../../common/inventory_views';
export type InventoryViewsServiceSetup = void;
@ -23,14 +26,14 @@ export interface InventoryViewsServiceStartDeps {
}
export interface IInventoryViewsClient {
findInventoryViews(): Promise<InventoryView[]>;
getInventoryView(inventoryViewId: string): Promise<InventoryView>;
findInventoryViews(): Promise<FindInventoryViewResponsePayload['data']>;
getInventoryView(inventoryViewId: string): Promise<GetInventoryViewResposePayload>;
createInventoryView(
inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload
): Promise<InventoryView>;
): Promise<CreateInventoryViewResponsePayload>;
updateInventoryView(
inventoryViewId: string,
inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload
): Promise<InventoryView>;
): Promise<UpdateInventoryViewResponsePayload>;
deleteInventoryView(inventoryViewId: string): Promise<null>;
}

View file

@ -130,26 +130,62 @@ Status code: 404
Creates a new inventory view.
`metric.type`: `"count" | "cpu" | "diskLatency" | "diskSpaceUsage" | "load" | "memory" | "memoryFree" | "memoryTotal" | "normalizedLoad1m" | "tx" | "rx" | "logRate" | "diskIOReadBytes" | "diskIOWriteBytes" | "s3TotalRequests" | "s3NumberOfObjects" | "s3BucketSize" | "s3DownloadBytes" | "s3UploadBytes" | "rdsConnections" | "rdsQueriesExecuted" | "rdsActiveTransactions" | "rdsLatency" | "sqsMessagesVisible" | "sqsMessagesDelayed" | "sqsMessagesSent" | "sqsMessagesEmpty" | "sqsOldestMessage"`
`boundsOverride.max`: `range 0 to 1`
`boundsOverride.min`: `range 0 to 1`
`sort.by`: `"name" | "value"`
`sort.direction`: `"asc | "desc"`
`legend.pallete`: `"status" | "temperature" | "cool" | "warm" | "positive" | "negative"`
`view`: `"map" | "table"`
### Request
- **Method**: POST
- **Path**: /api/infra/inventory_views
- **Request body**:
```json
{
"attributes": {
"name": "View name",
"metric": {
"type": "cpu"
"type": "cpu"
},
"sort": {
"by": "name",
"direction": "desc"
"by": "name",
"direction": "desc"
},
//...
"groupBy": [],
"nodeType": "host",
"view": "map",
"customOptions": [],
"customMetrics": [],
"boundsOverride": {
"max": 1,
"min": 0
},
"autoBounds": true,
"accountId": "",
"region": "",
"autoReload": false,
"filterQuery": {
"expression": "",
"kind": "kuery"
},
"legend": {
"palette": "cool",
"steps": 10,
"reverseColors": false
},
"timelineOpen": false,
"name": "test-uptime"
}
}
```
```
### Response

View file

@ -5,14 +5,76 @@
* 2.0.
*/
import { isoToEpochRt, nonEmptyStringRt } from '@kbn/io-ts-utils';
import { inRangeRt, isoToEpochRt, nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { ItemTypeRT } from '../../../common/inventory_models/types';
export const inventorySavedObjectColorPaletteRT = rt.keyof({
status: null,
temperature: null,
cool: null,
warm: null,
positive: null,
negative: null,
});
const inventorySavedObjectLegendOptionsRT = rt.type({
palette: inventorySavedObjectColorPaletteRT,
steps: inRangeRt(2, 18),
reverseColors: rt.boolean,
});
export const inventorySavedObjectSortOptionRT = rt.type({
by: rt.keyof({ name: null, value: null }),
direction: rt.keyof({ asc: null, desc: null }),
});
export const inventorySavedObjectViewOptionsRT = rt.keyof({ table: null, map: null });
export const inventorySabedObjectMapBoundsRT = rt.type({
min: inRangeRt(0, 1),
max: inRangeRt(0, 1),
});
export const inventorySavedObjectFiltersStateRT = rt.type({
kind: rt.literal('kuery'),
expression: rt.string,
});
export const inventorySavedObjectOptionsStateRT = rt.intersection([
rt.type({
accountId: rt.string,
autoBounds: rt.boolean,
boundsOverride: inventorySabedObjectMapBoundsRT,
customMetrics: rt.UnknownArray,
customOptions: rt.array(
rt.type({
text: rt.string,
field: rt.string,
})
),
groupBy: rt.UnknownArray,
metric: rt.UnknownRecord,
nodeType: ItemTypeRT,
region: rt.string,
sort: inventorySavedObjectSortOptionRT,
view: inventorySavedObjectViewOptionsRT,
}),
rt.partial({
legend: inventorySavedObjectLegendOptionsRT,
source: rt.string,
timelineOpen: rt.boolean,
}),
]);
export const inventoryViewSavedObjectAttributesRT = rt.intersection([
rt.strict({
inventorySavedObjectOptionsStateRT,
rt.type({
name: nonEmptyStringRt,
autoReload: rt.boolean,
filterQuery: inventorySavedObjectFiltersStateRT,
}),
rt.UnknownRecord,
rt.partial({ time: rt.number, isDefault: rt.boolean, isStatic: rt.boolean }),
]);
export const inventoryViewSavedObjectRT = rt.intersection([
@ -25,3 +87,5 @@ export const inventoryViewSavedObjectRT = rt.intersection([
updated_at: isoToEpochRt,
}),
]);
export type InventoryViewSavedObject = rt.TypeOf<typeof inventoryViewSavedObjectRT>;

View file

@ -14,6 +14,7 @@ import {
} from '@kbn/core/server';
import Boom from '@hapi/boom';
import {
inventoryViewAttributesRT,
staticInventoryViewAttributes,
staticInventoryViewId,
} from '../../../common/inventory_views';
@ -131,10 +132,10 @@ export class InventoryViewsClient implements IInventoryViewsClient {
return this.savedObjectsClient.delete(inventoryViewSavedObjectName, inventoryViewId);
}
private mapSavedObjectToInventoryView(
savedObject: SavedObject | SavedObjectsUpdateResponse,
private mapSavedObjectToInventoryView<T>(
savedObject: SavedObject<T> | SavedObjectsUpdateResponse<T>,
defaultViewId?: string
) {
): InventoryView {
const inventoryViewSavedObject = decodeOrThrow(inventoryViewSavedObjectRT)(savedObject);
return {
@ -142,7 +143,7 @@ export class InventoryViewsClient implements IInventoryViewsClient {
version: inventoryViewSavedObject.version,
updatedAt: inventoryViewSavedObject.updated_at,
attributes: {
...inventoryViewSavedObject.attributes,
...decodeOrThrow(inventoryViewAttributesRT)(inventoryViewSavedObject.attributes),
isDefault: inventoryViewSavedObject.id === defaultViewId,
isStatic: false,
},

View file

@ -224,9 +224,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
// FLAKY: https://github.com/elastic/kibana/issues/157740
describe('Saved Views', () => {
this.tags('skipFirefox');
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
await pageObjects.infraHome.goToMetricExplorer();

View file

@ -54,7 +54,10 @@ export function InfraSavedViewsProvider({ getService }: FtrProviderContext) {
async createNewSavedView(name: string) {
await testSubjects.setValue('savedViewName', name);
await testSubjects.click('createSavedViewButton');
await testSubjects.missingOrFail('savedViews-upsertModal');
await retry.tryForTime(10 * 1000, async () => {
await testSubjects.missingOrFail('savedViews-upsertModal');
});
},
async createView(name: string) {