diff --git a/tsconfig.base.json b/tsconfig.base.json index 787992f6e2133..aaa195b4a304d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1058,6 +1058,8 @@ "@kbn/alerting-plugin/*": ["x-pack/plugins/alerting/*"], "@kbn/apm-plugin": ["x-pack/plugins/apm"], "@kbn/apm-plugin/*": ["x-pack/plugins/apm/*"], + "@kbn/asset-inventory-plugin": ["x-pack/plugins/asset_inventory"], + "@kbn/asset-inventory-plugin/*": ["x-pack/plugins/asset_inventory/*"], "@kbn/banners-plugin": ["x-pack/plugins/banners"], "@kbn/banners-plugin/*": ["x-pack/plugins/banners/*"], "@kbn/canvas-plugin": ["x-pack/plugins/canvas"], diff --git a/x-pack/plugins/asset_inventory/common/debug_log.ts b/x-pack/plugins/asset_inventory/common/debug_log.ts new file mode 100644 index 0000000000000..fd819c0d1ecd5 --- /dev/null +++ b/x-pack/plugins/asset_inventory/common/debug_log.ts @@ -0,0 +1,14 @@ +/* + * 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 function debug(...args: any[]) { + if (process.env.NODE_ENV === 'production') { + return; + } + // eslint-disable-next-line no-console + console.log('[DEBUG LOG]', ...args); +} diff --git a/x-pack/plugins/asset_inventory/common/types_api.ts b/x-pack/plugins/asset_inventory/common/types_api.ts new file mode 100644 index 0000000000000..b86a7ce0e85d4 --- /dev/null +++ b/x-pack/plugins/asset_inventory/common/types_api.ts @@ -0,0 +1,109 @@ +/* + * 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 type AssetKind = 'unknown' | 'node'; +export type AssetType = 'k8s.pod' | 'k8s.cluster' | 'k8s.node'; + +export interface ECSDocument { + '@timestamp': string; + kubernetes?: EcsKubernetesFieldset; + orchestrator?: EcsOrchestratorFieldset; + cloud?: EcsCloudFieldset; +} + +export type AssetStatus = 'CREATING' | 'ACTIVE' | 'DELETING' | 'FAILED' | 'UPDATING' | 'PENDING'; + +export interface Asset extends ECSDocument { + 'asset.collection_version'?: string; + 'asset.ean': string; + 'asset.id': string; + 'asset.kind': AssetKind; + 'asset.name'?: string; + 'asset.type': AssetType; + 'asset.status'?: AssetStatus; + 'asset.parents'?: string | string[]; + 'asset.children'?: string | string[]; + 'asset.namespace'?: string; +} + +export interface K8sPod extends ECSDocument { + id: string; + name: string; + ean: string; + node?: string; +} + +export interface K8sNode extends ECSDocument { + id: string; + name: string; + ean: string; + pods?: K8sPod[]; + cluster?: string; +} + +export interface K8sCluster extends ECSDocument { + name: string; + nodes: K8sNode[]; + status: string; + version: string; +} + +export interface EcsKubernetesFieldset { + namespace?: string; + pod?: { + name: string; + uid: string; + start_time?: Date; + }; + node?: { + name: string; + start_time?: Date; + }; +} + +// See: https://www.elastic.co/guide/en/ecs/current/ecs-orchestrator.html +export interface EcsOrchestratorFieldset { + api_version?: string; + namespace?: string; + organization?: string; + type?: string; + cluster?: { + id?: string; + name?: string; + url?: string; + version?: string; + }; + resource?: { + id?: string; + ip?: string; + name?: string; + type?: string; + parent?: { + type?: string; + }; + }; +} + +export type CloudProviderName = 'aws' | 'gcp' | 'azure' | 'other' | 'unknown' | 'none'; + +export interface EcsCloudFieldset { + provider: CloudProviderName; + region?: string; + service?: { + name?: string; + }; +} + +export interface AssetFilters { + type?: AssetType; + kind?: AssetKind; + ean?: string; + id?: string; + typeLike?: string; + eanLike?: string; + collectionVersion?: number | 'latest' | 'all'; +} diff --git a/x-pack/plugins/asset_inventory/kibana.json b/x-pack/plugins/asset_inventory/kibana.json new file mode 100644 index 0000000000000..e1001df0d804d --- /dev/null +++ b/x-pack/plugins/asset_inventory/kibana.json @@ -0,0 +1,30 @@ +{ + "id": "assetInventory", + "owner": { + "name": "TBD" + }, + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": [ + "xpack", + "assetInventory" + ], + "optionalPlugins": [ + ], + "requiredPlugins": [ + "alerting", + "cases", + "data", + "dataViews", + "features", + "inspector", + "unifiedSearch", + "usageCollection" + ], + "ui": true, + "server": true, + "requiredBundles": [ + "kibanaReact", + "kibanaUtils" + ] +} diff --git a/x-pack/plugins/asset_inventory/public/app/index.tsx b/x-pack/plugins/asset_inventory/public/app/index.tsx new file mode 100644 index 0000000000000..3377729a8b53d --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/app/index.tsx @@ -0,0 +1,90 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Route, Router, Switch } from 'react-router-dom'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; + +import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; + +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { AssetInventoryPublicPluginsStart } from '../plugin'; +import { routes } from './routes'; +import { AssetFilterContextProvider } from '../hooks/asset_filters'; + +function App() { + return ( + <> + + {routes.map((routeProps) => { + return ; + })} + + + ); +} + +export function renderApp({ + core, + config, + plugins, + appMountParameters, + usageCollection, + isDev, +}: { + core: CoreStart; + config: {}; + plugins: AssetInventoryPublicPluginsStart; + appMountParameters: AppMountParameters; + usageCollection: UsageCollectionSetup; + isDev?: boolean; +}) { + const { element, history, theme$ } = appMountParameters; + const i18nCore = core.i18n; + const isDarkMode = core.uiSettings.get('theme:darkMode'); + + core.chrome.setHelpExtension({ + appName: i18n.translate('xpack.observability.feedbackMenu.appName', { + defaultMessage: 'Observability', + }), + links: [{ linkType: 'discuss', href: 'https://ela.st/observability-discuss' }], + }); + + // ensure all divs are .kbnAppWrappers + element.classList.add(APP_WRAPPER_CLASS); + + const ApplicationUsageTrackingProvider = + usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; + + ReactDOM.render( + + + + + + + + + + + + + + + , + element + ); + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/asset_inventory/public/app/routes.ts b/x-pack/plugins/asset_inventory/public/app/routes.ts new file mode 100644 index 0000000000000..aad421fc2e4a4 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/app/routes.ts @@ -0,0 +1,29 @@ +/* + * 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 { RouteProps } from 'react-router-dom'; +import { AssetInventoryListPage } from '../pages/asset_inventory_list_page'; +import { K8sClustersListPage } from '../pages/k8s/clusters_list_page'; +import { K8sClusterPage } from '../pages/k8s/cluster_page'; + +export const routes: RouteProps[] = [ + { + path: '/', + exact: true, + component: AssetInventoryListPage, + }, + { + path: '/k8s/clusters', + exact: true, + component: K8sClustersListPage, + }, + { + path: '/k8s/clusters/:name', + exact: true, + component: K8sClusterPage, + }, +]; diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx new file mode 100644 index 0000000000000..602e915f875d5 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx @@ -0,0 +1,84 @@ +/* + * 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 { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; +import { AssetFilters } from '../../common/types_api'; +import { useAssetFilters } from '../hooks/asset_filters'; + +interface AutocompleteOptions { + field: string; + label: string; + filtersKey: keyof AssetFilters; + allowMultipleValues?: boolean; + placeholder?: string; + fullWidth?: boolean; +} + +interface FieldValueResult { + key: string; + doc_count: number; +} + +export function AssetFilterAutocomplete({ + field, + label, + filtersKey, + allowMultipleValues = false, + placeholder, + fullWidth = false, +}: AutocompleteOptions) { + const [options, setOptions] = useState([]); + const [selected, setSelected] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const { filters, setFilters } = useAssetFilters(); + + const { collectionVersion } = filters; + + useEffect(() => { + async function retrieve() { + setIsLoading(true); + const response = await axios.get<{ results: FieldValueResult[] }>( + `/local/api/asset-inventory/field-values?field=${field}&version=${ + collectionVersion || 'all' + }` + ); + + if (response.data.results) { + const newOptions: EuiComboBoxOptionOption[] = response.data.results.map((result) => ({ + label: `${result.key} (${result.doc_count})`, + value: result.key, + })); + setOptions(newOptions); + } + setIsLoading(false); + } + + retrieve(); + }, [field, collectionVersion]); + + useEffect(() => { + const selectedValues = selected.map((option) => option.value); + const values = allowMultipleValues ? selectedValues : selectedValues[0]; + setFilters((prevFilters) => ({ ...prevFilters, [filtersKey]: values })); + }, [selected, allowMultipleValues, filtersKey, setFilters]); + + return ( + + setSelected(newOptions)} + placeholder={placeholder} + fullWidth={fullWidth} + /> + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx new file mode 100644 index 0000000000000..d02d7e07c6882 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx @@ -0,0 +1,63 @@ +/* + * 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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AssetFilterAutocomplete } from './asset_filter_autocomplete'; + +export function AssetFilterControls() { + return ( + <> + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx new file mode 100644 index 0000000000000..12b7c25f766af --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx @@ -0,0 +1,48 @@ +/* + * 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 { EuiFormRow, EuiSelect, EuiSelectOption, useGeneratedHtmlId } from '@elastic/eui'; +import React from 'react'; +import { AssetFilters } from '../../common/types_api'; +import { useAssetFilters } from '../hooks/asset_filters'; + +interface FilterSelectBoxOptions { + options: Array; + label?: string; + filterKey: keyof AssetFilters; + value: string | undefined; +} + +export function AssetFilterSelectBox({ + value = '', + options, + label, + filterKey, +}: FilterSelectBoxOptions) { + const basicSelectId = useGeneratedHtmlId({ prefix: 'asset-filter-select_' }); + const { setFilters } = useAssetFilters(); + + const euiSelectOptions = options.map((option) => { + if (typeof option === 'string') { + return { value: option, text: option }; + } else { + return option; + } + }); + + return ( + + { + setFilters((filters) => ({ ...filters, [filterKey]: e.target.value })); + }} + /> + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/assets_table.tsx b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx new file mode 100644 index 0000000000000..d81c0f3cf3ee5 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx @@ -0,0 +1,65 @@ +/* + * 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 React from 'react'; +import { EuiBasicTable } from '@elastic/eui'; +import { Asset } from '../../common/types_api'; + +interface AssetsTableProps { + assets: Asset[]; +} + +const columns = [ + { + field: '@timestamp', + name: 'Timestamp', + render: (date: Asset['@timestamp']) => { + const d = new Date(date); + return d.toLocaleString(); + }, + width: '200px', + }, + { + field: 'asset.ean', + name: 'EAN (Elastic Asset Name)', + sortable: true, + width: '300px', + }, + { + field: 'asset.kind', + name: 'Asset Kind', + sortable: true, + }, + { + field: 'asset.type', + name: 'Asset Type', + sortable: true, + }, + { + field: 'asset.id', + name: 'Asset Original ID', + sortable: true, + }, + { + field: 'asset.parents', + name: '# of parents', + render: (parents: Asset['asset.parents']) => parents?.length || 0, + width: '100px', + }, + { + field: 'asset.children', + name: '# of children', + render: (children: Asset['asset.children']) => children?.length || 0, + width: '100px', + }, +]; + +export function AssetsTable({ assets }: AssetsTableProps) { + return ( + tableCaption="Asset Inventory Demo" items={assets} columns={columns} /> + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx b/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx new file mode 100644 index 0000000000000..b55eb37af62fb --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx @@ -0,0 +1,47 @@ +/* + * 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 { EuiDescriptionList, EuiPageTemplate } from '@elastic/eui'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { K8sCluster } from '../../common/types_api'; + +export function K8sClusterInfo({ cluster }: { cluster: K8sCluster }) { + const list = [ + { + title: 'Cluster name', + description: cluster.name, + }, + { + title: 'Status', + description: cluster.status, + }, + { + title: 'Version', + description: cluster.version, + }, + { + title: 'Nodes', + description: ( +
    + {cluster.nodes.map((node) => ( +
  • + {node.name} +
  • + ))} +
+ ), + }, + ]; + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx new file mode 100644 index 0000000000000..b53dc920a2d0e --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx @@ -0,0 +1,75 @@ +/* + * 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 { EuiHealth, EuiIcon, EuiInMemoryTable } from '@elastic/eui'; +import { capitalize } from 'lodash'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { AssetStatus, CloudProviderName, K8sCluster, K8sNode } from '../../common/types_api'; + +const cloudIconMap: Record = { + gcp: 'logoGCP', + aws: 'logoAWS', + azure: 'logoAzure', + other: 'questionInCircle', + unknown: 'questionInCircle', + none: 'crossInACircleFilled', +}; + +const statusMap: Record = { + ACTIVE: 'success', + CREATING: 'subdued', + DELETING: 'subdued', + FAILED: 'danger', + UPDATING: 'subdued', + PENDING: 'warning', +}; + +export function K8sClustersTable({ clusters }: { clusters: K8sCluster[] }) { + const columns = [ + { + field: 'name', + name: 'Cluster name', + sortable: true, + width: '400px', + render: (name: string) => { + return {name}; + }, + }, + { + field: 'status', + name: 'Status', + render: (status: AssetStatus) => ( + {capitalize(status)} + ), + }, + { + field: 'cloud', + name: 'Provider', + render: (cloud: K8sCluster['cloud']) => ( + + ), + }, + { + field: 'cloud', + name: 'Region', + render: (cloud: K8sCluster['cloud']) => cloud?.region || 'unknown', + }, + { + field: 'version', + name: 'Version', + }, + { + field: 'nodes', + name: 'Nodes', + render: (nodes: K8sNode[]) => <>{nodes.length}, + }, + ]; + + // @ts-ignore + return ; +} diff --git a/x-pack/plugins/asset_inventory/public/components/page_template.tsx b/x-pack/plugins/asset_inventory/public/components/page_template.tsx new file mode 100644 index 0000000000000..c595bb84e1de0 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/page_template.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; +import { EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; + +export function PageTemplate(props: EuiPageTemplateProps) { + return ; +} diff --git a/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx b/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx new file mode 100644 index 0000000000000..ffb14368e5875 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx @@ -0,0 +1,57 @@ +/* + * 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 React, { useContext, useState, useEffect } from 'react'; +import { AssetFilters } from '../../common/types_api'; +import { convertAssetFiltersToQueryString } from '../lib/convert_asset_filters_to_query_string'; + +export type FilterSetter = React.Dispatch>; + +export interface AssetFilterContextValue { + filters: AssetFilters; + setFilters: FilterSetter; + filtersQS: string; +} + +const AssetFilterContext = React.createContext({ + filters: {}, + setFilters: () => null, + filtersQS: '', +}); + +const AssetFilterContextConsumer = AssetFilterContext.Consumer; + +const AssetFilterContextProvider: React.FC<{}> = ({ children }) => { + const [filters, setFilters] = useState({}); + const [filtersQS, setFiltersQS] = useState(''); + + useEffect(() => { + setFiltersQS(convertAssetFiltersToQueryString(filters)); + }, [filters]); + + return ( + + {children} + + ); +}; + +const useAssetFilters = () => { + return useContext(AssetFilterContext); +}; + +export { + AssetFilterContext, + AssetFilterContextProvider, + AssetFilterContextConsumer, + useAssetFilters, +}; diff --git a/x-pack/plugins/asset_inventory/public/index.ts b/x-pack/plugins/asset_inventory/public/index.ts new file mode 100644 index 0000000000000..919c10b2b78ec --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/index.ts @@ -0,0 +1,32 @@ +/* + * 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; + +import { + Plugin, + AssetInventoryPublicPluginsStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicStart, + AssetInventoryPublicSetup, +} from './plugin'; + +export type { + AssetInventoryPublicSetup, + AssetInventoryPublicStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicPluginsStart, +}; + +export const plugin: PluginInitializer< + AssetInventoryPublicSetup, + AssetInventoryPublicStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicPluginsStart +> = (initializerContext: PluginInitializerContext) => { + return new Plugin(initializerContext); +}; diff --git a/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts b/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts new file mode 100644 index 0000000000000..cb8089a8ad9eb --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts @@ -0,0 +1,14 @@ +/* + * 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 { AssetFilters } from '../../common/types_api'; + +export function convertAssetFiltersToQueryString(filters: AssetFilters) { + const keys = Object.keys(filters) as unknown as Array; + const definedKeys = keys.filter((k) => typeof filters[k] !== 'undefined'); + return definedKeys.map((k) => `${k}=${filters[k]}`).join('&'); +} diff --git a/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx new file mode 100644 index 0000000000000..fc32bc30f59e3 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx @@ -0,0 +1,57 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { EuiButton, EuiPageTemplate, EuiSpacer } from '@elastic/eui'; +import axios from 'axios'; +import { useHistory } from 'react-router-dom'; +import { PageTemplate } from '../components/page_template'; +import { Asset } from '../../common/types_api'; +import { AssetsTable } from '../components/assets_table'; +import { AssetFilterControls } from '../components/asset_filter_controls'; +import { useAssetFilters } from '../hooks/asset_filters'; + +export function AssetInventoryListPage() { + const [assets, setAssets] = useState([]); + const { filtersQS } = useAssetFilters(); + const history = useHistory(); + + useEffect(() => { + // console.log('Filters changed, new qs:', filtersQS); + + async function retrieve() { + const response = await axios.get(`/local/api/asset-inventory?${filtersQS}`); + if (response.data && response.data.assets) { + setAssets(response.data.assets); + } + } + retrieve(); + }, [filtersQS]); + + return ( + + ) => { + e.preventDefault(); + history.push('/k8s/clusters'); + }} + > + K8s Clusters + , + ]} + /> + + + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx b/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx new file mode 100644 index 0000000000000..72ac767b3fb35 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx @@ -0,0 +1,63 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { EuiIcon, EuiPageTemplate } from '@elastic/eui'; +import axios from 'axios'; +import { useHistory, useParams } from 'react-router-dom'; +import { K8sCluster } from '../../../common/types_api'; +import { PageTemplate } from '../../components/page_template'; +import { K8sClusterInfo } from '../../components/k8s_cluster_info'; + +export function K8sClusterPage() { + const [cluster, setCluster] = useState(null); + const { name } = useParams<{ name: string }>(); + const history = useHistory(); + + useEffect(() => { + async function retrieve() { + const response = await axios.get( + `/local/api/asset-inventory/k8s/clusters/${name}` + ); + if (response.data && response.data?.result) { + setCluster(response.data.result); + } + } + retrieve(); + }, [name]); + + if (cluster === null) { + return null; + } + + return ( + + + Return to cluster list + + ), + color: 'primary', + 'aria-current': false, + href: '#', + onClick: (e) => { + e.preventDefault(); + history.push('/k8s/clusters'); + }, + }, + ]} + /> + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx new file mode 100644 index 0000000000000..0d8d2751cf7a9 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx @@ -0,0 +1,52 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { EuiButton, EuiPageTemplate } from '@elastic/eui'; +import axios from 'axios'; +import { useHistory } from 'react-router-dom'; +import { K8sCluster } from '../../../common/types_api'; +import { PageTemplate } from '../../components/page_template'; +import { K8sClustersTable } from '../../components/k8s_clusters_table'; + +export function K8sClustersListPage() { + const [clusters, setClusters] = useState([]); + const history = useHistory(); + + useEffect(() => { + async function retrieve() { + const response = await axios.get( + `/local/api/asset-inventory/k8s/clusters` + ); + if (response.data && response.data?.results) { + setClusters(response.data.results); + } + } + retrieve(); + }, []); + + return ( + + ) => { + e.preventDefault(); + history.push('/'); + }} + > + Asset Inventory + , + ]} + /> + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/plugin.ts b/x-pack/plugins/asset_inventory/public/plugin.ts new file mode 100644 index 0000000000000..96d10585a176a --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/plugin.ts @@ -0,0 +1,111 @@ +/* + * 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. + */ + +// NOTE: Originally copied from plugins/observability/public/plugin.ts, heavily modified + +import { i18n } from '@kbn/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { + AppMountParameters, + AppUpdater, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin as PluginClass, + PluginInitializerContext, +} from '@kbn/core/public'; + +import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; + +export type AssetInventoryPublicSetup = ReturnType; + +export interface AssetInventoryPublicPluginsSetup { + data: DataPublicPluginSetup; + usageCollection: UsageCollectionSetup; +} + +export interface AssetInventoryPublicPluginsStart { + data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; +} + +export type AssetInventoryPublicStart = ReturnType; + +export class Plugin + implements + PluginClass< + AssetInventoryPublicSetup, + AssetInventoryPublicStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicPluginsStart + > +{ + private readonly appUpdater$ = new BehaviorSubject(() => ({})); + + constructor(private readonly initContext: PluginInitializerContext<{}>) {} + + public setup( + coreSetup: CoreSetup, + pluginsSetup: AssetInventoryPublicPluginsSetup + ) { + const category = DEFAULT_APP_CATEGORIES.observability; + const euiIconType = 'logoObservability'; + const config = this.initContext.config.get(); + + const mount = async (params: AppMountParameters) => { + // Load application bundle + const { renderApp } = await import('./app'); + // Get start services + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + + return renderApp({ + core: coreStart, + config, + plugins: pluginsStart, + appMountParameters: params, + // ObservabilityPageTemplate: navigation.PageTemplate, + usageCollection: pluginsSetup.usageCollection, + isDev: this.initContext.env.mode.dev, + }); + }; + + const appUpdater$ = this.appUpdater$; + const app = { + appRoute: '/app/asset-inventory', + category, + euiIconType, + id: 'assetInventory', + mount, + order: 8000, + title: i18n.translate('xpack.assetInventory.overviewTitle', { + defaultMessage: 'Asset Inventory', + }), + updater$: appUpdater$, + keywords: [ + 'observability', + 'monitor', + 'logs', + 'metrics', + 'apm', + 'performance', + 'trace', + 'agent', + 'rum', + 'user', + 'experience', + ], + }; + + coreSetup.application.register(app); + + return {}; + } + + public start(coreStart: CoreStart, pluginsStart: AssetInventoryPublicPluginsStart) {} +} diff --git a/x-pack/plugins/asset_inventory/server/constants.ts b/x-pack/plugins/asset_inventory/server/constants.ts new file mode 100644 index 0000000000000..a16b78045a8d8 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/constants.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 const ASSETS_INDEX = 'assets*'; diff --git a/x-pack/plugins/asset_inventory/server/index.ts b/x-pack/plugins/asset_inventory/server/index.ts new file mode 100644 index 0000000000000..d2f12f290c994 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +// TODO: https://github.com/elastic/kibana/issues/110905 + +import { AssetInventoryServerPlugin } from './plugin'; + +export const plugin = () => new AssetInventoryServerPlugin(); diff --git a/x-pack/plugins/asset_inventory/server/lib/es_client.ts b/x-pack/plugins/asset_inventory/server/lib/es_client.ts new file mode 100644 index 0000000000000..54fb630d3ee51 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/es_client.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +// TODO: Replace all of this with Kibana ES client and/or data plugin + +import { Client } from '@elastic/elasticsearch'; +const { + ELASTICSEARCH_HOSTS = 'https://localhost:9200', + ELASTICSEARCH_USERNAME = 'elastic', + ELASTICSEARCH_PASSWORD = 'changeme', +} = process.env; + +export const esClient = new Client({ + node: ELASTICSEARCH_HOSTS, + auth: { + username: ELASTICSEARCH_USERNAME, + password: ELASTICSEARCH_PASSWORD, + }, + tls: { + rejectUnauthorized: false, + }, +}); diff --git a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts new file mode 100644 index 0000000000000..8540865ea1afc --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts @@ -0,0 +1,105 @@ +/* + * 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 { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; +import { Asset, AssetFilters } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; +import { esClient } from './es_client'; + +interface GetAssetsOptions { + filters?: AssetFilters; +} + +export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promise { + const dsl: SearchRequest = { + index: ASSETS_INDEX, + }; + + if (filters && Object.keys(filters).length > 0) { + const musts: QueryDslQueryContainer[] = []; + + if (typeof filters.collectionVersion === 'number') { + musts.push({ + term: { + ['asset.collection_version']: filters.collectionVersion, + }, + }); + } + + if (filters.type) { + musts.push({ + term: { + ['asset.type']: filters.type, + }, + }); + } + + if (filters.kind) { + musts.push({ + term: { + ['asset.kind']: filters.kind, + }, + }); + } + + if (filters.ean) { + musts.push({ + term: { + ['asset.ean']: filters.ean, + }, + }); + } + + if (filters.id) { + musts.push({ + term: { + ['asset.id']: filters.id, + }, + }); + } + + if (filters.typeLike) { + musts.push({ + wildcard: { + ['asset.type']: filters.typeLike, + }, + }); + } + + if (filters.eanLike) { + musts.push({ + wildcard: { + ['asset.ean']: filters.eanLike, + }, + }); + } + + if (musts.length > 0) { + dsl.query = { + bool: { + must: musts, + }, + }; + } + + dsl.collapse = { + field: 'asset.ean', + }; + + dsl.sort = { + '@timestamp': { + order: 'desc', + }, + }; + } + + debug('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{}>(dsl); + return response.hits.hits.map((hit) => hit._source as Asset); +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts new file mode 100644 index 0000000000000..179c4b3fd3e63 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts @@ -0,0 +1,67 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; +import { EcsOrchestratorFieldset, K8sCluster } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; +import { esClient } from './es_client'; +import { getK8sNodes } from './get_k8s_nodes'; + +export async function getK8sCluster(name: string): Promise { + const dsl: SearchRequest = { + index: ASSETS_INDEX, + query: { + bool: { + must: [ + { + term: { + ['asset.name']: name, + }, + }, + { + term: { + ['asset.type']: 'k8s.cluster', + }, + }, + ], + }, + }, + collapse: { + field: 'asset.ean', + }, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + }; + + debug('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{ + '@timestamp': string; + 'asset.name': string; + 'asset.ean': string; + 'asset.id': string; + orchestrator: EcsOrchestratorFieldset; + }>(dsl); + + const { _source: cluster } = response.hits.hits[0]; + if (!cluster) { + throw new Error('No cluster returned'); + } + const nodes = await getK8sNodes({ clusterEan: cluster['asset.ean'] }); + + return { + '@timestamp': cluster['@timestamp'], + name: cluster['asset.name'], + nodes, + status: 'Healthy', + version: cluster.orchestrator?.cluster?.version || 'unspecified', + }; +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts new file mode 100644 index 0000000000000..143cff526fe98 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts @@ -0,0 +1,61 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; +import { Asset, K8sCluster } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; +import { esClient } from './es_client'; +import { getK8sNodes } from './get_k8s_nodes'; + +export async function getK8sClusters(): Promise { + const dsl: SearchRequest = { + index: ASSETS_INDEX, + query: { + bool: { + must: [ + { + term: { + ['asset.type']: 'k8s.cluster', + }, + }, + ], + }, + }, + collapse: { + field: 'asset.ean', + }, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + }; + + debug('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search(dsl); + + const results = await Promise.all( + response.hits.hits.map(async (hit) => { + if (!hit._source) { + throw new Error('Missing _source in cluster result'); + } + const doc = hit._source; + return { + '@timestamp': doc['@timestamp'], + name: doc['asset.name'] || doc['asset.id'], + nodes: await getK8sNodes({ clusterEan: doc['asset.ean'] }), + status: doc['asset.status'] || 'UNKNOWN', + cloud: doc.cloud, + version: doc.orchestrator?.cluster?.version || 'Unspecified', + }; + }) + ); + + return results; +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts new file mode 100644 index 0000000000000..003cd1c6b3a38 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts @@ -0,0 +1,72 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; +import { K8sNode } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; +import { esClient } from './es_client'; + +interface GetK8sNodesOptions { + clusterEan?: string; +} + +export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Promise { + const dsl: SearchRequest = { + index: ASSETS_INDEX, + query: { + bool: { + must: [ + { + term: { + ['asset.type']: 'k8s.node', + }, + }, + { + term: { + 'asset.parents': clusterEan, + }, + }, + ], + }, + }, + collapse: { + field: 'asset.ean', + }, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + }; + + debug('Performing K8s Nodes Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{ + '@timestamp': string; + 'asset.id': string; + 'asset.name': string; + 'asset.ean': string; + }>(dsl); + + const results = await Promise.all( + response.hits.hits.map(async (hit) => { + if (!hit._source) { + throw new Error('Missing _source in node result'); + } + const s = hit._source; + return { + '@timestamp': s['@timestamp'], + id: s['asset.id'], + name: s['asset.name'], + ean: s['asset.ean'], + }; + }) + ); + + return results; +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts new file mode 100644 index 0000000000000..60a778dad78dd --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts @@ -0,0 +1,29 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; +import { ASSETS_INDEX } from '../constants'; +import { esClient } from './es_client'; + +export async function getLatestCollectionVersion() { + const dsl: SearchRequest = { + index: ASSETS_INDEX, + size: 0, + aggregations: { + maxVersion: { + max: { + field: 'asset.collectionVersion', + }, + }, + }, + }; + + debug('Performing Latest Version Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search(dsl); + return response.aggregations?.maxVersion.value || 0; +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts new file mode 100644 index 0000000000000..c8a487a1ee517 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts @@ -0,0 +1,61 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; +import { Asset } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; +import { esClient } from './es_client'; + +interface GetValuesForFieldOptions { + field: keyof Asset; + version?: number; + searchText?: string; +} + +interface AggBucket { + key: string; + doc_count: number; +} + +export async function getValuesForField({ + field, + version, + searchText, +}: GetValuesForFieldOptions): Promise { + const dsl: SearchRequest = { + index: ASSETS_INDEX, + size: 0, + aggs: { + field_values: { + terms: { + field, + include: searchText, + }, + }, + }, + }; + + if (version || version === 0) { + dsl.query = { + bool: { + must: [ + { + term: { + ['asset.collection_version']: version, + }, + }, + ], + }, + }; + } + + debug(`Performing Field Value Query for ${field}`, '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{}, { field_values: { buckets: AggBucket[] } }>(dsl); + return response.aggregations?.field_values.buckets || []; +} diff --git a/x-pack/plugins/asset_inventory/server/plugin.ts b/x-pack/plugins/asset_inventory/server/plugin.ts new file mode 100644 index 0000000000000..f393f4f99335a --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/plugin.ts @@ -0,0 +1,123 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { Plugin, CoreSetup } from '@kbn/core/server'; +import { debug } from '../common/debug_log'; +import { AssetFilters } from '../common/types_api'; +import { getAssets } from './lib/get_assets'; +import { getK8sCluster } from './lib/get_k8s_cluster'; +import { getK8sClusters } from './lib/get_k8s_clusters'; +import { getValuesForField } from './lib/get_values_for_field'; + +export type AssetInventoryServerPluginSetup = ReturnType; + +export class AssetInventoryServerPlugin implements Plugin { + public async setup(core: CoreSetup) { + const router = core.http.createRouter(); + + router.get( + { + path: '/api/asset-inventory/ping', + validate: false, + }, + (context, req, res) => { + return res.ok({ + body: { message: 'Asset Inventory OK' }, + headers: { 'content-type': 'application/json' }, + }); + } + ); + + router.get( + { + path: '/api/asset-inventory', + validate: { + query: schema.any({}), + }, + }, + async (context, req, res) => { + const filters = req.query || {}; + + try { + const assets = await getAssets({ filters }); + return res.ok({ body: { assets } }); + } catch (error: unknown) { + debug('error looking up asset records', error); + return res.customError({ statusCode: 500 }); + } + } + ); + + router.get( + { + path: '/api/asset-inventory/field-values', + validate: { + query: schema.any({}), + }, + }, + async (context, req, res) => { + const { field, searchText, version } = req.query; + + try { + const results = await getValuesForField({ + field, + searchText, + version: Number(version), + }); + return res.ok({ body: { results } }); + } catch (error: unknown) { + debug('error looking up field values', error); + return res.customError({ statusCode: 500 }); + } + } + ); + + router.get( + { + path: '/api/asset-inventory/k8s/clusters', + validate: false, + }, + async (context, req, res) => { + try { + const results = await getK8sClusters(); + return res.ok({ body: { results } }); + } catch (error: unknown) { + debug('error looking up field values', error); + return res.customError({ statusCode: 500 }); + } + } + ); + + router.get<{ name: string }, {}, {}>( + { + path: '/api/asset-inventory/k8s/clusters/{name}', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, req, res) => { + const name = req.params.name; + try { + const result = await getK8sCluster(name); + return res.ok({ body: { result } }); + } catch (error: unknown) { + debug('error looking up field values', error); + return res.customError({ statusCode: 500 }); + } + } + ); + + return {}; + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/asset_inventory/tsconfig.json b/x-pack/plugins/asset_inventory/tsconfig.json new file mode 100644 index 0000000000000..4d38e0dfeaee4 --- /dev/null +++ b/x-pack/plugins/asset_inventory/tsconfig.json @@ -0,0 +1,42 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "public/**/*.json", + "server/**/*", + "typings/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../actions/tsconfig.json" }, + { "path": "../alerting/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../infra/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../maps/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../observability/tsconfig.json" }, + { "path": "../reporting/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../fleet/tsconfig.json" } + ] +}