diff --git a/packages/kbn-io-ts-utils/src/in_range_rt/index.ts b/packages/kbn-io-ts-utils/src/in_range_rt/index.ts index b632173cb69d9..99d04b3f6abc6 100644 --- a/packages/kbn-io-ts-utils/src/in_range_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/in_range_rt/index.ts @@ -16,11 +16,15 @@ export interface InRangeBrand { export type InRange = rt.Branded; 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( + '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) => { diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts index 65e056f30e0d9..f1eb9b24ee039 100644 --- a/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts @@ -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; - -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; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts index 8bad088b00542..67a3bd7df1a70 100644 --- a/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts @@ -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; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts index 17a9820a93a4d..3452a22a45d17 100644 --- a/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts @@ -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; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts index 8e5cef06bb916..a13541c1e8a44 100644 --- a/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts @@ -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; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts index b21bafbecec18..5698ab2a0b2c9 100644 --- a/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts @@ -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; diff --git a/x-pack/plugins/infra/common/inventory_views/types.ts b/x-pack/plugins/infra/common/inventory_views/types.ts index 49979c1063efa..a493d2332f212 100644 --- a/x-pack/plugins/infra/common/inventory_views/types.ts +++ b/x-pack/plugins/infra/common/inventory_views/types.ts @@ -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 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.partial({ legend: inventoryLegendOptionsRT, source: rt.string, timelineOpen: rt.boolean }), +]); + +export const inventoryViewBasicAttributesRT = rt.type({ + name: nonEmptyStringRt, +}); + +const inventoryViewFlagsRT = rt.partial({ isDefault: rt.boolean, isStatic: rt.boolean }); export const inventoryViewAttributesRT = rt.intersection([ - rt.strict({ - name: nonEmptyStringRt, - isDefault: rt.boolean, - isStatic: rt.boolean, + inventoryOptionsStateRT, + inventoryViewBasicAttributesRT, + inventoryViewFlagsRT, + rt.type({ + autoReload: rt.boolean, + filterQuery: inventoryFiltersStateRT, }), - rt.UnknownRecord, + rt.partial({ time: rt.number }), ]); -export type InventoryViewAttributes = rt.TypeOf; +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; +export type InventoryFiltersState = rt.TypeOf; +export type InventoryLegendOptions = rt.TypeOf; +export type InventoryMapBounds = rt.TypeOf; +export type InventoryOptionsState = rt.TypeOf; +export type InventorySortOption = rt.TypeOf; export type InventoryView = rt.TypeOf; +export type InventoryViewAttributes = rt.TypeOf; +export type InventoryViewOptions = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/types.ts b/x-pack/plugins/infra/common/metrics_explorer_views/types.ts index 47ecb06ceace5..0d0c2fa3166e0 100644 --- a/x-pack/plugins/infra/common/metrics_explorer_views/types.ts +++ b/x-pack/plugins/infra/common/metrics_explorer_views/types.ts @@ -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, diff --git a/x-pack/plugins/infra/common/saved_views/index.ts b/x-pack/plugins/infra/common/saved_views/index.ts new file mode 100644 index 0000000000000..6cc0ccaa93a6d --- /dev/null +++ b/x-pack/plugins/infra/common/saved_views/index.ts @@ -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'; diff --git a/x-pack/plugins/infra/common/saved_views/types.ts b/x-pack/plugins/infra/common/saved_views/types.ts new file mode 100644 index 0000000000000..01bf806da44d9 --- /dev/null +++ b/x-pack/plugins/infra/common/saved_views/types.ts @@ -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; + +export interface SavedViewState { + 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; + deleteViewById: UseMutateFunction>; + fetchViews: QueryObserverBaseResult['refetch']; + updateViewById: UseMutateAsyncFunction>; + switchViewById: (id: TId) => void; + setDefaultViewById: UseMutateFunction>; +} + +export interface SavedViewResult< + TView extends { + id: TView['id']; + }, + TId extends string = '', + TPayload = any, + TConfig = any +> extends SavedViewState, + SavedViewOperations {} + +export interface UpdateViewParams { + id: string; + attributes: TRequestPayload; +} + +export interface MutationContext { + id?: string; + previousViews?: TView[]; +} + +export interface BasicAttributes { + name?: string; + time?: number; + isDefault?: boolean; + isStatic?: boolean; +} +export interface SavedViewItem { + id: string; + attributes: BasicAttributes; +} diff --git a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx index e503bdebafa03..67235c96a8bcc 100644 --- a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx @@ -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 { + views?: SavedViewItem[]; loading: boolean; onClose(): void; - onMakeDefaultView: UseViewResult['setDefaultViewById']; - onSwitchView: UseViewResult['switchViewById']; - onDeleteView: UseViewResult['deleteViewById']; + onMakeDefaultView: SavedViewOperations['setDefaultViewById']; + onSwitchView: SavedViewOperations['switchViewById']; + onDeleteView: SavedViewOperations['deleteViewById']; } interface DeleteConfimationProps { @@ -50,18 +44,18 @@ const searchConfig = { box: { incremental: true }, }; -export function ManageViewsFlyout({ +export function ManageViewsFlyout({ onClose, views = [], onSwitchView, onMakeDefaultView, onDeleteView, loading, -}: ManageViewsFlyoutProps) { +}: ManageViewsFlyoutProps) { // 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) => ( ); - const renderDeleteAction = (item: View) => { + const renderDeleteAction = (item: SavedViewItem) => { return ( { + const renderMakeDefaultAction = (item: SavedViewItem) => { return ( > = [ + const columns: Array> = [ { 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 = (view: TSavedViewState) => ({ + ...view, + name: view.attributes.name, +}); diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index b52d83cac60c6..11d45a51a0b2c 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -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; -type MetricsExplorerViewsResult = Pick; - -interface Props 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 + extends SavedViewState { + viewState: TViewState & BasicAttributes; + onCreateView: SavedViewOperations['createView']; + onDeleteView: SavedViewOperations['deleteViewById']; + onUpdateView: SavedViewOperations['updateViewById']; + onLoadViews: SavedViewOperations['fetchViews']; + onSetDefaultView: SavedViewOperations['setDefaultViewById']; + onSwitchView: SavedViewOperations['switchViewById']; } -export function SavedViewsToolbarControls(props: Props) { +export function SavedViewsToolbarControls( + props: Props +) { const { currentView, views, diff --git a/x-pack/plugins/infra/public/hooks/use_inventory_views.ts b/x-pack/plugins/infra/public/hooks/use_inventory_views.ts index 5bbc52e17afda..93873a307d59d 100644 --- a/x-pack/plugins/infra/public/hooks/use_inventory_views.ts +++ b/x-pack/plugins/infra/public/hooks/use_inventory_views.ts @@ -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; - fetchViews: QueryObserverBaseResult['refetch']; - updateViewById: UseMutateAsyncFunction; - switchViewById: (id: InventoryViewId) => void; - setDefaultViewById: UseMutateFunction< - MetricsSourceConfigurationResponse, - ServerError, - string, - MutationContext - >; - isCreatingView: boolean; - isFetchingCurrentView: boolean; - isFetchingViews: boolean; - isUpdatingView: boolean; -} - -type ServerError = IHttpFetchError; - -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 >({ mutationFn: (id) => updateSourceConfiguration({ inventoryDefaultView: id }), /** @@ -167,7 +133,7 @@ export const useInventoryViews = (): UseInventoryViewsResult => { const { mutateAsync: updateViewById, isLoading: isUpdatingView } = useMutation< InventoryView, ServerError, - UpdateViewParams + UpdateViewParams >({ mutationFn: ({ id, attributes }) => inventoryViews.client.updateInventoryView(id, attributes), onError: (error) => { @@ -178,7 +144,12 @@ export const useInventoryViews = (): UseInventoryViewsResult => { }, }); - const { mutate: deleteViewById } = useMutation({ + const { mutate: deleteViewById } = useMutation< + null, + ServerError, + string, + MutationContext + >({ mutationFn: (id: string) => inventoryViews.client.deleteInventoryView(id), /** * To provide a quick feedback, we perform an optimistic update on the list diff --git a/x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts b/x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts index 210a23a3b21ef..f2c9500f9ea63 100644 --- a/x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts +++ b/x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts @@ -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; - fetchViews: QueryObserverBaseResult['refetch']; - updateViewById: UseMutateAsyncFunction; - switchViewById: (id: MetricsExplorerViewId) => void; - setDefaultViewById: UseMutateFunction< - MetricsSourceConfigurationResponse, - ServerError, - string, - MutationContext - >; - isCreatingView: boolean; - isFetchingCurrentView: boolean; - isFetchingViews: boolean; - isUpdatingView: boolean; -} - -type ServerError = IHttpFetchError; - -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 >({ mutationFn: (id) => updateSourceConfiguration({ metricsExplorerDefaultView: id }), /** @@ -167,7 +133,7 @@ export const useMetricsExplorerViews = (): UseMetricsExplorerViewsResult => { const { mutateAsync: updateViewById, isLoading: isUpdatingView } = useMutation< MetricsExplorerView, ServerError, - UpdateViewParams + UpdateViewParams >({ mutationFn: ({ id, attributes }) => metricsExplorerViews.client.updateMetricsExplorerView(id, attributes), @@ -179,7 +145,12 @@ export const useMetricsExplorerViews = (): UseMetricsExplorerViewsResult => { }, }); - const { mutate: deleteViewById } = useMutation({ + const { mutate: deleteViewById } = useMutation< + null, + ServerError, + string, + MutationContext + >({ mutationFn: (id: string) => metricsExplorerViews.client.deleteMetricsExplorerView(id), /** * To provide a quick feedback, we perform an optimistic update on the list diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index a37a9af7d9320..ac6527b9cc9c3 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -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; - 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 { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx index 4547b0dbb0147..feb5283a39dcb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx @@ -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 ( - + { 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({ + const [urlState, setUrlState] = useUrlState({ defaultState: DEFAULT_WAFFLE_FILTERS_STATE, decodeUrlState, encodeUrlState, urlStateKey: 'waffleFilter', }); - const [state, setState] = useState(urlState); + const [state, setState] = useState(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; -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; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts index 8767be4f8a27e..9151e591a09f1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts @@ -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 = rt.TypeOf; - -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; -export type WaffleOptionsState = rt.TypeOf; -const encodeUrlState = (state: WaffleOptionsState) => { - return WaffleOptionsStateRT.encode(state); +export type WaffleLegendOptions = InventoryLegendOptions; +export type WaffleSortOption = InventorySortOption; +export type WaffleOptionsState = InventoryOptionsState; + +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'; } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts index 6e685a6cc105f..c1ff4c67addbb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts @@ -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' +>; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_legend.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_legend.ts index c7015764ddf24..cd37b0d8aab25 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_legend.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_legend.ts @@ -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 = ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx index 2d329f121f008..ddce0eac506fe 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx @@ -31,7 +31,7 @@ export const SavedViews = ({ viewState }: Props) => { } = useMetricsExplorerViews(); return ( - + currentView={currentView} views={views} isFetchingViews={isFetchingViews} diff --git a/x-pack/plugins/infra/public/services/inventory_views/inventory_views_client.ts b/x-pack/plugins/infra/public/services/inventory_views/inventory_views_client.ts index eeabd15e60a4b..5cfda02fa4c17 100644 --- a/x-pack/plugins/infra/public/services/inventory_views/inventory_views_client.ts +++ b/x-pack/plugins/infra/public/services/inventory_views/inventory_views_client.ts @@ -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 { + async findInventoryViews(): Promise { 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 { + async getInventoryView(inventoryViewId: string): Promise { 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 { + ): Promise { 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 { + ): Promise { const response = await this.http .put(getInventoryViewUrl(inventoryViewId), { body: JSON.stringify( diff --git a/x-pack/plugins/infra/public/services/inventory_views/types.ts b/x-pack/plugins/infra/public/services/inventory_views/types.ts index 573c144e9c441..e2e26e6ef7f5b 100644 --- a/x-pack/plugins/infra/public/services/inventory_views/types.ts +++ b/x-pack/plugins/infra/public/services/inventory_views/types.ts @@ -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; - getInventoryView(inventoryViewId: string): Promise; + findInventoryViews(): Promise; + getInventoryView(inventoryViewId: string): Promise; createInventoryView( inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload - ): Promise; + ): Promise; updateInventoryView( inventoryViewId: string, inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload - ): Promise; + ): Promise; deleteInventoryView(inventoryViewId: string): Promise; } diff --git a/x-pack/plugins/infra/server/routes/inventory_views/README.md b/x-pack/plugins/infra/server/routes/inventory_views/README.md index be7d1c3734157..6edf5a540536a 100644 --- a/x-pack/plugins/infra/server/routes/inventory_views/README.md +++ b/x-pack/plugins/infra/server/routes/inventory_views/README.md @@ -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 diff --git a/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts b/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts index 45e738f3920f1..30ab7068b7f40 100644 --- a/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts +++ b/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts @@ -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; diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts index 5efce009da410..78c7007ed1d1a 100644 --- a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts @@ -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( + savedObject: SavedObject | SavedObjectsUpdateResponse, 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, }, diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 98481732be9d5..59ea0e5d21702 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -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(); diff --git a/x-pack/test/functional/page_objects/infra_saved_views.ts b/x-pack/test/functional/page_objects/infra_saved_views.ts index 56c6e0d1354fb..218a7058f1141 100644 --- a/x-pack/test/functional/page_objects/infra_saved_views.ts +++ b/x-pack/test/functional/page_objects/infra_saved_views.ts @@ -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) {