diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index 8ecd59063093b..3214cc98d4330 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -25,7 +25,7 @@ export const mockSourceLayer = { style: { type: 'VECTOR', properties: { - fillColor: { type: 'STATIC', options: { color: '#3cb44b' } }, + fillColor: { type: 'STATIC', options: { color: '#3185FC' } }, lineColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, lineWidth: { type: 'STATIC', options: { size: 1 } }, iconSize: { type: 'STATIC', options: { size: 6 } }, @@ -41,7 +41,7 @@ export const mockSourceLayer = { visible: true, applyGlobalQuery: true, type: 'VECTOR', - query: { query: 'source.geo.location:* and destination.geo.location:*', language: 'kuery' }, + query: { query: '', language: 'kuery' }, joins: [], }; @@ -65,7 +65,7 @@ export const mockDestinationLayer = { style: { type: 'VECTOR', properties: { - fillColor: { type: 'STATIC', options: { color: '#e6194b' } }, + fillColor: { type: 'STATIC', options: { color: '#DB1374' } }, lineColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, lineWidth: { type: 'STATIC', options: { size: 1 } }, iconSize: { type: 'STATIC', options: { size: 6 } }, @@ -81,7 +81,7 @@ export const mockDestinationLayer = { visible: true, applyGlobalQuery: true, type: 'VECTOR', - query: { query: 'source.geo.location:* and destination.geo.location:*', language: 'kuery' }, + query: { query: '', language: 'kuery' }, }; export const mockLineLayer = { diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap index e25746c4f41af..df8610c794293 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap @@ -4,7 +4,7 @@ exports[`EmbeddedMap renders correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx index ba5e196e18683..efed190825673 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { EmbeddedMap } from './embedded_map'; -import { inputsModel } from '../../store/inputs'; +import { SetQuery } from './types'; jest.mock('ui/new_platform', () => ({ npStart: { @@ -29,17 +29,7 @@ jest.mock('ui/new_platform', () => ({ describe('EmbeddedMap', () => { let applyFilterQueryFromKueryExpression: (expression: string) => void; - let setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; + let setQuery: SetQuery; beforeEach(() => { applyFilterQueryFromKueryExpression = jest.fn(expression => {}); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index baa100d245267..62a1bb15c90e1 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -9,41 +9,21 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { npStart } from 'ui/new_platform'; import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; -import uuid from 'uuid'; import styled from 'styled-components'; import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; -import { - APPLY_FILTER_ACTION, - APPLY_FILTER_TRIGGER, - CONTEXT_MENU_TRIGGER, - EmbeddablePanel, - PANEL_BADGE_TRIGGER, - ViewMode, -} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -// @ts-ignore Missing type defs as maps moves to Typescript -import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; +import { EmbeddablePanel } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; + import { Loader } from '../loader'; -import { - APPLY_SIEM_FILTER_ACTION_ID, - ApplySiemFilterAction, -} from './actions/apply_siem_filter_action'; import { useIndexPatterns } from '../ml_popover/hooks/use_index_patterns'; -import { getLayerList } from './map_config'; import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { getIndexPatternTitleIdMapping } from '../ml_popover/helpers'; import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; -import { - EmbeddableOutput, - IEmbeddable, -} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/embeddables'; -import { IndexPatternMapping, MapEmbeddableInput } from './types'; +import { MapEmbeddable, SetQuery } from './types'; import * as i18n from './translations'; -import { inputsModel } from '../../store/inputs'; - -// Used for setQuery to get a hook for when the user requests a refresh. Scope to page type if using map elsewhere -const ID = 'embeddedMap'; +import { useStateToaster } from '../toasters'; +import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers'; const EmbeddableWrapper = styled(EuiFlexGroup)` position: relative; @@ -60,118 +40,62 @@ export interface EmbeddedMapProps { queryExpression: string; startDate: number; endDate: number; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; + setQuery: SetQuery; } export const EmbeddedMap = React.memo( ({ applyFilterQueryFromKueryExpression, queryExpression, startDate, endDate, setQuery }) => { - const [embeddable, setEmbeddable] = React.useState | null>(null); + const [embeddable, setEmbeddable] = React.useState(null); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); + const [isIndexError, setIsIndexError] = useState(false); + const [, dispatchToaster] = useStateToaster(); const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns(); const [siemDefaultIndices] = useKibanaUiSetting(DEFAULT_INDEX_KEY); - const loadEmbeddable = async (id: string, indexPatterns: IndexPatternMapping[]) => { + const setupEmbeddable = async () => { + // Configure Embeddables API try { - const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); - - const state = { - layerList: getLayerList(indexPatterns), - title: i18n.MAP_TITLE, - }; - const input = { - id, - filters: [], - hidePanelTitles: true, - query: { query: queryExpression, language: 'kuery' }, - refreshConfig: { value: 0, pause: true }, - timeRange: { - from: new Date(startDate).toISOString(), - to: new Date(endDate).toISOString(), - }, - viewMode: ViewMode.VIEW, - isLayerTOCOpen: false, - openTOCDetails: [], - hideFilterActions: false, - mapCenter: { lon: -1.05469, lat: 15.96133, zoom: 1 }, - }; - - // @ts-ignore method added in https://github.com/elastic/kibana/pull/43878 - const embeddableObject = await factory.createFromState(state, input); - - // Wire up to app refresh action - setQuery({ - id: ID, - inspect: null, - loading: false, - refetch: embeddableObject.reload, - }); - - setEmbeddable(embeddableObject); + setupEmbeddablesAPI(applyFilterQueryFromKueryExpression); } catch (e) { - // TODO: Throw toast https://github.com/elastic/siem-team/issues/449 + displayErrorToast(i18n.ERROR_CONFIGURING_EMBEDDABLES_API, e.message, dispatchToaster); + setIsLoading(false); + setIsError(true); + return false; } - }; - /** - * Temporary Embeddables API configuration override until ability to edit actions is addressed: - * https://github.com/elastic/kibana/issues/43643 - */ - const setupEmbeddablesAPI = (): boolean => { + // Ensure at least one `siem:defaultIndex` index pattern exists before trying to import + const matchingIndexPatterns = kibanaIndexPatterns.filter(ip => + siemDefaultIndices.includes(ip.attributes.title) + ); + if (matchingIndexPatterns.length === 0) { + setIsLoading(false); + setIsIndexError(true); + return; + } + + // Create & set Embeddable try { - const actions = start.getTriggerActions(APPLY_FILTER_TRIGGER); - const actionLoaded = actions.some(a => a.id === APPLY_SIEM_FILTER_ACTION_ID); - if (!actionLoaded) { - const siemFilterAction = new ApplySiemFilterAction({ - applyFilterQueryFromKueryExpression, - }); - start.registerAction(siemFilterAction); - start.attachAction(APPLY_FILTER_TRIGGER, siemFilterAction.id); - - start.detachAction(CONTEXT_MENU_TRIGGER, 'CUSTOM_TIME_RANGE'); - start.detachAction(PANEL_BADGE_TRIGGER, 'CUSTOM_TIME_RANGE_BADGE'); - start.detachAction(APPLY_FILTER_TRIGGER, APPLY_FILTER_ACTION); - } - return true; + const embeddableObject = await createEmbeddable( + getIndexPatternTitleIdMapping(matchingIndexPatterns), + queryExpression, + startDate, + endDate, + setQuery + ); + setEmbeddable(embeddableObject); } catch (e) { - // TODO: Throw toast https://github.com/elastic/siem-team/issues/449 - return false; + displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, e.message, dispatchToaster); + setIsError(true); } + setIsLoading(false); }; // Initial Load useEffect useEffect(() => { - setIsLoading(true); - - const importIfNotExists = async () => { - const matchingIndexPatterns = kibanaIndexPatterns.filter(ip => - siemDefaultIndices.includes(ip.attributes.title) - ); - - const setupSuccessfully = setupEmbeddablesAPI(); - - // Ensure at least one `siem:defaultIndex` index pattern exists before trying to import - if (matchingIndexPatterns.length === 0 || !setupSuccessfully) { - setIsLoading(false); - setIsError(true); - return; - } - - await loadEmbeddable(uuid.v4(), getIndexPatternTitleIdMapping(matchingIndexPatterns)); - setIsLoading(false); - }; - - if (!loadingKibanaIndexPatterns && kibanaIndexPatterns.length > 0) { - importIfNotExists(); + if (!loadingKibanaIndexPatterns) { + setupEmbeddable(); } }, [loadingKibanaIndexPatterns, kibanaIndexPatterns]); @@ -194,11 +118,12 @@ export const EmbeddedMap = React.memo( } }, [startDate, endDate]); - return ( + return isError ? null : ( <> {embeddable != null ? ( ( inspector={npStart.plugins.inspector} SavedObjectFinder={SavedObjectFinder} /> - ) : !isLoading && isError ? ( - + ) : !isLoading && isIndexError ? ( + ) : ( - + )} diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx new file mode 100644 index 0000000000000..9be78cee87288 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers'; +import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; + +jest.mock('../../lib/settings/use_kibana_ui_setting'); + +jest.mock( + '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy', + () => ({ + start: { + getTriggerActions: jest.fn(() => []), + registerAction: jest.fn(), + attachAction: jest.fn(), + detachAction: jest.fn(), + getEmbeddableFactory: () => ({ + createFromState: () => ({ + reload: jest.fn(), + }), + }), + }, + }) +); + +jest.mock('uuid', () => { + return { + v4: jest.fn(() => '9e1f72a9-7c73-4b7f-a562-09940f7daf4a'), + }; +}); + +describe('embedded_map_helpers', () => { + describe('displayErrorToast', () => { + test('dispatches toast with correct title and message', () => { + const mockToast = { + toast: { + color: 'danger', + errors: ['message'], + iconType: 'alert', + id: '9e1f72a9-7c73-4b7f-a562-09940f7daf4a', + title: 'Title', + }, + type: 'addToaster', + }; + const dispatchToasterMock = jest.fn(); + displayErrorToast('Title', 'message', dispatchToasterMock); + expect(dispatchToasterMock.mock.calls[0][0]).toEqual(mockToast); + }); + }); + + describe('setupEmbeddablesAPI', () => { + test('attaches SIEM_FILTER_ACTION, and detaches extra UI actions', () => { + const applyFilterMock = jest.fn(); + setupEmbeddablesAPI(applyFilterMock); + expect(start.registerAction).toHaveBeenCalledTimes(1); + expect(start.detachAction).toHaveBeenCalledTimes(3); + }); + }); + + describe('createEmbeddable', () => { + test('attaches refresh action', async () => { + const setQueryMock = jest.fn(); + await createEmbeddable([], '', 0, 0, setQueryMock); + expect(setQueryMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx new file mode 100644 index 0000000000000..a6cc9f9b8b00b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { ActionToaster, AppToast } from '../toasters'; +import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { + APPLY_FILTER_TRIGGER, + CONTEXT_MENU_TRIGGER, + PANEL_BADGE_TRIGGER, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/triggers'; +import { + APPLY_SIEM_FILTER_ACTION_ID, + ApplySiemFilterAction, +} from './actions/apply_siem_filter_action'; +import { APPLY_FILTER_ACTION } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions'; +import { IndexPatternMapping, MapEmbeddable, SetQuery } from './types'; +import { getLayerList } from './map_config'; +import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +// @ts-ignore Missing type defs as maps moves to Typescript +import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; +import * as i18n from './translations'; + +/** + * Displays an error toast for the provided title and message + * + * @param errorTitle Title of error to display in toaster and modal + * @param errorMessage Message to display in error modal when clicked + * @param dispatchToaster provided by useStateToaster() + */ +export const displayErrorToast = ( + errorTitle: string, + errorMessage: string, + dispatchToaster: React.Dispatch +) => { + const toast: AppToast = { + id: uuid.v4(), + title: errorTitle, + color: 'danger', + iconType: 'alert', + errors: [errorMessage], + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; + +/** + * Temporary Embeddables API configuration override until ability to edit actions is addressed: + * https://github.com/elastic/kibana/issues/43643 + * + * @param applyFilterQueryFromKueryExpression function for updating KQL as provided by NetworkFilter + * + * @throws Error if action is already registered + */ +export const setupEmbeddablesAPI = ( + applyFilterQueryFromKueryExpression: (expression: string) => void +) => { + try { + const actions = start.getTriggerActions(APPLY_FILTER_TRIGGER); + const actionLoaded = actions.some(a => a.id === APPLY_SIEM_FILTER_ACTION_ID); + if (!actionLoaded) { + const siemFilterAction = new ApplySiemFilterAction({ + applyFilterQueryFromKueryExpression, + }); + start.registerAction(siemFilterAction); + start.attachAction(APPLY_FILTER_TRIGGER, siemFilterAction.id); + + start.detachAction(CONTEXT_MENU_TRIGGER, 'CUSTOM_TIME_RANGE'); + start.detachAction(PANEL_BADGE_TRIGGER, 'CUSTOM_TIME_RANGE_BADGE'); + start.detachAction(APPLY_FILTER_TRIGGER, APPLY_FILTER_ACTION); + } + } catch (e) { + throw e; + } +}; + +/** + * Creates MapEmbeddable with provided initial configuration + * + * @param indexPatterns list of index patterns to configure layers for + * @param queryExpression initial query constraints as an expression + * @param startDate + * @param endDate + * @param setQuery function as provided by the GlobalTime component for reacting to refresh + * + * @throws Error if EmbeddableFactory does not exist + */ +export const createEmbeddable = async ( + indexPatterns: IndexPatternMapping[], + queryExpression: string, + startDate: number, + endDate: number, + setQuery: SetQuery +): Promise => { + const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + + const state = { + layerList: getLayerList(indexPatterns), + title: i18n.MAP_TITLE, + }; + const input = { + id: uuid.v4(), + filters: [], + hidePanelTitles: true, + query: { query: queryExpression, language: 'kuery' }, + refreshConfig: { value: 0, pause: true }, + timeRange: { + from: new Date(startDate).toISOString(), + to: new Date(endDate).toISOString(), + }, + viewMode: ViewMode.VIEW, + isLayerTOCOpen: false, + openTOCDetails: [], + hideFilterActions: false, + mapCenter: { lon: -1.05469, lat: 15.96133, zoom: 1 }, + }; + + // @ts-ignore method added in https://github.com/elastic/kibana/pull/43878 + const embeddableObject = await factory.createFromState(state, input); + + // Wire up to app refresh action + setQuery({ + id: 'embeddedMap', // Scope to page type if using map elsewhere + inspect: null, + loading: false, + refetch: embeddableObject.reload, + }); + + return embeddableObject; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts index 492a33b549d42..09b497a6fd907 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts @@ -60,7 +60,7 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string style: { type: 'VECTOR', properties: { - fillColor: { type: 'STATIC', options: { color: '#3cb44b' } }, + fillColor: { type: 'STATIC', options: { color: '#3185FC' } }, lineColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, lineWidth: { type: 'STATIC', options: { size: 1 } }, iconSize: { type: 'STATIC', options: { size: 6 } }, @@ -76,7 +76,7 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string visible: true, applyGlobalQuery: true, type: 'VECTOR', - query: { query: 'source.geo.location:* and destination.geo.location:*', language: 'kuery' }, + query: { query: '', language: 'kuery' }, joins: [], }); @@ -107,7 +107,7 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s style: { type: 'VECTOR', properties: { - fillColor: { type: 'STATIC', options: { color: '#e6194b' } }, + fillColor: { type: 'STATIC', options: { color: '#DB1374' } }, lineColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, lineWidth: { type: 'STATIC', options: { size: 1 } }, iconSize: { type: 'STATIC', options: { size: 6 } }, @@ -123,7 +123,7 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s visible: true, applyGlobalQuery: true, type: 'VECTOR', - query: { query: 'source.geo.location:* and destination.geo.location:*', language: 'kuery' }, + query: { query: '', language: 'kuery' }, }); /** diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts index df84b68e203cb..fb07924f68f1d 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts @@ -7,12 +7,26 @@ import { i18n } from '@kbn/i18n'; export const MAP_TITLE = i18n.translate( - 'xpack.siem.components.embeddables.maps.embeddablePanelTitle', + 'xpack.siem.components.embeddables.embeddedMap.embeddablePanelTitle', { defaultMessage: 'Source -> Destination Point-to-Point Map', } ); +export const ERROR_CONFIGURING_EMBEDDABLES_API = i18n.translate( + 'xpack.siem.components.embeddables.embeddedMap.errorConfiguringEmbeddableApiTitle', + { + defaultMessage: 'Error configuring Embeddables API', + } +); + +export const ERROR_CREATING_EMBEDDABLE = i18n.translate( + 'xpack.siem.components.embeddables.embeddedMap.errorCreatingMapEmbeddableTitle', + { + defaultMessage: 'Error creating Map Embeddable', + } +); + export const ERROR_TITLE = i18n.translate( 'xpack.siem.components.embeddables.indexPatternsMissingPrompt.errorTitle', { diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts index 62f99688f9f8b..92430b569b15a 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts @@ -6,7 +6,12 @@ import { Filter as ESFilterType } from '@kbn/es-query'; import { TimeRange } from 'ui/timefilter'; -import { EmbeddableInput } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { inputsModel } from '../../store/inputs'; export interface MapEmbeddableInput extends EmbeddableInput { filters: ESFilterType[]; @@ -21,7 +26,16 @@ export interface MapEmbeddableInput extends EmbeddableInput { timeRange?: TimeRange; } +export type MapEmbeddable = IEmbeddable; + export interface IndexPatternMapping { title: string; id: string; } + +export type SetQuery = (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; +}) => void;