mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
74c3658dd6
commit
d79e69a1ed
30 changed files with 496 additions and 344 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
|
|
8
x-pack/plugins/infra/common/saved_views/index.ts
Normal file
8
x-pack/plugins/infra/common/saved_views/index.ts
Normal 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';
|
70
x-pack/plugins/infra/common/saved_views/types.ts
Normal file
70
x-pack/plugins/infra/common/saved_views/types.ts
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
>;
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -31,7 +31,7 @@ export const SavedViews = ({ viewState }: Props) => {
|
|||
} = useMetricsExplorerViews();
|
||||
|
||||
return (
|
||||
<SavedViewsToolbarControls<MetricExplorerViewState>
|
||||
<SavedViewsToolbarControls<any, MetricExplorerViewState>
|
||||
currentView={currentView}
|
||||
views={views}
|
||||
isFetchingViews={isFetchingViews}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue