diff --git a/.gitignore b/.gitignore index fbe28b8f1e77cc..1a4ffefd6d12fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .aws-config.json .signing-config.json +/api_docs .ackrc /.es /.chromium diff --git a/dev_docs/tutorials/data/search.mdx b/dev_docs/tutorials/data/search.mdx new file mode 100644 index 00000000000000..69b4d5dab58b58 --- /dev/null +++ b/dev_docs/tutorials/data/search.mdx @@ -0,0 +1,496 @@ +--- +id: kibDevTutorialDataSearchAndSessions +slug: /kibana-dev-docs/tutorials/data/search-and-sessions +title: Kibana data.search Services +summary: Kibana Search Services +date: 2021-02-10 +tags: ['kibana', 'onboarding', 'dev', 'tutorials', 'search', 'sessions', 'search-sessions'] +--- + +## Search service + +### Low level search + +Searching data stored in Elasticsearch can be done in various ways, for example using the Elasticsearch REST API or using an `Elasticsearch Client` for low level access. + +However, the recommended and easist way to search Elasticsearch is by using the low level search service. The service is exposed from the `data` plugin, and by using it, you not only gain access to the data you stored, but also to capabilities, such as Custom Search Strategies, Asynchronous Search, Partial Results, Search Sessions, and more. + +Here is a basic example for using the `data.search` service from a custom plugin: + +```ts +import { CoreStart, Plugin } from 'kibana/public'; +import { DataPublicPluginStart, isCompleteResponse, isErrorResponse } from import { DataPublicPluginStart, isCompleteResponse, isErrorResponse } from '../../src/plugins/data'; + +export interface MyPluginStartDependencies { + data: DataPublicPluginStart; +} + +export class MyPlugin implements Plugin { + public start(core: CoreStart, { data }: MyPluginStartDependencies) { + const query = { + filter: [{ + match_all: {} + }], + }; + const req = { + params: { + index: 'my-index-*', + body: { + query, + aggs: {}, + }, + } + }; + data.search.search(req).subscribe({ + next: (result) => { + if (isCompleteResponse(res)) { + // handle search result + } else if (isErrorResponse(res)) { + // handle error, this means that some results were returned, but the search has failed to complete. + } else { + // handle partial results if you want. + } + }, + error: (e) => { + // handle error thrown, for example a server hangup + }, + }) + } +} +``` + +Note: The `data` plugin contains services to help you generate the `query` and `aggs` portions, as well as managing indices using the `data.indexPatterns` service. + + + The `data.search` service is available on both server and client, with similar APIs. + + +#### Error handling + +The `search` method can throw several types of errors, for example: + + - `EsError` for errors originating in Elasticsearch errors + - `PainlessError` for errors originating from a Painless script + - `AbortError` if the search was aborted via an `AbortController` + - `HttpError` in case of a network error + +To display the errors in the context of an application, use the helper method provided on the `data.search` service. These errors are shown in a toast message, using the `core.notifications` service. + +```ts +data.search.search(req).subscribe({ + next: (result) => {}, + error: (e) => { + data.search.showError(e); + }, +}) +``` + +If you decide to handle errors by yourself, watch for errors coming from `Elasticsearch`. They have an additional `attributes` property that holds the raw error from `Elasticsearch`. + +```ts +data.search.search(req).subscribe({ + next: (result) => {}, + error: (e) => { + if (e instanceof IEsError) { + showErrorReason(e.attributes); + } + }, +}) +``` + +#### Stop a running search + +The search service `search` method supports a second argument called `options`. One of these options provides an `abortSignal` to stop searches from running to completion, if the result is no longer needed. + +```ts +import { AbortError } from '../../src/data/public'; + +const abortController = new AbortController(); +data.search.search(req, { + abortSignal: abortController.signal, +}).subscribe({ + next: (result) => { + // handle result + }, + error: (e) => { + if (e instanceof AbortError) { + // you can ignore this error + return; + } + // handle error, for example a server hangup + }, +}); + +// Abort the search request after a second +setTimeout(() => { + abortController.abort(); +}, 1000); +``` + +#### Search strategies + +By default, the search service uses the DSL query and aggregation syntax and returns the response from Elasticsearch as is. It also provides several additional basic strategies, such as Async DSL (`x-pack` default) and EQL. + +For example, to run an EQL query using the `data.search` service, you should to specify the strategy name using the options parameter: + +```ts +const req = getEqlRequest(); +data.search.search(req, { + strategy: EQL_SEARCH_STRATEGY, +}).subscribe({ + next: (result) => { + // handle EQL result + }, +}); +``` + +##### Custom search strategies + +To use a different query syntax, preprocess the request, or process the response before returning it to the client, you can create and register a custom search strategy to encapsulate your custom logic. + +The following example shows how to define, register, and use a search strategy that preprocesses the request before sending it to the default DSL search strategy, and then processes the response before returning. + +```ts +// ./myPlugin/server/myStrategy.ts + +/** + * Your custom search strategy should implement the ISearchStrategy interface, requiring at minimum a `search` function. + */ +export const mySearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy => { + const preprocessRequest = (request: IMyStrategyRequest) => { + // Custom preprocessing + } + + const formatResponse = (response: IMyStrategyResponse) => { + // Custom post-processing + } + + // Get the default search strategy + const es = data.search.getSearchStrategy(ES_SEARCH_STRATEGY); + return { + search: (request, options, deps) => { + return formatResponse(es.search(preprocessRequest(request), options, deps)); + }, + }; +}; +``` + +```ts +// ./myPlugin/server/plugin.ts +import type { + CoreSetup, + CoreStart, + Plugin, +} from 'kibana/server'; + +import { mySearchStrategyProvider } from './my_strategy'; + +/** + * Your plugin will receive the `data` plugin contact in both the setup and start lifecycle hooks. + */ +export interface MyPluginSetupDeps { + data: PluginSetup; +} + +export interface MyPluginStartDeps { + data: PluginStart; +} + +/** + * In your custom server side plugin, register the strategy from the setup contract + */ +export class MyPlugin implements Plugin { + public setup( + core: CoreSetup, + deps: MyPluginSetupDeps + ) { + core.getStartServices().then(([_, depsStart]) => { + const myStrategy = mySearchStrategyProvider(depsStart.data); + deps.data.search.registerSearchStrategy('myCustomStrategy', myStrategy); + }); + } +} +``` + +```ts +// ./myPlugin/public/plugin.ts +const req = getRequest(); +data.search.search(req, { + strategy: 'myCustomStrategy', +}).subscribe({ + next: (result) => { + // handle result + }, +}); +``` + +##### Async search and custom async search strategies + +The open source default search strategy (`ES_SEARCH_STRATEGY`), run searches synchronously, keeping an open connection to Elasticsearch while the query executes. The duration of these queries is restricted by the `elasticsearch.requestTimeout` setting in `kibana.yml`, which is 30 seconds by default. + +This synchronous execution works great in most cases. However, with the introduction of features such as `data tiers` and `runtime fields`, the need to allow slower-running queries, where holding an open connection might be inefficient, has increased. In 7.7, `Elasticsearch` introduced the [async_search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html), allowing a query to run longer without keeping an open connection. Instead, the initial search request returns an ID that identifies the search running in `Elasticsearch`. This ID can then be used to retrieve, cancel, or manage the search result. + +The [async_search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html) is what drives more advanced `Kibana` `search` features, such as `partial results` and `search sessions`. [When available](https://www.elastic.co/subscriptions), the default search strategy of `Kibana` is automatically set to the **async** default search strategy (`ENHANCED_ES_SEARCH_STRATEGY`), empowering Kibana to run longer queries, with an **optional** duration restriction defined by the UI setting `search:timeout`. + +If you are implementing your own async custom search strategy, make sure to implement `cancel` and `extend`, as shown in the following example: + +```ts +// ./myPlugin/server/myEnhancedStrategy.ts +export const myEnhancedSearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy => { + // Get the default search strategy + const ese = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + return { + search: (request, options, deps) => { + // search will be called multiple times, + // be sure your response formatting is capable of handling partial results, as well as the final result. + return formatResponse(ese.search(request, options, deps)); + }, + cancel: async (id, options, deps) => { + // call the cancel method of the async strategy you are using or implement your own cancellation function. + await ese.cancel(id, options, deps); + }, + extend: async (id, keepAlive, options, deps) => { + // async search results are not stored indefinitely. By default, they expire after 7 days (or as defined by xpack.data_enhanced.search.sessions.defaultExpiration setting in kibana.yml). + // call the extend method of the async strategy you are using or implement your own extend function. + await ese.extend(id, options, deps); + }, + }; +}; +``` + +### High level search + +The high level search service is a simplified way to create and run search requests, without writing custom DSL queries. + +#### Search source + +```ts +function searchWithSearchSource() { + const indexPattern = data.indexPatterns.getDefault(); + const query = data.query.queryString.getQuery(); + const filters = data.query.filterManager.getFilters(); + const timefilter = data.query.timefilter.timefilter.createFilter(indexPattern); + if (timefilter) { + filters.push(timefilter); + } + + const searchSource = await data.search.searchSource.create(); + + searchSource + .setField('index', indexPattern) + .setField('filter', filters) + .setField('query', query) + .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']) + .setField('aggs', getAggsDsl()); + + searchSource.fetch$().subscribe({ + next: () => {}, + error: () => {}, + }); +} +``` + +### Partial results + +When searching using an `async` strategy (such as async DSL and async EQL), the search service will stream back partial results. + +Although you can ignore the partial results and wait for the final result before rendering, you can also use the partial results to create a more interactive experience for your users. It is highly advised, however, to make sure users are aware that the results they are seeing are partial. + +```ts +// Handling partial results +data.search.search(req).subscribe({ + next: (result) => { + if (isCompleteResponse(res)) { + renderFinalResult(res); + } else if (isPartialResponse(res)) { + renderPartialResult(res); + } + }, +}) + +// Skipping partial results +const finalResult = await data.search.search(req).toPromise(); +``` + +### Search sessions + +A search session is a higher level concept than search. A search session describes a grouping of one or more async search requests with additional context. + +Search sessions are handy when you want to enable a user to run something asynchronously (for example, a dashboard over a long period of time), and then quickly restore the results at a later time. The `Search Service` transparently fetches results from the `.async-search` index, instead of running each request again. + +Internally, any search run within a search session is saved into an object, allowing Kibana to manage their lifecycle. Most saved objects are deleted automatically after a short period of time, but if a user chooses to save the search session, the saved object is persisted, so that results can be restored in a later time. + +Stored search sessions are listed in the *Management* application, under *Kibana > Search Sessions*, making it easy to find, manage, and restore them. + +As a developer, you might encounter these two common, use cases: + + * Running a search inside an existing search session + * Supporting search sessions in your application + +#### Running a search inside an existing search session + +For this example, assume you are implementing a new type of `Embeddable` that will be shown on dashboards. The same principle applies, however, to any search requests that you are running, as long as the application you are running inside is managing an active session. + +Because the Dashboard application is already managing a search session, all you need to do is pass down the `searchSessionId` argument to any `search` call. This applies to both the low and high level search APIs. + +The search information will be added to the saved object for the search session. + +```ts +export class SearchEmbeddable + extends Embeddable { + + private async fetchData() { + // Every embeddable receives an optional `searchSessionId` input parameter. + const { searchSessionId } = this.input; + + // Setup your search source + this.configureSearchSource(); + + try { + // Mark the embeddable as loading + this.updateOutput({ loading: true, error: undefined }); + + // Make the request, wait for the final result + const resp = await searchSource.fetch$({ + sessionId: searchSessionId, + }).toPromise(); + + this.useSearchResult(resp); + + this.updateOutput({ loading: false, error: undefined }); + } catch (error) { + // handle search errors + this.updateOutput({ loading: false, error }); + } + } +} + +``` + +You can also retrieve the active `Search Session ID` from the `Search Service` directly: + +```ts +async function fetchData(data: DataPublicPluginStart) { + try { + return await searchSource.fetch$({ + sessionId: data.search.sessions.getSessionId(), + }).toPromise(); + } catch (e) { + // handle search errors + } +} + +``` + + + Search sessions are initiated by the client. If you are using a route that runs server side searches, you can send the `searchSessionId` to the server, and then pass it down to the server side `data.search` function call. + + +#### Supporting search sessions in your application + +Before implementing the ability to create and restore search sessions in your application, ask yourself the following questions: + +1. **Does your application normally run long operations?** For example, it makes sense for a user to generate a Dashboard or a Canvas report from data stored in cold storage. However, when editing a single visualization, it is best to work with a shorter timeframe of hot or warm data. +2. **Does it make sense for your application to restore a search session?** For example, you might want to restore an interesting configuration of filters of older documents you found in Discover. However, a single Lens or Map visualization might not be as helpful, outside the context of a specific dashboard. +3. **What is a search session in the context of your application?** Although Discover and Dashboard start a new search session every time the time range or filters change, or when the user clicks **Refresh**, you can manage your sessions differently. For example, if your application has tabs, you might group searches from multiple tabs into a single search session. You must be able to clearly define the **state** used to create the search session`. The **state** refers to any setting that might change the queries being set to `Elasticsearch`. + +Once you answer those questions, proceed to implement the following bits of code in your application. + +##### Provide storage configuration + +In your plugin's `start` lifecycle method, call the `enableStorage` method. This method helps the `Session Service` gather the information required to save the search sessions upon a user's request and construct the restore state: + +```ts +export class MyPlugin implements Plugin { + public start(core: CoreStart, { data }: MyPluginStartDependencies) { + const sessionRestorationDataProvider: SearchSessionInfoProvider = { + data, + getDashboard + } + + data.search.session.enableStorage({ + getName: async () => { + // return the name you want to give the saved Search Session + return `MyApp_${Math.random()}`; + }, + getUrlGeneratorData: async () => { + return { + urlGeneratorId: MY_URL_GENERATOR, + initialState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: false }), + restoreState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: true }), + }; + }, + }); + } +} +``` + + + The restore state of a search session may be different from the initial state used to create it. For example, where the initial state may contain relative dates, in the restore state, those must be converted to absolute dates. Read more about the [NowProvider](). + + + + Calling `enableStorage` will also enable the `Search Session Indicator` component in the chrome component of your solution. The `Search Session Indicator` is a small button, used by default to engage users and save new search sessions. To implement your own UI, contact the Kibana application services team to decouple this behavior. + + +##### Start a new search session + +Make sure to call `start` when the **state** you previously defined changes. + +```ts + +function onSearchSessionConfigChange() { + this.searchSessionId = data.search.sessions.start(); +} + +``` + +Pass the `searchSessionId` to every `search` call inside your application. If you're using `Embeddables`, pass down the `searchSessionId` as `input`. + +If you can't pass the `searchSessionId` directly, you can retrieve it from the service. + +```ts +const currentSearchSessionId = data.search.sessions.getSessionId(); + +``` + +##### Clear search sessions + +Creating a new search session clears the previous one. You must explicitly `clear` the search session when your application is being destroyed: + +```ts +function onDestroy() { + data.search.session.clear(); +} + +``` + +If you don't call `clear`, you will see a warning in the console while developing. However, when running in production, you will get a fatal error. This is done to avoid leakage of unrelated search requests into an existing search session left open by mistake. + +##### Restore search sessions + +The last step of the integration is restoring an existing search session. The `searchSessionId` parameter and the rest of the restore state are passed into the application via the URL. Non-URL support is planned for future releases. + +If you detect the presense of a `searchSessionId` parameter in the URL, call the `restore` method **instead** of calling `start`. The previous example would now become: + +```ts + +function onSearchSessionConfigChange(searchSessionIdFromUrl?: string) { + if (searchSessionIdFromUrl) { + data.search.sessions.restore(searchSessionIdFromUrl); + } else { + data.search.sessions.start(); + } +} + +``` + +Once you `restore` the session, as long as all `search` requests run with the same `searchSessionId`, the search session should be seamlessly restored. + +##### Customize the user experience + +TBD diff --git a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md index a50df950628b3a..f6de959589eca7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md +++ b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RollingFileAppenderConfig; +export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md index b0182a7c48e16c..bd15b95d73acea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md @@ -28,6 +28,7 @@ Should never be used in code outside of Core but is exported for documentation p | [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md) | readonly string[] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | | [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | | [server](./kibana-plugin-core-server.pluginmanifest.server.md) | boolean | Specifies whether plugin includes some server-side specific functionality. | +| [serviceFolders](./kibana-plugin-core-server.pluginmanifest.servicefolders.md) | readonly string[] | Only used for the automatically generated API documentation. Specifying service folders will cause your plugin API reference to be broken up into sub sections. | | [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | boolean | Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via public/ui_plugin.js file. | | [version](./kibana-plugin-core-server.pluginmanifest.version.md) | string | Version of the plugin. | diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.servicefolders.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.servicefolders.md new file mode 100644 index 00000000000000..8ee33bdfa0f3fa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.servicefolders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) > [serviceFolders](./kibana-plugin-core-server.pluginmanifest.servicefolders.md) + +## PluginManifest.serviceFolders property + +Only used for the automatically generated API documentation. Specifying service folders will cause your plugin API reference to be broken up into sub sections. + +Signature: + +```typescript +readonly serviceFolders?: readonly string[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md index 5cfd5e1bc99292..80f4832ba5643a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md @@ -12,5 +12,6 @@ export declare type IndexPatternSelectProps = Required void; + maxIndexPatterns?: number; }; ``` diff --git a/package.json b/package.json index 70918f02dcd412..90096bfdf1b80f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "test:ftr:runner": "node scripts/functional_test_runner", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", + "build:apidocs": "node scripts/build_api_docs", "start": "node scripts/kibana --dev", "debug": "node --nolazy --inspect scripts/kibana --dev", "debug-break": "node --nolazy --inspect-brk scripts/kibana --dev", @@ -74,11 +75,11 @@ "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", - "**/graphql-toolkit/lodash": "^4.17.15", + "**/graphql-toolkit/lodash": "^4.17.21", "**/hoist-non-react-statics": "^3.3.2", "**/isomorphic-fetch/node-fetch": "^2.6.1", "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", - "**/load-grunt-config/lodash": "^4.17.20", + "**/load-grunt-config/lodash": "^4.17.21", "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", "**/prismjs": "1.22.0", @@ -233,7 +234,7 @@ "json-stringify-safe": "5.0.1", "jsonwebtoken": "^8.5.1", "load-json-file": "^6.2.0", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "lru-cache": "^4.1.5", "markdown-it": "^10.0.0", "md5": "^2.1.0", @@ -367,7 +368,7 @@ "@kbn/plugin-generator": "link:packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", - "@kbn/release-notes": "link:packages/kbn-release-notes", + "@kbn/docs-utils": "link:packages/kbn-docs-utils", "@kbn/storybook": "link:packages/kbn-storybook", "@kbn/telemetry-tools": "link:packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", @@ -390,7 +391,6 @@ "@storybook/addon-essentials": "^6.0.26", "@storybook/addon-knobs": "^6.0.26", "@storybook/addon-storyshots": "^6.0.26", - "@storybook/addons": "^6.0.16", "@storybook/components": "^6.0.26", "@storybook/core": "^6.0.26", "@storybook/core-events": "^6.0.26", @@ -822,6 +822,7 @@ "tinycolor2": "1.4.1", "topojson-client": "3.0.0", "ts-loader": "^7.0.5", + "ts-morph": "^9.1.0", "tsd": "^0.13.1", "typescript": "4.1.3", "typescript-fsa": "^3.0.0", diff --git a/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts index 62a6f1d347c9bd..8d3fdb0f390c5e 100644 --- a/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts +++ b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts @@ -29,6 +29,7 @@ interface Manifest { server: boolean; kibanaVersion: string; version: string; + serviceFolders: readonly string[]; requiredPlugins: readonly string[]; optionalPlugins: readonly string[]; requiredBundles: readonly string[]; @@ -64,6 +65,7 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP id: manifest.id, version: manifest.version, kibanaVersion: manifest.kibanaVersion || manifest.version, + serviceFolders: manifest.serviceFolders || [], requiredPlugins: isValidDepsDeclaration(manifest.requiredPlugins, 'requiredPlugins'), optionalPlugins: isValidDepsDeclaration(manifest.optionalPlugins, 'optionalPlugins'), requiredBundles: isValidDepsDeclaration(manifest.requiredBundles, 'requiredBundles'), diff --git a/packages/kbn-release-notes/jest.config.js b/packages/kbn-docs-utils/jest.config.js similarity index 89% rename from packages/kbn-release-notes/jest.config.js rename to packages/kbn-docs-utils/jest.config.js index db5e48b1704fbb..e9cdc1978231e2 100644 --- a/packages/kbn-release-notes/jest.config.js +++ b/packages/kbn-docs-utils/jest.config.js @@ -9,5 +9,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../..', - roots: ['/packages/kbn-release-notes'], + roots: ['/packages/kbn-docs-utils'], }; diff --git a/packages/kbn-release-notes/package.json b/packages/kbn-docs-utils/package.json similarity index 92% rename from packages/kbn-release-notes/package.json rename to packages/kbn-docs-utils/package.json index c325367c7d47e8..6571f8c87dbfaf 100644 --- a/packages/kbn-release-notes/package.json +++ b/packages/kbn-docs-utils/package.json @@ -1,5 +1,5 @@ { - "name": "@kbn/release-notes", + "name": "@kbn/docs-utils", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": "true", diff --git a/packages/kbn-docs-utils/src/api_docs/README.md b/packages/kbn-docs-utils/src/api_docs/README.md new file mode 100644 index 00000000000000..f980fe83b95963 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/README.md @@ -0,0 +1,12 @@ +# Autogenerated API documentation + +[RFC](../../../rfcs/text/0014_api_documentation.md) + +This is an experimental api documentation system that is managed by the Kibana Tech Leads until +we determine the value of such a system and what kind of maintenance burder it will incur. + +To generate the docs run + +``` +node scripts/build_api_docs +``` diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/buid_api_declaration.test.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/buid_api_declaration.test.ts new file mode 100644 index 00000000000000..31e0d59dcca8a9 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/buid_api_declaration.test.ts @@ -0,0 +1,85 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import { Project, Node } from 'ts-morph'; +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; + +import { TypeKind, ApiScope } from '../types'; +import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock'; +import { getDeclarationNodesForPluginScope } from '../get_declaration_nodes_for_plugin'; +import { buildApiDeclaration } from './build_api_declaration'; +import { isNamedNode } from '../tsmorph_utils'; + +const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, +}); + +let nodes: Node[]; +let plugins: KibanaPlatformPlugin[]; + +function getNodeName(node: Node): string { + return isNamedNode(node) ? node.getName() : ''; +} + +beforeAll(() => { + const tsConfigFilePath = Path.resolve(__dirname, '../tests/__fixtures__/src/tsconfig.json'); + const project = new Project({ + tsConfigFilePath, + }); + + plugins = [getKibanaPlatformPlugin('pluginA')]; + + nodes = getDeclarationNodesForPluginScope(project, plugins[0], ApiScope.CLIENT, log); +}); + +it('Test number primitive doc def', () => { + const node = nodes.find((n) => getNodeName(n) === 'aNum'); + expect(node).toBeDefined(); + const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT); + + expect(def.type).toBe(TypeKind.NumberKind); +}); + +it('Function type is exported as type with signature', () => { + const node = nodes.find((n) => getNodeName(n) === 'FnWithGeneric'); + expect(node).toBeDefined(); + const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT); + expect(def).toBeDefined(); + expect(def?.type).toBe(TypeKind.TypeKind); + expect(def?.signature?.length).toBeGreaterThan(0); +}); + +it('Test Interface Kind doc def', () => { + const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface'); + expect(node).toBeDefined(); + const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT); + + expect(def.type).toBe(TypeKind.InterfaceKind); + expect(def.children).toBeDefined(); + expect(def.children!.length).toBe(3); +}); + +it('Test union export', () => { + const node = nodes.find((n) => getNodeName(n) === 'aUnionProperty'); + expect(node).toBeDefined(); + const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT); + expect(def.type).toBe(TypeKind.CompoundTypeKind); +}); + +it('Function inside interface has a label', () => { + const node = nodes.find((n) => getNodeName(n) === 'ExampleInterface'); + expect(node).toBeDefined(); + const def = buildApiDeclaration(node!, plugins, log, plugins[0].manifest.id, ApiScope.CLIENT); + + const fn = def!.children?.find((c) => c.label === 'aFn'); + expect(fn).toBeDefined(); + expect(fn?.label).toBe('aFn'); + expect(fn?.type).toBe(TypeKind.FunctionKind); +}); diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts new file mode 100644 index 00000000000000..3ee6676cf5e32c --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts @@ -0,0 +1,88 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Node } from 'ts-morph'; +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { buildClassDec } from './build_class_dec'; +import { buildFunctionDec } from './build_function_dec'; +import { getCommentsFromNode } from './js_doc_utils'; +import { isNamedNode } from '../tsmorph_utils'; +import { AnchorLink, ApiDeclaration } from '../types'; +import { buildVariableDec } from './build_variable_dec'; +import { getApiSectionId } from '../utils'; +import { getSourceForNode } from './utils'; +import { buildTypeLiteralDec } from './build_type_literal_dec'; +import { ApiScope } from '../types'; +import { getSignature } from './get_signature'; +import { buildInterfaceDec } from './build_interface_dec'; +import { getTypeKind } from './get_type_kind'; + +/** + * A potentially recursive function, depending on the node type, that builds a JSON like structure + * that can be passed to the elastic-docs component for rendering as an API. Nodes like classes, + * interfaces, objects and functions will have children for their properties, members and parameters. + * + * @param node The ts-morph node to build an ApiDeclaration for. + * @param plugins The list of plugins registered is used for building cross plugin links by looking up + * the plugin by import path. We could accomplish the same thing via a regex on the import path, but this lets us + * decouple plugin path from plugin id. + * @param log Logs messages to console. + * @param pluginName The name of the plugin this declaration belongs to. + * @param scope The scope this declaration belongs to (server, public, or common). + * @param parentApiId If this declaration is nested inside another declaration, it should have a parent id. This + * is used to create the anchor link to this API item. + * @param name An optional name to pass through which will be used instead of node.getName, if it + * exists. For some types, like Parameters, the name comes on the parent node, but we want the doc def + * to be built from the TypedNode + */ +export function buildApiDeclaration( + node: Node, + plugins: KibanaPlatformPlugin[], + log: ToolingLog, + pluginName: string, + scope: ApiScope, + parentApiId?: string, + name?: string +): ApiDeclaration { + const apiName = name ? name : isNamedNode(node) ? node.getName() : 'Unnamed'; + log.debug(`Building API Declaration for ${apiName} of kind ${node.getKindName()}`); + const apiId = parentApiId ? parentApiId + '.' + apiName : apiName; + const anchorLink: AnchorLink = { scope, pluginName, apiName: apiId }; + + if (Node.isClassDeclaration(node)) { + return buildClassDec(node, plugins, anchorLink, log); + } else if (Node.isInterfaceDeclaration(node)) { + return buildInterfaceDec(node, plugins, anchorLink, log); + } else if ( + Node.isMethodSignature(node) || + Node.isFunctionDeclaration(node) || + Node.isMethodDeclaration(node) || + Node.isConstructorDeclaration(node) + ) { + return buildFunctionDec(node, plugins, anchorLink, log); + } else if ( + Node.isPropertySignature(node) || + Node.isPropertyDeclaration(node) || + Node.isShorthandPropertyAssignment(node) || + Node.isPropertyAssignment(node) || + Node.isVariableDeclaration(node) + ) { + return buildVariableDec(node, plugins, anchorLink, log); + } else if (Node.isTypeLiteralNode(node)) { + return buildTypeLiteralDec(node, plugins, anchorLink, log, apiName); + } + + return { + id: getApiSectionId(anchorLink), + type: getTypeKind(node), + label: apiName, + description: getCommentsFromNode(node), + source: getSourceForNode(node), + signature: getSignature(node, plugins, log), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts new file mode 100644 index 00000000000000..146fcf4fa4d0a9 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; + +import { + ArrowFunction, + VariableDeclaration, + PropertyDeclaration, + PropertySignature, + ShorthandPropertyAssignment, + PropertyAssignment, +} from 'ts-morph'; +import { getApiSectionId } from '../utils'; +import { getCommentsFromNode } from './js_doc_utils'; +import { AnchorLink, TypeKind } from '../types'; +import { getSourceForNode } from './utils'; +import { buildApiDecsForParameters } from './build_parameter_decs'; +import { getSignature } from './get_signature'; +import { getJSDocReturnTagComment } from './js_doc_utils'; + +/** + * Arrow functions are handled differently than regular functions because you need the arrow function + * initializer as well as the node. The initializer is where the parameters are grabbed from and the + * signature, while the node has the comments and name. + * + * @param node + * @param initializer + * @param plugins + * @param anchorLink + * @param log + */ +export function getArrowFunctionDec( + node: + | VariableDeclaration + | PropertyDeclaration + | PropertySignature + | ShorthandPropertyAssignment + | PropertyAssignment, + initializer: ArrowFunction, + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog +) { + log.debug( + `Getting Arrow Function doc def for node ${node.getName()} of kind ${node.getKindName()}` + ); + return { + id: getApiSectionId(anchorLink), + type: TypeKind.FunctionKind, + children: buildApiDecsForParameters(initializer.getParameters(), plugins, anchorLink, log), + signature: getSignature(initializer, plugins, log), + description: getCommentsFromNode(node), + label: node.getName(), + source: getSourceForNode(node), + returnComment: getJSDocReturnTagComment(node), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_class_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_class_dec.ts new file mode 100644 index 00000000000000..2ccce506dde530 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_class_dec.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { ClassDeclaration } from 'ts-morph'; +import { AnchorLink, ApiDeclaration, TypeKind } from '../types'; +import { getCommentsFromNode } from './js_doc_utils'; +import { buildApiDeclaration } from './build_api_declaration'; +import { getSourceForNode, isPrivate } from './utils'; +import { getApiSectionId } from '../utils'; +import { getSignature } from './get_signature'; + +export function buildClassDec( + node: ClassDeclaration, + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog +): ApiDeclaration { + return { + id: getApiSectionId(anchorLink), + type: TypeKind.ClassKind, + label: node.getName() || 'Missing label', + description: getCommentsFromNode(node), + signature: getSignature(node, plugins, log), + children: node.getMembers().reduce((acc, m) => { + if (!isPrivate(m)) { + acc.push( + buildApiDeclaration( + m, + plugins, + log, + anchorLink.pluginName, + anchorLink.scope, + anchorLink.apiName + ) + ); + } + return acc; + }, [] as ApiDeclaration[]), + source: getSourceForNode(node), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts new file mode 100644 index 00000000000000..2936699152a83c --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts @@ -0,0 +1,60 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + FunctionDeclaration, + MethodDeclaration, + ConstructorDeclaration, + Node, + MethodSignature, +} from 'ts-morph'; + +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { buildApiDecsForParameters } from './build_parameter_decs'; +import { AnchorLink, ApiDeclaration, TypeKind } from '../types'; +import { getCommentsFromNode } from './js_doc_utils'; +import { getApiSectionId } from '../utils'; +import { getJSDocReturnTagComment, getJSDocs, getJSDocTagNames } from './js_doc_utils'; +import { getSourceForNode } from './utils'; +import { getSignature } from './get_signature'; + +/** + * Takes the various function-like node declaration types and converts them into an ApiDeclaration. + * @param node + * @param plugins + * @param anchorLink + * @param log + */ +export function buildFunctionDec( + node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature, + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog +): ApiDeclaration { + const label = Node.isConstructorDeclaration(node) + ? 'Constructor' + : node.getName() || '(WARN: Missing name)'; + log.debug(`Getting function doc def for node ${label} of kind ${node.getKindName()}`); + return { + id: getApiSectionId(anchorLink), + type: TypeKind.FunctionKind, + label, + signature: getSignature(node, plugins, log), + description: getCommentsFromNode(node), + children: buildApiDecsForParameters( + node.getParameters(), + plugins, + anchorLink, + log, + getJSDocs(node) + ), + tags: getJSDocTagNames(node), + returnComment: getJSDocReturnTagComment(node), + source: getSourceForNode(node), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_type_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_type_dec.ts new file mode 100644 index 00000000000000..3cd4317e23f0c9 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_type_dec.ts @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { FunctionTypeNode, JSDoc } from 'ts-morph'; +import { getApiSectionId } from '../utils'; +import { getCommentsFromNode } from './js_doc_utils'; +import { AnchorLink, ApiDeclaration, TypeKind } from '../types'; +import { buildApiDecsForParameters } from './build_parameter_decs'; +import { extractImportReferences } from './extract_import_refs'; +import { getJSDocReturnTagComment, getJSDocs, getJSDocTagNames } from './js_doc_utils'; +import { getSourceForNode } from './utils'; + +export function buildApiDecFromFunctionType( + name: string, + node: FunctionTypeNode, + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog, + jsDocs?: JSDoc[] +): ApiDeclaration { + log.debug(`Getting Function Type doc def for node ${name} of kind ${node.getKindName()}`); + return { + type: TypeKind.FunctionKind, + id: getApiSectionId(anchorLink), + label: name, + signature: extractImportReferences(node.getType().getText(), plugins, log), + description: getCommentsFromNode(node), + tags: jsDocs ? getJSDocTagNames(jsDocs) : [], + returnComment: jsDocs ? getJSDocReturnTagComment(jsDocs) : [], + children: buildApiDecsForParameters( + node.getParameters(), + plugins, + anchorLink, + log, + jsDocs || getJSDocs(node) + ), + source: getSourceForNode(node), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_interface_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_interface_dec.ts new file mode 100644 index 00000000000000..2329aa2190d958 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_interface_dec.ts @@ -0,0 +1,44 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { InterfaceDeclaration } from 'ts-morph'; +import { AnchorLink, ApiDeclaration, TypeKind } from '../types'; +import { getCommentsFromNode } from './js_doc_utils'; +import { buildApiDeclaration } from './build_api_declaration'; +import { getSourceForNode } from './utils'; +import { getApiSectionId } from '../utils'; +import { getSignature } from './get_signature'; + +export function buildInterfaceDec( + node: InterfaceDeclaration, + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog +): ApiDeclaration { + return { + id: getApiSectionId(anchorLink), + type: TypeKind.InterfaceKind, + label: node.getName(), + signature: getSignature(node, plugins, log), + description: getCommentsFromNode(node), + children: node + .getMembers() + .map((m) => + buildApiDeclaration( + m, + plugins, + log, + anchorLink.pluginName, + anchorLink.scope, + anchorLink.apiName + ) + ), + source: getSourceForNode(node), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_parameter_decs.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_parameter_decs.ts new file mode 100644 index 00000000000000..e420f76357f75c --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_parameter_decs.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ParameterDeclaration, JSDoc, SyntaxKind } from 'ts-morph'; +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { extractImportReferences } from './extract_import_refs'; +import { AnchorLink, ApiDeclaration } from '../types'; +import { buildApiDeclaration } from './build_api_declaration'; +import { getJSDocParamComment } from './js_doc_utils'; +import { getSourceForNode } from './utils'; +import { getTypeKind } from './get_type_kind'; + +/** + * A helper function to capture function parameters, whether it comes from an arrow function, a regular function or + * a function type. + * + * @param params + * @param plugins + * @param anchorLink + * @param log + * @param jsDocs + */ +export function buildApiDecsForParameters( + params: ParameterDeclaration[], + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog, + jsDocs?: JSDoc[] +): ApiDeclaration[] { + return params.reduce((acc, param) => { + const label = param.getName(); + log.debug(`Getting parameter doc def for ${label} of kind ${param.getKindName()}`); + // Literal types are non primitives that aren't references to other types. We add them as a more + // defined node, with children. + // If we don't want the docs to be too deeply nested we could avoid this special handling. + if (param.getTypeNode() && param.getTypeNode()!.getKind() === SyntaxKind.TypeLiteral) { + acc.push( + buildApiDeclaration( + param.getTypeNode()!, + plugins, + log, + anchorLink.pluginName, + anchorLink.scope, + anchorLink.apiName, + label + ) + ); + } else { + acc.push({ + type: getTypeKind(param), + label, + isRequired: param.getType().isNullable() === false, + signature: extractImportReferences(param.getType().getText(), plugins, log), + description: jsDocs ? getJSDocParamComment(jsDocs, label) : [], + source: getSourceForNode(param), + }); + } + return acc; + }, [] as ApiDeclaration[]); +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_type_literal_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_type_literal_dec.ts new file mode 100644 index 00000000000000..39d47d5bacba1d --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_type_literal_dec.ts @@ -0,0 +1,55 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { TypeLiteralNode } from 'ts-morph'; +import { getApiSectionId } from '../utils'; +import { getCommentsFromNode } from './js_doc_utils'; +import { AnchorLink, ApiDeclaration, TypeKind } from '../types'; +import { buildApiDeclaration } from './build_api_declaration'; +import { getSourceForNode } from './utils'; + +/** + * This captures function parameters that are object types, and makes sure their + * properties are recursively walked so they are expandable in the docs. + * + * The test verifying `crazyFunction` will fail without this special handling. + * + * @param node + * @param plugins + * @param anchorLink + * @param log + * @param name + */ +export function buildTypeLiteralDec( + node: TypeLiteralNode, + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog, + name: string +): ApiDeclaration { + return { + id: getApiSectionId(anchorLink), + type: TypeKind.ObjectKind, + label: name, + description: getCommentsFromNode(node), + children: node + .getMembers() + .map((m) => + buildApiDeclaration( + m, + plugins, + log, + anchorLink.pluginName, + anchorLink.scope, + anchorLink.apiName + ) + ), + source: getSourceForNode(node), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts new file mode 100644 index 00000000000000..3e0b48de1e18b1 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts @@ -0,0 +1,82 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { + VariableDeclaration, + Node, + PropertyAssignment, + PropertyDeclaration, + PropertySignature, + ShorthandPropertyAssignment, +} from 'ts-morph'; +import { getApiSectionId } from '../utils'; +import { getCommentsFromNode } from './js_doc_utils'; +import { AnchorLink, ApiDeclaration, TypeKind } from '../types'; +import { getArrowFunctionDec } from './build_arrow_fn_dec'; +import { buildApiDeclaration } from './build_api_declaration'; +import { getSourceForNode } from './utils'; +import { getSignature } from './get_signature'; +import { getTypeKind } from './get_type_kind'; + +/** + * Special handling for objects and arrow functions which are variable or property node types. + * Objects and arrow functions need their children extracted recursively. This uses the name from the + * node, but checks for an initializer to get inline arrow functions and objects defined recursively. + * + * @param node + * @param plugins + * @param anchorLink + * @param log + */ +export function buildVariableDec( + node: + | VariableDeclaration + | PropertyAssignment + | PropertyDeclaration + | PropertySignature + | ShorthandPropertyAssignment, + plugins: KibanaPlatformPlugin[], + anchorLink: AnchorLink, + log: ToolingLog +): ApiDeclaration { + log.debug('buildVariableDec for ' + node.getName()); + const initializer = node.getInitializer(); + // Recusively list object properties as children. + if (initializer && Node.isObjectLiteralExpression(initializer)) { + return { + id: getApiSectionId(anchorLink), + type: TypeKind.ObjectKind, + children: initializer.getProperties().map((prop) => { + return buildApiDeclaration( + prop, + plugins, + log, + anchorLink.pluginName, + anchorLink.scope, + anchorLink.apiName + ); + }), + description: getCommentsFromNode(node), + label: node.getName(), + source: getSourceForNode(node), + }; + } else if (initializer && Node.isArrowFunction(initializer)) { + return getArrowFunctionDec(node, initializer, plugins, anchorLink, log); + } + + // Otherwise return it just as a single entry. + return { + id: getApiSectionId(anchorLink), + type: getTypeKind(node), + label: node.getName(), + description: getCommentsFromNode(node), + source: getSourceForNode(node), + signature: getSignature(node, plugins, log), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts new file mode 100644 index 00000000000000..a757df2ece366b --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts @@ -0,0 +1,96 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; +import { getPluginApiDocId } from '../utils'; +import { extractImportReferences } from './extract_import_refs'; +import { ApiScope, Reference } from '../types'; +import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock'; + +const plugin = getKibanaPlatformPlugin('pluginA'); +const plugins: KibanaPlatformPlugin[] = [plugin]; + +const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, +}); + +it('when there are no imports', () => { + const results = extractImportReferences(`(param: string) => Bar`, plugins, log); + expect(results.length).toBe(1); + expect(results[0]).toBe('(param: string) => Bar'); +}); + +it('test extractImportReference', () => { + const results = extractImportReferences( + `(param: string) => import("${plugin.directory}/public/bar").Bar`, + plugins, + log + ); + expect(results.length).toBe(2); + expect(results[0]).toBe('(param: string) => '); + expect(results[1]).toEqual({ + text: 'Bar', + docId: getPluginApiDocId('plugin_a', log), + section: 'def-public.Bar', + pluginId: 'pluginA', + scope: ApiScope.CLIENT, + }); +}); + +it('test extractImportReference with public folder nested under server folder', () => { + const results = extractImportReferences( + `import("${plugin.directory}/server/routes/public/bar").Bar`, + plugins, + log + ); + expect(results.length).toBe(1); + expect(results[0]).toEqual({ + text: 'Bar', + docId: getPluginApiDocId('plugin_a', log), + section: 'def-server.Bar', + pluginId: 'pluginA', + scope: ApiScope.SERVER, + }); +}); + +it('test extractImportReference with two imports', () => { + const results = extractImportReferences( + ``, + plugins, + log + ); + expect(results.length).toBe(5); + expect(results[0]).toBe(''); +}); + +it('test extractImportReference with unknown imports', () => { + const results = extractImportReferences( + ``, + plugins, + log + ); + expect(results.length).toBe(3); + expect(results[0]).toBe(''); +}); + +it('test single link', () => { + const results = extractImportReferences( + `import("${plugin.directory}/public/foo/index").FooFoo`, + plugins, + log + ); + expect(results.length).toBe(1); + expect((results[0] as Reference).text).toBe('FooFoo'); +}); diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts new file mode 100644 index 00000000000000..1147e15a1acb60 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts @@ -0,0 +1,115 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; +import { getApiSectionId, getPluginApiDocId, getPluginForPath } from '../utils'; +import { ApiScope, TextWithLinks } from '../types'; + +/** + * + * @param text A string that may include an API item that was imported from another file. For example: + * "export type foo = string | import("kibana/src/plugins/a_plugin/public/path").Bar". + * @param plugins The list of registered Kibana plugins. Used to get the plugin id, which is then used to create + * the DocLink to that plugin's page, based off the relative path of any imports. + * @param log Logging utility for debuging + * + * @returns An array structure that can be used to create react DocLinks. For example, the above text would return + * something like: + * [ "export type foo = string | ", // Just a string for the pretext + * { id: "a_plugin", section: "public.Bar", text: "Bar" } // An object with info to create the DocLink. + * ] + */ +export function extractImportReferences( + text: string, + plugins: KibanaPlatformPlugin[], + log: ToolingLog +): TextWithLinks { + const texts: TextWithLinks = []; + let pos = 0; + let textSegment: string | undefined = text; + const max = 5; + while (textSegment) { + pos++; + if (pos > max) break; + + const ref = extractImportRef(textSegment); + if (ref) { + const { name, path, index, length } = ref; + if (index !== 0) { + texts.push(textSegment.substr(0, index)); + } + const plugin = getPluginForPath(path, plugins); + + if (!plugin) { + if (path.indexOf('plugin') >= 0) { + log.warning('WARN: no plugin found for reference path ' + path); + } + // If we can't create a link for this, still remove the import("..."). part to make + // it easier to read. + const str = textSegment.substr(index + length - name.length, name.length); + if (str && str !== '') { + texts.push(str); + } + } else { + const section = getApiSectionId({ + pluginName: plugin.manifest.id, + scope: getScopeFromPath(path, plugin, log), + apiName: name, + }); + texts.push({ + pluginId: plugin.manifest.id, + scope: getScopeFromPath(path, plugin, log), + docId: getPluginApiDocId(plugin.manifest.id, log, { + serviceFolders: plugin.manifest.serviceFolders, + apiPath: path, + directory: plugin.directory, + }), + section, + text: name, + }); + } + textSegment = textSegment.substr(index + length); + } else { + if (textSegment && textSegment !== '') { + texts.push(textSegment); + } + textSegment = undefined; + } + } + return texts; +} + +function extractImportRef( + str: string +): { path: string; name: string; index: number; length: number } | undefined { + const groups = str.match(/import\("(.*?)"\)\.(\w*)/); + if (groups) { + const path = groups[1]; + const name = groups[2]; + const index = groups.index!; + const length = groups[0].length; + return { path, name, index, length }; + } +} + +/** + * + * @param path An absolute path to a file inside a plugin directory. + */ +function getScopeFromPath(path: string, plugin: KibanaPlatformPlugin, log: ToolingLog): ApiScope { + if (path.startsWith(`${plugin.directory}/public/`)) { + return ApiScope.CLIENT; + } else if (path.startsWith(`${plugin.directory}/server/`)) { + return ApiScope.SERVER; + } else if (path.startsWith(`${plugin.directory}/common/`)) { + return ApiScope.COMMON; + } else { + log.warning(`Unexpected path encountered ${path}`); + return ApiScope.COMMON; + } +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/get_signature.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/get_signature.ts new file mode 100644 index 00000000000000..b7df94a03638f0 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/get_signature.ts @@ -0,0 +1,120 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; +import { Node, Type } from 'ts-morph'; +import { isNamedNode } from '../tsmorph_utils'; +import { Reference } from '../types'; +import { extractImportReferences } from './extract_import_refs'; +import { getTypeKind } from './get_type_kind'; + +/** + * Special logic for creating the signature based on the type of node. See https://github.com/dsherret/ts-morph/issues/923#issue-795332729 + * for some issues that have been encountered in getting these accurate. + * + * By passing node to `getText`, ala `node.getType().getText(node)`, all reference links + * will be lost. However, if you do _not_ pass node, there are quite a few situations where it returns a reference + * to itself and has no helpful information. + * + * @param node + * @param plugins + * @param log + */ +export function getSignature( + node: Node, + plugins: KibanaPlatformPlugin[], + log: ToolingLog +): Array | undefined { + let signature = ''; + // node.getType() on a TypeAliasDeclaration is just a reference to itself. If we don't special case this, then + // `export type Foo = string | number;` would show up with a signagure of `Foo` that is a link to itself, instead of + // `string | number`. + if (Node.isTypeAliasDeclaration(node)) { + signature = getSignatureForTypeAlias(node.getType(), log, node); + } else if (Node.isFunctionDeclaration(node)) { + // See https://github.com/dsherret/ts-morph/issues/907#issue-770284331. + // Unfortunately this has to be manually pieced together, or it comes up as "typeof TheFunction" + const params = node + .getParameters() + .map((p) => `${p.getName()}: ${p.getType().getText()}`) + .join(', '); + const returnType = node.getReturnType().getText(); + signature = `(${params}) => ${returnType}`; + } else if (Node.isInterfaceDeclaration(node) || Node.isClassDeclaration(node)) { + // Need to tack on manually any type parameters or "extends/implements" section. + const heritageClause = node + .getHeritageClauses() + .map((h) => { + const heritance = h.getText().indexOf('implements') > -1 ? 'implements' : 'extends'; + return `${heritance} ${h.getTypeNodes().map((n) => n.getType().getText())}`; + }) + .join(' '); + signature = `${node.getType().getText()}${heritageClause ? ' ' + heritageClause : ''}`; + } else { + // Here, 'node' is explicitly *not* passed in to `getText` otherwise arrow functions won't + // include reference links. Tests will break if you add it in here, or remove it from above. + // There is test coverage for all this oddness. + signature = node.getType().getText(); + } + + // Don't return the signature if it's the same as the type (string, string) + if (getTypeKind(node).toString() === signature) return undefined; + + const referenceLinks = extractImportReferences(signature, plugins, log); + + // Don't return the signature if it's a single self referential link. + if ( + isNamedNode(node) && + referenceLinks.length === 1 && + typeof referenceLinks[0] === 'object' && + (referenceLinks[0] as Reference).text === node.getName() + ) { + return undefined; + } + + return referenceLinks; +} + +/** + * Not all types are handled here, but does return links for the more common ones. + */ +function getSignatureForTypeAlias(type: Type, log: ToolingLog, node?: Node): string { + if (type.isUnion()) { + return type + .getUnionTypes() + .map((nestedType) => getSignatureForTypeAlias(nestedType, log)) + .join(' | '); + } else if (node && type.getCallSignatures().length >= 1) { + return type + .getCallSignatures() + .map((sig) => { + const params = sig + .getParameters() + .map((p) => `${p.getName()}: ${p.getTypeAtLocation(node).getText()}`) + .join(', '); + const returnType = sig.getReturnType().getText(); + return `(${params}) => ${returnType}`; + }) + .join(' '); + } else if (node) { + const symbol = node.getSymbol(); + if (symbol) { + const declarations = symbol + .getDeclarations() + .map((d) => d.getType().getText(node)) + .join(' '); + if (symbol.getDeclarations().length !== 1) { + log.error( + `Node is type alias declaration with more than one declaration. This is not handled! ${declarations} and node is ${node.getText()}` + ); + } + return declarations; + } + } + return type.getText(); +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/get_type_kind.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/get_type_kind.ts new file mode 100644 index 00000000000000..592833f40cebf2 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/get_type_kind.ts @@ -0,0 +1,69 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Type, Node } from 'ts-morph'; +import { TypeKind } from '../types'; + +export function getTypeKind(node: Node): TypeKind { + if (Node.isTypeAliasDeclaration(node)) { + return TypeKind.TypeKind; + } else { + return getTypeKindForType(node.getType()); + } +} + +function getTypeKindForType(type: Type): TypeKind { + // I think a string literal is also a string... but just in case, checking both. + if (type.isString() || type.isStringLiteral()) { + return TypeKind.StringKind; + } else if (type.isNumber() || type.isNumberLiteral()) { + return TypeKind.NumberKind; + + // I could be wrong about this logic. Does this existance of a call signature mean it's a function? + } else if (type.getCallSignatures().length > 0) { + return TypeKind.FunctionKind; + } else if (type.isArray()) { + // Arrays are also objects, check this first. + return TypeKind.ArrayKind; + } else if (type.isObject()) { + return TypeKind.ObjectKind; + } else if (type.isBoolean() || type.isBooleanLiteral()) { + return TypeKind.BooleanKind; + } else if (type.isEnum() || type.isEnumLiteral()) { + return TypeKind.EnumKind; + } else if (type.isUnion()) { + // Special handling for "type | undefined" which happens alot and should be represented in docs as + // "type", but with an "optional" flag. Anything more complicated will just be returned as a + // "CompoundType". + if (getIsTypeOptional(type) && type.getUnionTypes().length === 2) { + const otherType = type.getUnionTypes().find((u) => u.isUndefined() === false); + if (otherType) { + return getTypeKindForType(otherType); + } + } + } else if (type.isAny()) { + return TypeKind.AnyKind; + } else if (type.isUnknown()) { + return TypeKind.UnknownKind; + } + + if (type.isUnionOrIntersection()) { + return TypeKind.CompoundTypeKind; + } + + return TypeKind.Uncategorized; +} + +function getIsTypeOptional(type: Type): boolean { + if (type.isUnion()) { + const unions = type.getUnionTypes(); + return unions.find((u) => u.isUndefined()) !== undefined; + } else { + return false; + } +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/js_doc_utils.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/js_doc_utils.ts new file mode 100644 index 00000000000000..baac7153dfb758 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/js_doc_utils.ts @@ -0,0 +1,87 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { JSDoc, JSDocTag, Node } from 'ts-morph'; +import { TextWithLinks } from '../types'; + +/** + * Extracts comments out of the node to use as the description. + */ +export function getCommentsFromNode(node: Node): TextWithLinks | undefined { + let comments: TextWithLinks | undefined; + const jsDocs = getJSDocs(node); + if (jsDocs) { + return getTextWithLinks(jsDocs.map((jsDoc) => jsDoc.getDescription()).join('\n')); + } else { + comments = getTextWithLinks( + node + .getLeadingCommentRanges() + .map((c) => c.getText()) + .join('\n') + ); + } + + return comments; +} + +export function getJSDocs(node: Node): JSDoc[] | undefined { + if (Node.isJSDocableNode(node)) { + return node.getJsDocs(); + } else if (Node.isVariableDeclaration(node)) { + const gparent = node.getParent()?.getParent(); + if (Node.isJSDocableNode(gparent)) { + return gparent.getJsDocs(); + } + } +} + +export function getJSDocReturnTagComment(node: Node | JSDoc[]): TextWithLinks { + const tags = getJSDocTags(node); + const returnTag = tags.find((tag) => Node.isJSDocReturnTag(tag)); + if (returnTag) return getTextWithLinks(returnTag.getComment()); + return []; +} + +export function getJSDocParamComment(node: Node | JSDoc[], name: string): TextWithLinks { + const tags = getJSDocTags(node); + const paramTag = tags.find((tag) => Node.isJSDocParameterTag(tag) && tag.getName() === name); + if (paramTag) return getTextWithLinks(paramTag.getComment()); + return []; +} + +export function getJSDocTagNames(node: Node | JSDoc[]): string[] { + return getJSDocTags(node).reduce((tags, tag) => { + if (tag.getTagName() !== 'param' && tag.getTagName() !== 'returns') { + tags.push(tag.getTagName()); + } + return tags; + }, [] as string[]); +} + +function getJSDocTags(node: Node | JSDoc[]): JSDocTag[] { + const jsDocs = node instanceof Array ? node : getJSDocs(node); + if (!jsDocs) return []; + + return jsDocs.reduce((tagsAcc, jsDoc) => { + tagsAcc.push(...jsDoc.getTags()); + return tagsAcc; + }, [] as JSDocTag[]); +} + +/** + * TODO. This feature is not implemented yet. It will be used to create links for comments + * that use {@link AnotherAPIItemInThisPlugin}. + * + * @param text + */ +function getTextWithLinks(text?: string): TextWithLinks { + if (text) return [text]; + else return []; + // TODO: + // Replace `@links` in comments with relative api links. +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts new file mode 100644 index 00000000000000..9efa96b6e96769 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import Path from 'path'; +import { REPO_ROOT, kibanaPackageJson } from '@kbn/utils'; +import { ParameterDeclaration, ClassMemberTypes, Node } from 'ts-morph'; +import { SourceLink } from '../types'; + +export function isPrivate(node: ParameterDeclaration | ClassMemberTypes): boolean { + return node.getModifiers().find((mod) => mod.getText() === 'private') !== undefined; +} + +/** + * Change the absolute path into a relative one. + */ +function getRelativePath(fullPath: string): string { + return Path.relative(REPO_ROOT, fullPath); +} + +export function getSourceForNode(node: Node): SourceLink { + const path = getRelativePath(node.getSourceFile().getFilePath()); + const lineNumber = node.getStartLineNumber(); + return { + path, + lineNumber, + link: `https://github.com/elastic/kibana/tree/${kibanaPackageJson.branch}${path}#L${lineNumber}`, + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts new file mode 100644 index 00000000000000..ac6d6088c25c0c --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts @@ -0,0 +1,152 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs'; +import Path from 'path'; + +import { REPO_ROOT, run } from '@kbn/dev-utils'; +import { Project } from 'ts-morph'; + +import { getPluginApi } from './get_plugin_api'; +import { writePluginDocs } from './mdx/write_plugin_mdx_docs'; +import { ApiDeclaration, PluginApi } from './types'; +import { findPlugins } from './find_plugins'; +import { removeBrokenLinks } from './utils'; + +export interface PluginInfo { + apiCount: number; + apiCountMissingComments: number; + id: string; + missingApiItems: string[]; +} + +export function runBuildApiDocsCli() { + run( + async ({ log }) => { + const project = getTsProject(REPO_ROOT); + + const plugins = findPlugins(); + + const pluginInfos: { + [key: string]: PluginInfo; + } = {}; + + const outputFolder = Path.resolve(REPO_ROOT, 'api_docs'); + if (!Fs.existsSync(outputFolder)) { + Fs.mkdirSync(outputFolder); + } else { + // Delete all files except the README that warns about the auto-generated nature of + // the folder. + const files = Fs.readdirSync(outputFolder); + files.forEach((file) => { + if (file.indexOf('README.md') < 0) { + Fs.rmSync(Path.resolve(outputFolder, file)); + } + }); + } + + const pluginApiMap: { [key: string]: PluginApi } = {}; + plugins.map((plugin) => { + pluginApiMap[plugin.manifest.id] = getPluginApi(project, plugin, plugins, log); + }); + + const missingApiItems: { [key: string]: string[] } = {}; + + plugins.forEach((plugin) => { + const id = plugin.manifest.id; + const pluginApi = pluginApiMap[id]; + removeBrokenLinks(pluginApi, missingApiItems, pluginApiMap); + }); + + plugins.forEach((plugin) => { + const id = plugin.manifest.id; + const pluginApi = pluginApiMap[id]; + const info = { + id, + apiCount: countApiForPlugin(pluginApi), + apiCountMissingComments: countMissingCommentsApiForPlugin(pluginApi), + missingApiItems: missingApiItems[id], + }; + + if (info.apiCount > 0) { + writePluginDocs(outputFolder, pluginApi, log); + pluginInfos[id] = info; + } + }); + + // eslint-disable-next-line no-console + console.table(pluginInfos); + }, + { + log: { + defaultLevel: 'debug', + }, + } + ); +} + +function getTsProject(repoPath: string) { + const xpackTsConfig = `${repoPath}/tsconfig.json`; + const project = new Project({ + tsConfigFilePath: xpackTsConfig, + }); + project.addSourceFilesAtPaths(`${repoPath}/x-pack/plugins/**/*{.d.ts,.ts}`); + project.resolveSourceFileDependencies(); + return project; +} + +function countMissingCommentsApiForPlugin(doc: PluginApi) { + return ( + doc.client.reduce((sum, def) => { + return sum + countMissingCommentsForApi(def); + }, 0) + + doc.server.reduce((sum, def) => { + return sum + countMissingCommentsForApi(def); + }, 0) + + doc.common.reduce((sum, def) => { + return sum + countMissingCommentsForApi(def); + }, 0) + ); +} + +function countMissingCommentsForApi(doc: ApiDeclaration): number { + const missingCnt = doc.description && doc.description.length > 0 ? 0 : 1; + if (!doc.children) return missingCnt; + else + return ( + missingCnt + + doc.children.reduce((sum, child) => { + return sum + countMissingCommentsForApi(child); + }, 0) + ); +} + +function countApiForPlugin(doc: PluginApi) { + return ( + doc.client.reduce((sum, def) => { + return sum + countApi(def); + }, 0) + + doc.server.reduce((sum, def) => { + return sum + countApi(def); + }, 0) + + doc.common.reduce((sum, def) => { + return sum + countApi(def); + }, 0) + ); +} + +function countApi(doc: ApiDeclaration): number { + if (!doc.children) return 1; + else + return ( + 1 + + doc.children.reduce((sum, child) => { + return sum + countApi(child); + }, 0) + ); +} diff --git a/packages/kbn-docs-utils/src/api_docs/find_plugins.ts b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts new file mode 100644 index 00000000000000..004124f13889d3 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts @@ -0,0 +1,25 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import { getPluginSearchPaths } from '@kbn/config'; +import { simpleKibanaPlatformPluginDiscovery, REPO_ROOT } from '@kbn/dev-utils'; + +export function findPlugins() { + const pluginSearchPaths = getPluginSearchPaths({ + rootDir: REPO_ROOT, + oss: false, + examples: false, + }); + + return simpleKibanaPlatformPluginDiscovery(pluginSearchPaths, [ + // discover "core" as a plugin + Path.resolve(REPO_ROOT, 'src/core'), + ]); +} diff --git a/packages/kbn-docs-utils/src/api_docs/get_declaration_nodes_for_plugin.ts b/packages/kbn-docs-utils/src/api_docs/get_declaration_nodes_for_plugin.ts new file mode 100644 index 00000000000000..6676c5e753c9be --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/get_declaration_nodes_for_plugin.ts @@ -0,0 +1,73 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; +import { Project, SourceFile, Node } from 'ts-morph'; +import { ApiScope } from './types'; +import { isNamedNode, getSourceFileMatching } from './tsmorph_utils'; + +/** + * Determines which file in the project to grab nodes from, depending on the plugin and scope, then returns those nodes. + * + * @param project - TS project. + * @param plugin - The plugin we are interested in. + * @param scope - The "scope" of the API we want to extract: public, server or common. + * @param log - logging utility. + * + * @return Every publically exported Node from the given plugin and scope (public, server, common). + */ +export function getDeclarationNodesForPluginScope( + project: Project, + plugin: KibanaPlatformPlugin, + scope: ApiScope, + log: ToolingLog +): Node[] { + const path = Path.join(`${plugin.directory}`, scope.toString(), 'index.ts'); + const file = getSourceFileMatching(project, path); + + if (file) { + return getExportedFileDeclarations(file, log); + } else { + log.debug(`No file found: ${path}`); + return []; + } +} + +/** + * + * @param source the file we want to extract exported declaration nodes from. + * @param log + */ +function getExportedFileDeclarations(source: SourceFile, log: ToolingLog): Node[] { + const nodes: Node[] = []; + const exported = source.getExportedDeclarations(); + + // Filter out the exported declarations that exist only for the plugin system itself. + exported.forEach((val) => { + val.forEach((ed) => { + const name: string = isNamedNode(ed) ? ed.getName() : ''; + + // Every plugin will have an export called "plugin". Don't bother listing + // it, it's only for the plugin infrastructure. + // Config is also a common export on the server side that is just for the + // plugin infrastructure. + if (name === 'plugin' || name === 'config') { + return; + } + if (name && name !== '') { + nodes.push(ed); + } else { + log.warning(`API with missing name encountered.`); + } + }); + }); + + log.debug(`Collected ${nodes.length} exports from file ${source.getFilePath()}`); + return nodes; +} diff --git a/packages/kbn-docs-utils/src/api_docs/get_plugin_api.ts b/packages/kbn-docs-utils/src/api_docs/get_plugin_api.ts new file mode 100644 index 00000000000000..ede60e95ce2b96 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/get_plugin_api.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import { Node, Project, Type } from 'ts-morph'; +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { ApiScope, Lifecycle } from './types'; +import { ApiDeclaration, PluginApi } from './types'; +import { buildApiDeclaration } from './build_api_declarations/build_api_declaration'; +import { getDeclarationNodesForPluginScope } from './get_declaration_nodes_for_plugin'; +import { getSourceFileMatching } from './tsmorph_utils'; + +/** + * Collects all the information neccessary to generate this plugins mdx api file(s). + */ +export function getPluginApi( + project: Project, + plugin: KibanaPlatformPlugin, + plugins: KibanaPlatformPlugin[], + log: ToolingLog +): PluginApi { + const client = getDeclarations(project, plugin, ApiScope.CLIENT, plugins, log); + const server = getDeclarations(project, plugin, ApiScope.SERVER, plugins, log); + const common = getDeclarations(project, plugin, ApiScope.COMMON, plugins, log); + return { + id: plugin.manifest.id, + client, + server, + common, + serviceFolders: plugin.manifest.serviceFolders, + }; +} + +/** + * + * @returns All exported ApiDeclarations for the given plugin and scope (client, server, common), broken into + * groups of typescript kinds (functions, classes, interfaces, etc). + */ +function getDeclarations( + project: Project, + plugin: KibanaPlatformPlugin, + scope: ApiScope, + plugins: KibanaPlatformPlugin[], + log: ToolingLog +): ApiDeclaration[] { + const nodes = getDeclarationNodesForPluginScope(project, plugin, scope, log); + + const contractTypes = getContractTypes(project, plugin, scope); + + const declarations = nodes.reduce((acc, node) => { + const apiDec = buildApiDeclaration(node, plugins, log, plugin.manifest.id, scope); + // Filter out apis with the @internal flag on them. + if (!apiDec.tags || apiDec.tags.indexOf('internal') < 0) { + // buildApiDeclaration doesn't set the lifecycle, so we set it here. + const lifecycle = getLifecycle(node, contractTypes); + acc.push({ + ...apiDec, + lifecycle, + initialIsOpen: lifecycle !== undefined, + }); + } + return acc; + }, []); + + // We have all the ApiDeclarations, now lets group them by typescript kinds. + return declarations; +} + +/** + * Checks if this node is one of the special start or setup contract interface types. We pull these + * to the top of the API docs. + * + * @param node ts-morph node + * @param contractTypeNames the start and setup contract interface names + * @returns Which, if any, lifecycle contract this node happens to represent. + */ +function getLifecycle( + node: Node, + contractTypeNames: { start?: Type; setup?: Type } +): Lifecycle | undefined { + // Note this logic is not tested if a plugin uses "as", + // like export { Setup as MyPluginSetup } from ..." + if (contractTypeNames.start && node.getType() === contractTypeNames.start) { + return Lifecycle.START; + } + + if (contractTypeNames.setup && node.getType() === contractTypeNames.setup) { + return Lifecycle.SETUP; + } +} + +/** + * + * @param project + * @param plugin the plugin we are interested in. + * @param scope Whether we are interested in the client or server plugin contracts. + * Common scope will never return anything. + * @returns the name of the two types used for Start and Setup contracts, if they + * exist and were exported from the plugin class. + */ +function getContractTypes( + project: Project, + plugin: KibanaPlatformPlugin, + scope: ApiScope +): { setup?: Type; start?: Type } { + const contractTypes: { setup?: Type; start?: Type } = {}; + const file = getSourceFileMatching( + project, + Path.join(`${plugin.directory}`, scope.toString(), 'plugin.ts') + ); + if (file) { + file.getClasses().forEach((c) => { + c.getImplements().forEach((i) => { + let index = 0; + i.getType() + .getTypeArguments() + .forEach((arg) => { + // Setup type comes first + if (index === 0) { + contractTypes.setup = arg; + } else if (index === 1) { + contractTypes.start = arg; + } + index++; + }); + }); + }); + } + return contractTypes; +} diff --git a/packages/kbn-docs-utils/src/api_docs/index.ts b/packages/kbn-docs-utils/src/api_docs/index.ts new file mode 100644 index 00000000000000..8dca507764a79b --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/index.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './build_api_docs_cli'; diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/split_apis_by_folder.test.ts b/packages/kbn-docs-utils/src/api_docs/mdx/split_apis_by_folder.test.ts new file mode 100644 index 00000000000000..67e17c95d2298a --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/mdx/split_apis_by_folder.test.ts @@ -0,0 +1,51 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import { Project } from 'ts-morph'; +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; + +import { PluginApi } from '../types'; +import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock'; +import { getPluginApi } from '../get_plugin_api'; +import { splitApisByFolder } from './write_plugin_split_by_folder'; + +const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, +}); + +let doc: PluginApi; + +beforeAll(() => { + const tsConfigFilePath = Path.resolve(__dirname, '../tests/__fixtures__/src/tsconfig.json'); + const project = new Project({ + tsConfigFilePath, + }); + + expect(project.getSourceFiles().length).toBeGreaterThan(0); + + const pluginA = getKibanaPlatformPlugin('pluginA'); + pluginA.manifest.serviceFolders = ['foo']; + const plugins: KibanaPlatformPlugin[] = [pluginA]; + + doc = getPluginApi(project, plugins[0], plugins, log); +}); + +test('foo service has all exports', () => { + expect(doc?.client.length).toBe(33); + const split = splitApisByFolder(doc); + expect(split.length).toBe(2); + + const fooDoc = split.find((d) => d.id === 'pluginA.foo'); + const mainDoc = split.find((d) => d.id === 'pluginA'); + + expect(fooDoc?.common.length).toBe(1); + expect(fooDoc?.client.length).toBe(2); + expect(mainDoc?.client.length).toBe(31); +}); diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts new file mode 100644 index 00000000000000..b35515eb9d209e --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts @@ -0,0 +1,143 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog } from '@kbn/dev-utils'; +import fs from 'fs'; +import Path from 'path'; +import dedent from 'dedent'; +import { PluginApi, ScopeApi } from '../types'; +import { + countScopeApi, + getPluginApiDocId, + snakeToCamel, + camelToSnake, + groupPluginApi, +} from '../utils'; +import { writePluginDocSplitByFolder } from './write_plugin_split_by_folder'; + +/** + * Converts the plugin doc to mdx and writes it into the file system. If the plugin, + * has serviceFolders specified in it's kibana.json, multiple mdx files will be written. + * + * @param folder The location the mdx files will be written too. + * @param doc Contains the information of the plugin that will be written into mdx. + * @param log Used for logging debug and error information. + */ +export function writePluginDocs(folder: string, doc: PluginApi, log: ToolingLog): void { + if (doc.serviceFolders) { + log.debug(`Splitting plugin ${doc.id}`); + writePluginDocSplitByFolder(folder, doc, log); + } else { + writePluginDoc(folder, doc, log); + } +} + +function hasPublicApi(doc: PluginApi): boolean { + return doc.client.length > 0 || doc.server.length > 0 || doc.common.length > 0; +} + +/** + * Converts the plugin doc to mdx and writes it into the file system. Ignores + * the serviceFolders setting. Use {@link writePluginDocs} if you wish to split + * the plugin into potentially multiple mdx files. + * + * @param folder The location the mdx file will be written too. + * @param doc Contains the information of the plugin that will be written into mdx. + * @param log Used for logging debug and error information. + */ +export function writePluginDoc(folder: string, doc: PluginApi, log: ToolingLog): void { + if (!hasPublicApi(doc)) { + log.debug(`${doc.id} does not have a public api. Skipping.`); + return; + } + + log.debug(`Writing plugin file for ${doc.id}`); + + const fileName = getFileName(doc.id); + // Append "obj" to avoid special names in here. 'case' is one in particular that + // caused issues. + const json = getJsonName(fileName) + 'Obj'; + let mdx = + dedent(` +--- +id: ${getPluginApiDocId(doc.id, log)} +slug: /kibana-dev-docs/${doc.id}PluginApi +title: ${doc.id} +image: https://source.unsplash.com/400x175/?github +summary: API docs for the ${doc.id} plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '${doc.id}'] +--- + +import ${json} from './${fileName}.json'; + +`) + '\n\n'; + + const scopedDoc = { + ...doc, + client: groupPluginApi(doc.client), + common: groupPluginApi(doc.common), + server: groupPluginApi(doc.server), + }; + fs.writeFileSync(Path.resolve(folder, fileName + '.json'), JSON.stringify(scopedDoc)); + + mdx += scopApiToMdx(scopedDoc.client, 'Client', json, 'client'); + mdx += scopApiToMdx(scopedDoc.server, 'Server', json, 'server'); + mdx += scopApiToMdx(scopedDoc.common, 'Common', json, 'common'); + + fs.writeFileSync(Path.resolve(folder, fileName + '.mdx'), mdx); +} + +function getJsonName(name: string): string { + return snakeToCamel(getFileName(name)); +} + +function getFileName(name: string): string { + return camelToSnake(name.replace('.', '_')); +} + +function scopApiToMdx(scope: ScopeApi, title: string, json: string, scopeName: string): string { + let mdx = ''; + if (countScopeApi(scope) > 0) { + mdx += `## ${title}\n\n`; + + if (scope.setup) { + mdx += `### Setup\n`; + mdx += `\n`; + } + if (scope.start) { + mdx += `### Start\n`; + mdx += `\n`; + } + if (scope.objects.length > 0) { + mdx += `### Objects\n`; + mdx += `\n`; + } + if (scope.functions.length > 0) { + mdx += `### Functions\n`; + mdx += `\n`; + } + if (scope.classes.length > 0) { + mdx += `### Classes\n`; + mdx += `\n`; + } + if (scope.interfaces.length > 0) { + mdx += `### Interfaces\n`; + mdx += `\n`; + } + if (scope.enums.length > 0) { + mdx += `### Enums\n`; + mdx += `\n`; + } + if (scope.misc.length > 0) { + mdx += `### Consts, variables and types\n`; + mdx += `\n`; + } + } + return mdx; +} diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.test.ts b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.test.ts new file mode 100644 index 00000000000000..153b3299d81363 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Project } from 'ts-morph'; +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; +import { splitApisByFolder } from './write_plugin_split_by_folder'; +import { getPluginApi } from '../get_plugin_api'; +import { getKibanaPlatformPlugin } from '../tests/kibana_platform_plugin_mock'; + +const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, +}); + +it('splitApisByFolder test splitting plugin by service folder', () => { + const project = new Project({ useInMemoryFileSystem: true }); + project.createSourceFile( + 'src/plugins/example/public/index.ts', + ` +import { bar } from './a_service/foo/bar'; +import { Zed, zed } from './a_service/zed'; +import { util } from './utils'; + +export { bar, Zed, zed, mainFoo, util }; +` + ); + project.createSourceFile( + 'src/plugins/example/public/a_service/zed.ts', + `export const zed: string = 'hi'; +export interface Zed = { zed: string }` + ); + project.createSourceFile( + 'src/plugins/example/public/a_service/foo/bar.ts', + `export const bar: string = 'bar';` + ); + project.createSourceFile( + 'src/plugins/example/public/utils.ts', + `export const util: string = 'Util';` + ); + + const plugin = getKibanaPlatformPlugin('example', '/src/plugins/example'); + const plugins: KibanaPlatformPlugin[] = [ + { + ...plugin, + manifest: { + ...plugin.manifest, + serviceFolders: ['a_service'], + }, + }, + ]; + + const doc = getPluginApi(project, plugins[0], plugins, log); + const docs = splitApisByFolder(doc); + + // The api at the main level, and one on a service level. + expect(docs.length).toBe(2); + + const mainDoc = docs.find((d) => d.id === 'example'); + + expect(mainDoc).toBeDefined(); + + const serviceDoc = docs.find((d) => d.id === 'example.aService'); + + expect(serviceDoc).toBeDefined(); + + expect(serviceDoc?.client.length).toBe(3); +}); diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.ts b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.ts new file mode 100644 index 00000000000000..f5d547fc03520d --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.ts @@ -0,0 +1,69 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog } from '@kbn/dev-utils'; +import { snakeToCamel } from '../utils'; +import { PluginApi, ApiDeclaration } from '../types'; +import { writePluginDoc } from './write_plugin_mdx_docs'; + +export function writePluginDocSplitByFolder(folder: string, doc: PluginApi, log: ToolingLog) { + const apisByFolder = splitApisByFolder(doc); + + log.debug(`Split ${doc.id} into ${apisByFolder.length} services`); + apisByFolder.forEach((docDef) => { + writePluginDoc(folder, docDef, log); + }); +} + +export function splitApisByFolder(pluginDoc: PluginApi): PluginApi[] { + const pluginDocDefsByFolder: { [key: string]: PluginApi } = {}; + const mainPluginDocDef = createServicePluginDocDef(pluginDoc); + + pluginDoc.client.forEach((dec: ApiDeclaration) => { + addSection(dec, 'client', mainPluginDocDef, pluginDocDefsByFolder, pluginDoc.serviceFolders!); + }); + pluginDoc.server.forEach((dec: ApiDeclaration) => { + addSection(dec, 'server', mainPluginDocDef, pluginDocDefsByFolder, pluginDoc.serviceFolders!); + }); + pluginDoc.common.forEach((dec: ApiDeclaration) => { + addSection(dec, 'common', mainPluginDocDef, pluginDocDefsByFolder, pluginDoc.serviceFolders!); + }); + + return [...Object.values(pluginDocDefsByFolder), mainPluginDocDef]; +} + +function addSection( + dec: ApiDeclaration, + scope: 'client' | 'server' | 'common', + mainPluginDocDef: PluginApi, + pluginServices: { [key: string]: PluginApi }, + serviceFolders: readonly string[] +) { + const scopeFolder = scope === 'client' ? 'public' : scope; + const matchGroup = dec.source.path.match(`.*?\/${scopeFolder}\/([^\/]*?)\/`); + const serviceFolderName = matchGroup ? matchGroup[1] : undefined; + + if (serviceFolderName && serviceFolders.find((f) => f === serviceFolderName)) { + const service = snakeToCamel(serviceFolderName); + if (!pluginServices[service]) { + pluginServices[service] = createServicePluginDocDef(mainPluginDocDef, service); + } + pluginServices[service][scope].push(dec); + } else { + mainPluginDocDef[scope].push(dec); + } +} + +function createServicePluginDocDef(pluginDoc: PluginApi, service?: string): PluginApi { + return { + id: service ? pluginDoc.id + '.' + service : pluginDoc.id, + client: [], + server: [], + common: [], + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts new file mode 100644 index 00000000000000..198856dbb10c43 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const commonFoo = 'COMMON VAR!'; diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts new file mode 100644 index 00000000000000..3fb7e375b25425 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { commonFoo } from './foo'; + +export interface ImACommonType { + goo: number; +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/kibana.json b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/kibana.json new file mode 100644 index 00000000000000..84b46caa708021 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "pluginA", + "summary": "This an example plugin for testing the api documentation system", + "version": "kibana", + "serviceFolders": ["foo"] + } + \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts new file mode 100644 index 00000000000000..c68d146f715029 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts @@ -0,0 +1,80 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable max-classes-per-file */ + +import { ImAType } from './types'; + +/** + * An interface with a generic. + */ +export interface WithGen { + t: T; +} + +export interface AnotherInterface { + t: T; +} + +export class ExampleClass implements AnotherInterface { + /** + * This should not be exposed in the docs! + */ + private privateVar: string; + + public component?: React.ComponentType; + + constructor(public t: T) { + this.privateVar = 'hi'; + } + + /** + * an arrow fn on a class. + * @param a im a string + */ + arrowFn = (a: ImAType): ImAType => a; + + /** + * A function on a class. + * @param a a param + */ + getVar(a: ImAType) { + return this.privateVar; + } +} + +export class CrazyClass

extends ExampleClass> {} + +/** + * This is an example interface so we can see how it appears inside the API + * documentation system. + */ +export interface ExampleInterface extends AnotherInterface { + /** + * This gets a promise that resolves to a string. + */ + getAPromiseThatResolvesToString: () => Promise; + + /** + * This function takes a generic. It was sometimes being tripped on + * and returned as an unknown type with no signature. + */ + aFnWithGen: (t: T) => void; + + /** + * These are not coming back properly. + */ + aFn(): void; +} + +/** + * An interface that has a react component. + */ +export interface IReturnAReactComponent { + component: React.ComponentType; +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts new file mode 100644 index 00000000000000..52abd520f82590 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts @@ -0,0 +1,76 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CrazyClass } from './classes'; +import { notAnArrowFn } from './fns'; +import { ImAType } from './types'; + +/** + * Some of the plugins wrap static exports in an object to create + * a namespace like this. + */ +export const aPretendNamespaceObj = { + /** + * The docs should show this inline comment. + */ + notAnArrowFn, + + /** + * Should this comment show up? + */ + aPropertyMisdirection: notAnArrowFn, + + /** + * I'm a property inline fun. + */ + aPropertyInlineFn: (a: ImAType): ImAType => { + return a; + }, + + /** + * The only way for this to have a comment is to grab this. + */ + aPropertyStr: 'Hi', + + /** + * Will this nested object have it's children extracted appropriately? + */ + nestedObj: { + foo: 'string', + }, +}; + +/** + * This is a complicated union type + */ +export const aUnionProperty: string | number | (() => string) | CrazyClass = '6'; + +/** + * This is an array of strings. The type is explicit. + */ +export const aStrArray: string[] = ['hi', 'bye']; + +/** + * This is an array of numbers. The type is implied. + */ +export const aNumArray = [1, 3, 4]; + +/** + * A string that says hi to you! + */ +export const aStr: string = 'hi'; + +/** + * It's a number. A special number. + */ +export const aNum = 10; + +/** + * I'm a type of string, but more specifically, a literal string type. + */ +export const literalString = 'HI'; diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts new file mode 100644 index 00000000000000..c341a80c0875dc --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts @@ -0,0 +1,78 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TypeWithGeneric, ImAType } from './types'; + +/** + * This is a non arrow function. + * + * @param a The letter A + * @param b Feed me to the function + * @param c So many params + * @param d a great param + * @param e Another comment + * @returns something! + */ +export function notAnArrowFn( + a: string, + b: number | undefined, + c: TypeWithGeneric, + d: ImAType, + e?: string +): TypeWithGeneric { + return ['hi']; +} + +/** + * This is an arrow function. + * + * @param a The letter A + * @param b Feed me to the function + * @param c So many params + * @param d a great param + * @param e Another comment + * @returns something! + */ +export const arrowFn = ( + a: string, + b: number | undefined, + c: TypeWithGeneric, + d: ImAType, + e?: string +): TypeWithGeneric => { + return ['hi']; +}; + +/** + * Who would write such a complicated function?? Ewwww. + * + * According to https://jsdoc.app/tags-param.html#parameters-with-properties, + * this is how destructured arguements should be commented. + * + * @param obj A very crazy parameter that is destructured when passing in. + * @param objWithFn Im an object with a function. Destructed! + * @param objWithFn.fn A fn. + * @param objWithStr Im an object with a string. Destructed! + * @param objWithStr.str A str. + * + * @returns I have no idea. + * + */ +export const crazyFunction = ( + obj: { hi: string }, + { fn }: { fn: (foo: { param: string }) => number }, + { str }: { str: string } +) => () => () => fn({ param: str }); + +interface ImNotExported { + foo: string; +} + +export const fnWithNonExportedRef = (a: ImNotExported) => 'shi'; + +export type NotAnArrowFnType = typeof notAnArrowFn; diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts new file mode 100644 index 00000000000000..67db6d1a15db5a --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const doTheFooFnThing = () => {}; + +export type FooType = () => 'foo'; + +export type ImNotExportedFromIndex = () => { bar: string }; diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts new file mode 100644 index 00000000000000..89bfb07e515ff9 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts @@ -0,0 +1,24 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginA, Setup, Start, SearchSpec } from './plugin'; +export { Setup, Start, SearchSpec }; + +export { doTheFooFnThing, FooType } from './foo'; + +export * from './fns'; +export * from './classes'; +export * from './const_vars'; +export * from './types'; + +export const imAnAny: any = 'hi'; +export const imAnUnknown: unknown = 'hi'; + +export function plugin() { + return new PluginA(); +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts new file mode 100644 index 00000000000000..839dc828c1886c --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts @@ -0,0 +1,172 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// The logic for grabbing Setup and Start types relies on implementing an +// interface with at least two type args. Since the test code isn't adding +// every import file, use this mock, otherwise it won't have the type and will +// fail. +interface PluginMock { + setup(): Sp; + start(): St; +} + +/** + * The SearchSpec interface contains settings for creating a new SearchService, like + * username and password. + */ +export interface SearchSpec { + /** + * Stores the username. Duh, + */ + username: string; + /** + * Stores the password. I hope it's encrypted! + */ + password: string; +} + +/** + * The type of search language. + */ +export enum SearchLanguage { + /** + * The SQL SearchLanguage type + */ + SQL, + /** + * The EQL SearchLanguage type. Support sequences. + */ + EQL, + /** + * The ES DSL SearchLanguage type. It's the default. + */ + ES_DSL, +} + +/** + * Access start functionality from your plugin's start function by adding the example + * plugin as a dependency. + * + * ```ts + * Class MyPlugin { + * start(core: CoreDependencies, { example }: PluginDependencies) { + * // Here you can access this functionality. + * example.getSearchLanguage(); + * } + * } + * ``` + */ +export interface Start { + /** + * @returns The currently selected {@link SearchLanguage} + */ + getSearchLanguage: () => SearchLanguage; +} + +/** + * Access setup functionality from your plugin's setup function by adding the example + * plugin as a dependency. + * + * ```ts + * Class MyPlugin { + * setup(core: CoreDependencies, { example }: PluginDependencies) { + * // Here you can access this functionality. + * example.getSearchService(); + * } + * } + * ``` + */ +export interface Setup { + /** + * A factory function that returns a new instance of Foo based + * on the spec. We aren't sure if this is a good function so it's marked + * beta. That should be clear in the docs because of the js doc tag. + * + * @param searchSpec Provide the settings neccessary to create a new Search Service + * + * @returns the id of the search service. + * + * @beta + */ + getSearchService: (searchSpec: SearchSpec) => string; + + /** + * This uses an inlined object type rather than referencing an exported type, which is discouraged. + * prefer the way {@link getSearchService} is typed. + * + * @param searchSpec Provide the settings neccessary to create a new Search Service + */ + getSearchService2: (searchSpec: { username: string; password: string }) => string; + + /** + * This function does the thing and it's so good at it! But we decided to deprecate it + * anyway. I hope that's clear to developers in the docs! + * + * @param thingOne Thing one comment + * @param thingTwo ThingTwo comment + * @param thingThree Thing three is an object with a nested var + * + * @deprecated + * + */ + doTheThing: (thingOne: number, thingTwo: string, thingThree: { nestedVar: number }) => void; + + /** + * Who would write such a complicated function?? Ew, how will the obj parameter appear in docs? + * + * @param obj A funky parameter. + * + * @returns It's hard to tell but I think this returns a function that returns an object with a + * property that is a function that returns a string. Whoa. + * + */ + fnWithInlineParams: (obj: { + fn: (foo: { param: string }) => number; + }) => () => { retFoo: () => string }; + + /** + * Hi, I'm a comment for an id string! + */ + id: string; +} + +/** + * This comment won't show up in the API docs. + */ +function getSearchService() { + return 'hi'; +} + +function fnWithInlineParams() { + return () => ({ + retFoo: () => 'hi', + }); +} + +/** + * The example search plugin is a fake plugin that is built only to test our api documentation system. + * + */ +export class PluginA implements PluginMock { + setup() { + return { + // Don't put comments here - they won't show up. What's here shouldn't matter because + // the API documentation system works off the type `Setup`. + doTheThing: () => {}, + fnWithInlineParams, + getSearchService, + getSearchService2: getSearchService, + registerSearch: () => {}, + id: '123', + }; + } + + start() { + return { getSearchLanguage: () => SearchLanguage.EQL }; + } +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts new file mode 100644 index 00000000000000..0f06f08018c22e --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ImACommonType } from '../common'; +import { FooType, ImNotExportedFromIndex } from './foo'; + +/** + * How should a potentially undefined type show up. + */ +export type StringOrUndefinedType = string | undefined; + +export type TypeWithGeneric = T[]; + +export type ImAType = string | number | TypeWithGeneric | FooType | ImACommonType; + +/** + * This is a type that defines a function. + * + * @param t This is a generic T type. It can be anything. + */ +export type FnWithGeneric = (t: T) => TypeWithGeneric; + +/** + * Comments on enums. + */ +export enum DayOfWeek { + THURSDAY, + FRIDAY, // How about this comment, hmmm? + SATURDAY, +} + +/** + * Calling node.getSymbol().getDeclarations() will return > 1 declaration. + */ +export type MultipleDeclarationsType = TypeWithGeneric; + +export type IRefANotExportedType = ImNotExportedFromIndex | { zed: 'hi' }; +export interface ImAnObject { + foo: FnWithGeneric; +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json new file mode 100644 index 00000000000000..57353d8847ae13 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "incremental": false, + "strictNullChecks": true, + }, + "include": ["./**/*"] +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts b/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts new file mode 100644 index 00000000000000..f32e58bdfc023e --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts @@ -0,0 +1,397 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs'; +import Path from 'path'; + +import { Project } from 'ts-morph'; +import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; + +import { writePluginDocs } from '../mdx/write_plugin_mdx_docs'; +import { ApiDeclaration, PluginApi, Reference, TextWithLinks, TypeKind } from '../types'; +import { getKibanaPlatformPlugin } from './kibana_platform_plugin_mock'; +import { getPluginApi } from '../get_plugin_api'; +import { groupPluginApi } from '../utils'; + +const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, +}); + +let doc: PluginApi; +let mdxOutputFolder: string; + +function linkCount(signature: TextWithLinks): number { + return signature.reduce((cnt, next) => (typeof next === 'string' ? cnt : cnt + 1), 0); +} + +function fnIsCorrect(fn: ApiDeclaration | undefined) { + expect(fn).toBeDefined(); + expect(fn?.type).toBe(TypeKind.FunctionKind); + // The signature should contain a link to ExampleInterface param. + expect(fn?.signature).toBeDefined(); + expect(linkCount(fn!.signature!)).toBe(3); + + expect(fn?.children!.length).toBe(5); + expect(fn?.returnComment!.length).toBe(1); + + const p1 = fn?.children!.find((c) => c.label === 'a'); + expect(p1).toBeDefined(); + expect(p1!.type).toBe(TypeKind.StringKind); + expect(p1!.isRequired).toBe(true); + expect(p1!.signature?.length).toBe(1); + expect(linkCount(p1!.signature!)).toBe(0); + + const p2 = fn?.children!.find((c) => c.label === 'b'); + expect(p2).toBeDefined(); + expect(p2!.isRequired).toBe(false); + expect(p2!.type).toBe(TypeKind.NumberKind); + expect(p2!.signature?.length).toBe(1); + expect(linkCount(p2!.signature!)).toBe(0); + + const p3 = fn?.children!.find((c) => c.label === 'c'); + expect(p3).toBeDefined(); + expect(p3!.isRequired).toBe(true); + expect(p3!.type).toBe(TypeKind.ArrayKind); + expect(linkCount(p3!.signature!)).toBe(1); + + const p4 = fn?.children!.find((c) => c.label === 'd'); + expect(p4).toBeDefined(); + expect(p4!.isRequired).toBe(true); + expect(p4!.type).toBe(TypeKind.CompoundTypeKind); + expect(p4!.signature?.length).toBe(1); + expect(linkCount(p4!.signature!)).toBe(1); + + const p5 = fn?.children!.find((c) => c.label === 'e'); + expect(p5).toBeDefined(); + expect(p5!.isRequired).toBe(false); + expect(p5!.type).toBe(TypeKind.StringKind); + expect(p5!.signature?.length).toBe(1); + expect(linkCount(p5!.signature!)).toBe(0); +} + +beforeAll(() => { + const tsConfigFilePath = Path.resolve(__dirname, '__fixtures__/src/tsconfig.json'); + const project = new Project({ + tsConfigFilePath, + }); + + expect(project.getSourceFiles().length).toBeGreaterThan(0); + + const pluginA = getKibanaPlatformPlugin('pluginA'); + pluginA.manifest.serviceFolders = ['foo']; + const plugins: KibanaPlatformPlugin[] = [pluginA]; + + doc = getPluginApi(project, plugins[0], plugins, log); + + mdxOutputFolder = Path.resolve(__dirname, 'snapshots'); + writePluginDocs(mdxOutputFolder, doc, log); +}); + +it('Setup type is extracted', () => { + const grouped = groupPluginApi(doc.client); + expect(grouped.setup).toBeDefined(); +}); + +it('service mdx file was created', () => { + expect(fs.existsSync(Path.resolve(mdxOutputFolder, 'plugin_a_foo.mdx'))).toBe(true); +}); + +it('Setup type has comment', () => { + const grouped = groupPluginApi(doc.client); + expect(grouped.setup!.description).toBeDefined(); + expect(grouped.setup!.description).toMatchInlineSnapshot(` + Array [ + " + Access setup functionality from your plugin's setup function by adding the example + plugin as a dependency. + + \`\`\`ts + Class MyPlugin { + setup(core: CoreDependencies, { example }: PluginDependencies) { + // Here you can access this functionality. + example.getSearchService(); + } + } + \`\`\`", + ] + `); +}); + +it('const exported from common folder is correct', () => { + const fooConst = doc.common.find((c) => c.label === 'commonFoo'); + expect(fooConst).toBeDefined(); + + expect(fooConst!.source.path.replace(Path.sep, '/')).toContain( + 'src/plugin_a/common/foo/index.ts' + ); + expect(fooConst!.signature![0]).toBe('"COMMON VAR!"'); +}); + +describe('functions', () => { + it('function referencing missing type has link removed', () => { + const fn = doc.client.find((c) => c.label === 'fnWithNonExportedRef'); + expect(linkCount(fn?.signature!)).toBe(0); + }); + it('arrow function is exported correctly', () => { + const fn = doc.client.find((c) => c.label === 'arrowFn'); + // Using the same data as the not an arrow function so this is refactored. + fnIsCorrect(fn); + }); + + it('non arrow function is exported correctly', () => { + const fn = doc.client.find((c) => c.label === 'notAnArrowFn'); + // Using the same data as the arrow function so this is refactored. + fnIsCorrect(fn); + }); + + it('crazyFunction is typed correctly', () => { + const fn = doc.client!.find((c) => c.label === 'crazyFunction'); + + expect(fn).toBeDefined(); + + const obj = fn?.children?.find((c) => c.label === 'obj'); + expect(obj).toBeDefined(); + expect(obj!.children?.length).toBe(1); + + const hi = obj?.children?.find((c) => c.label === 'hi'); + expect(hi).toBeDefined(); + + const obj2 = fn?.children?.find((c) => c.label === '{ fn }'); + expect(obj2).toBeDefined(); + expect(obj2!.children?.length).toBe(1); + + const fn2 = obj2?.children?.find((c) => c.label === 'fn'); + expect(fn2).toBeDefined(); + expect(fn2?.type).toBe(TypeKind.FunctionKind); + }); +}); + +describe('objects', () => { + it('Object exported correctly', () => { + const obj = doc.client.find((c) => c.label === 'aPretendNamespaceObj'); + expect(obj).toBeDefined(); + + const fn = obj?.children?.find((c) => c.label === 'notAnArrowFn'); + expect(fn?.signature).toBeDefined(); + // Should just be typeof notAnArrowFn. + expect(linkCount(fn?.signature!)).toBe(1); + // Comment should be the inline one. + expect(fn?.description).toMatchInlineSnapshot(` + Array [ + "/** + * The docs should show this inline comment. + */", + ] + `); + + const fn2 = obj?.children?.find((c) => c.label === 'aPropertyInlineFn'); + expect(fn2?.signature).toBeDefined(); + // Should include 2 links to ImAType + expect(linkCount(fn2?.signature!)).toBe(2); + expect(fn2?.children).toBeDefined(); + + const nestedObj = obj?.children?.find((c) => c.label === 'nestedObj'); + // We aren't giving objects a signature. The children should contain all the information. + expect(nestedObj?.signature).toBeUndefined(); + expect(nestedObj?.children).toBeDefined(); + expect(nestedObj?.type).toBe(TypeKind.ObjectKind); + const foo = nestedObj?.children?.find((c) => c.label === 'foo'); + expect(foo?.type).toBe(TypeKind.StringKind); + }); +}); + +describe('Misc types', () => { + it('Explicitly typed array is returned with the correct type', () => { + const aStrArray = doc.client.find((c) => c.label === 'aStrArray'); + expect(aStrArray).toBeDefined(); + expect(aStrArray?.type).toBe(TypeKind.ArrayKind); + }); + + it('Implicitly typed array is returned with the correct type', () => { + const aNumArray = doc.client.find((c) => c.label === 'aNumArray'); + expect(aNumArray).toBeDefined(); + expect(aNumArray?.type).toBe(TypeKind.ArrayKind); + }); + + it('Explicitly typed string is returned with the correct type', () => { + const aStr = doc.client.find((c) => c.label === 'aStr'); + expect(aStr).toBeDefined(); + expect(aStr?.type).toBe(TypeKind.StringKind); + // signature would be the same as type, so it should be removed. + expect(aStr?.signature).toBeUndefined(); + }); + + it('Implicitly typed number is returned with the correct type', () => { + const aNum = doc.client.find((c) => c.label === 'aNum'); + expect(aNum).toBeDefined(); + expect(aNum?.type).toBe(TypeKind.NumberKind); + }); + + it('aUnionProperty is exported as a CompoundType with a call signature', () => { + const prop = doc.client.find((c) => c.label === 'aUnionProperty'); + expect(prop).toBeDefined(); + expect(prop?.type).toBe(TypeKind.CompoundTypeKind); + expect(linkCount(prop?.signature!)).toBe(1); + }); + + it('Function type is exported correctly', () => { + const fnType = doc.client.find((c) => c.label === 'FnWithGeneric'); + expect(fnType).toBeDefined(); + expect(fnType?.type).toBe(TypeKind.TypeKind); + expect(fnType?.signature!).toMatchInlineSnapshot(` + Array [ + "(t: T) => ", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric", + }, + "", + ] + `); + expect(linkCount(fnType?.signature!)).toBe(1); + }); + + it('Union type is exported correctly', () => { + const type = doc.client.find((c) => c.label === 'ImAType'); + expect(type).toBeDefined(); + expect(type?.type).toBe(TypeKind.TypeKind); + expect(type?.signature).toBeDefined(); + expect(type?.signature!).toMatchInlineSnapshot(` + Array [ + "string | number | ", + Object { + "docId": "kibPluginAFooPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.FooType", + "text": "FooType", + }, + " | ", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric", + }, + " | ", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "common", + "section": "def-common.ImACommonType", + "text": "ImACommonType", + }, + ] + `); + + expect(linkCount(type?.signature!)).toBe(3); + expect((type!.signature![1] as Reference).docId).toBe('kibPluginAFooPluginApi'); + }); +}); + +describe('interfaces and classes', () => { + it('Basic interface exported correctly', () => { + const anInterface = doc.client.find((c) => c.label === 'IReturnAReactComponent'); + expect(anInterface).toBeDefined(); + + // Make sure it doesn't include a self referential link. + expect(anInterface?.signature).toBeUndefined(); + }); + + it('Interface which extends exported correctly', () => { + const exampleInterface = doc.client.find((c) => c.label === 'ExampleInterface'); + expect(exampleInterface).toBeDefined(); + expect(exampleInterface?.signature).toBeDefined(); + expect(exampleInterface?.type).toBe(TypeKind.InterfaceKind); + + expect(linkCount(exampleInterface?.signature!)).toBe(2); + + // TODO: uncomment if the bug is fixed. + // This is wrong, the link should be to `AnotherInterface` + // Another bug, this link is not being captured. + expect(exampleInterface?.signature).toMatchInlineSnapshot(` + Array [ + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.ExampleInterface", + "text": "ExampleInterface", + }, + " extends ", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.AnotherInterface", + "text": "AnotherInterface", + }, + "", + ] + `); + }); + + it('Non arrow function on interface is exported as function type', () => { + const exampleInterface = doc.client.find((c) => c.label === 'ExampleInterface'); + expect(exampleInterface).toBeDefined(); + + const fn = exampleInterface!.children?.find((c) => c.label === 'aFn'); + expect(fn).toBeDefined(); + expect(fn?.type).toBe(TypeKind.FunctionKind); + }); + + it('Class exported correctly', () => { + const clss = doc.client.find((c) => c.label === 'CrazyClass'); + expect(clss).toBeDefined(); + expect(clss?.signature).toBeDefined(); + expect(clss?.type).toBe(TypeKind.ClassKind); + expect(clss?.signature).toMatchInlineSnapshot(` + Array [ + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.CrazyClass", + "text": "CrazyClass", + }, + "

extends ", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.ExampleClass", + "text": "ExampleClass", + }, + "<", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": "def-public.WithGen", + "text": "WithGen", + }, + "

>", + ] + `); + expect(linkCount(clss?.signature!)).toBe(3); + }); + + it('Function with generic inside interface is exported with function type', () => { + const exampleInterface = doc.client.find((c) => c.label === 'ExampleInterface'); + expect(exampleInterface).toBeDefined(); + + const fnWithGeneric = exampleInterface?.children?.find((c) => c.label === 'aFnWithGen'); + expect(fnWithGeneric).toBeDefined(); + expect(fnWithGeneric?.type).toBe(TypeKind.FunctionKind); + }); +}); diff --git a/packages/kbn-docs-utils/src/api_docs/tests/kibana_platform_plugin_mock.ts b/packages/kbn-docs-utils/src/api_docs/tests/kibana_platform_plugin_mock.ts new file mode 100644 index 00000000000000..9debca91b7ca80 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/kibana_platform_plugin_mock.ts @@ -0,0 +1,30 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPlatformPlugin } from '@kbn/dev-utils'; +import Path from 'path'; + +export function getKibanaPlatformPlugin(id: string, dir?: string): KibanaPlatformPlugin { + const directory = dir ?? Path.resolve(__dirname, '__fixtures__/src/plugin_a'); + return { + manifest: { + id, + ui: true, + server: true, + kibanaVersion: '1', + version: '1', + serviceFolders: [], + requiredPlugins: [], + requiredBundles: [], + optionalPlugins: [], + extraPublicDirs: [], + }, + directory, + manifestPath: Path.resolve(directory, 'kibana.json'), + }; +} diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json new file mode 100644 index 00000000000000..db25b8c4f021ee --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json @@ -0,0 +1 @@ +{"id":"pluginA","client":{"classes":[{"id":"def-public.ExampleClass","type":"Class","label":"ExampleClass","description":[],"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleClass","text":"ExampleClass"}," implements ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"children":[{"id":"def-public.ExampleClass.component","type":"CompoundType","label":"component","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":30,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L30"},"signature":["React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined"]},{"id":"def-public.ExampleClass.Unnamed","type":"Function","label":"Constructor","signature":["any"],"description":[],"children":[{"type":"Uncategorized","label":"t","isRequired":true,"signature":["T"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":32,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32"}}],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":32,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32"}},{"id":"def-public.ExampleClass.arrowFn","type":"Function","children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40"}}],"signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["\nan arrow fn on a class."],"label":"arrowFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40"},"returnComment":[]},{"id":"def-public.ExampleClass.getVar","type":"Function","label":"getVar","signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => string"],"description":["\nA function on a class."],"children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["a param"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46"}}],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":24,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L24"},"initialIsOpen":false},{"id":"def-public.CrazyClass","type":"Class","label":"CrazyClass","description":[],"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.CrazyClass","text":"CrazyClass"},"

extends ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleClass","text":"ExampleClass"},"<",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.WithGen","text":"WithGen"},"

>"],"children":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":51,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L51"},"initialIsOpen":false}],"functions":[{"id":"def-public.notAnArrowFn","type":"Function","label":"notAnArrowFn","signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["\nThis is a non arrow function.\n"],"children":[{"type":"string","label":"a","isRequired":true,"signature":["string"],"description":["The letter A"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":22,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L22"}},{"type":"number","label":"b","isRequired":false,"signature":["number | undefined"],"description":["Feed me to the function"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":23,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L23"}},{"type":"Array","label":"c","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["So many params"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":24,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L24"}},{"type":"CompoundType","label":"d","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["a great param"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":25,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L25"}},{"type":"string","label":"e","isRequired":false,"signature":["string | undefined"],"description":["Another comment"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L26"}}],"tags":[],"returnComment":["something!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L21"},"initialIsOpen":false},{"id":"def-public.arrowFn","type":"Function","children":[{"type":"string","label":"a","isRequired":true,"signature":["string"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":42,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L42"}},{"type":"number","label":"b","isRequired":false,"signature":["number | undefined"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L43"}},{"type":"Array","label":"c","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L44"}},{"type":"CompoundType","label":"d","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":45,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L45"}},{"type":"string","label":"e","isRequired":false,"signature":["string | undefined"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L46"}}],"signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e?: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["\nThis is an arrow function.\n"],"label":"arrowFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":41,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L41"},"returnComment":["something!"],"initialIsOpen":false},{"id":"def-public.crazyFunction","type":"Function","children":[{"id":"def-public.crazyFunction.obj","type":"Object","label":"obj","description":[],"children":[{"id":"def-public.crazyFunction.obj.hi","type":"string","label":"hi","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67"}},{"id":"def-public.crazyFunction.{-fn }","type":"Object","label":"{ fn }","description":[],"children":[{"id":"def-public.crazyFunction.{-fn }.fn","type":"Function","label":"fn","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68"},"signature":["(foo: { param: string; }) => number"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68"}},{"id":"def-public.crazyFunction.{-str }","type":"Object","label":"{ str }","description":[],"children":[{"id":"def-public.crazyFunction.{-str }.str","type":"string","label":"str","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":69,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":69,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69"}}],"signature":["(obj: { hi: string; }, { fn }: { fn: (foo: { param: string; }) => number; }, { str }: { str: string; }) => () => () => number"],"description":["\nWho would write such a complicated function?? Ewwww.\n\nAccording to https://jsdoc.app/tags-param.html#parameters-with-properties,\nthis is how destructured arguements should be commented.\n"],"label":"crazyFunction","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":66,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L66"},"returnComment":["I have no idea."],"initialIsOpen":false},{"id":"def-public.fnWithNonExportedRef","type":"Function","children":[{"type":"Object","label":"a","isRequired":true,"signature":["ImNotExported"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76"}}],"signature":["(a: ImNotExported) => string"],"description":[],"label":"fnWithNonExportedRef","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76"},"returnComment":[],"initialIsOpen":false}],"interfaces":[{"id":"def-public.SearchSpec","type":"Interface","label":"SearchSpec","description":["\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password."],"children":[{"id":"def-public.SearchSpec.username","type":"string","label":"username","description":["\nStores the username. Duh,"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L26"}},{"id":"def-public.SearchSpec.password","type":"string","label":"password","description":["\nStores the password. I hope it's encrypted!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":30,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L30"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":22,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L22"},"initialIsOpen":false},{"id":"def-public.WithGen","type":"Interface","label":"WithGen","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.WithGen","text":"WithGen"},""],"description":["\nAn interface with a generic."],"children":[{"id":"def-public.WithGen.t","type":"Uncategorized","label":"t","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L17"},"signature":["T"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":16,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L16"},"initialIsOpen":false},{"id":"def-public.AnotherInterface","type":"Interface","label":"AnotherInterface","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"description":[],"children":[{"id":"def-public.AnotherInterface.t","type":"Uncategorized","label":"t","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L21"},"signature":["T"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":20,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L20"},"initialIsOpen":false},{"id":"def-public.ExampleInterface","type":"Interface","label":"ExampleInterface","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleInterface","text":"ExampleInterface"}," extends ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"description":["\nThis is an example interface so we can see how it appears inside the API\ndocumentation system."],"children":[{"id":"def-public.ExampleInterface.getAPromiseThatResolvesToString","type":"Function","label":"getAPromiseThatResolvesToString","description":["\nThis gets a promise that resolves to a string."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":61,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L61"},"signature":["() => Promise"]},{"id":"def-public.ExampleInterface.aFnWithGen","type":"Function","label":"aFnWithGen","description":["\nThis function takes a generic. It was sometimes being tripped on\nand returned as an unknown type with no signature."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L67"},"signature":["(t: T) => void"]},{"id":"def-public.ExampleInterface.aFn","type":"Function","label":"aFn","signature":["() => void"],"description":["\nThese are not coming back properly."],"children":[],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":72,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L72"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":57,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L57"},"initialIsOpen":false},{"id":"def-public.IReturnAReactComponent","type":"Interface","label":"IReturnAReactComponent","description":["\nAn interface that has a react component."],"children":[{"id":"def-public.IReturnAReactComponent.component","type":"CompoundType","label":"component","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":79,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L79"},"signature":["React.ComponentType<{}>"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":78,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L78"},"initialIsOpen":false},{"id":"def-public.ImAnObject","type":"Interface","label":"ImAnObject","description":[],"children":[{"id":"def-public.ImAnObject.foo","type":"Function","label":"foo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L44"},"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.FnWithGeneric","text":"FnWithGeneric"}]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L43"},"initialIsOpen":false}],"enums":[{"id":"def-public.DayOfWeek","type":"Enum","label":"DayOfWeek","description":["\nComments on enums."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L31"},"initialIsOpen":false}],"misc":[{"id":"def-public.imAnAny","type":"Any","label":"imAnAny","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts","lineNumber":19,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L19"},"signature":["any"],"initialIsOpen":false},{"id":"def-public.imAnUnknown","type":"Unknown","label":"imAnUnknown","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts","lineNumber":20,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L20"},"signature":["unknown"],"initialIsOpen":false},{"id":"def-public.NotAnArrowFnType","type":"Type","label":"NotAnArrowFnType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":78,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L78"},"signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"initialIsOpen":false},{"id":"def-public.aUnionProperty","type":"CompoundType","label":"aUnionProperty","description":["\nThis is a complicated union type"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":51,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L51"},"signature":["string | number | (() => string) | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.CrazyClass","text":"CrazyClass"},""],"initialIsOpen":false},{"id":"def-public.aStrArray","type":"Array","label":"aStrArray","description":["\nThis is an array of strings. The type is explicit."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":56,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L56"},"signature":["string[]"],"initialIsOpen":false},{"id":"def-public.aNumArray","type":"Array","label":"aNumArray","description":["\nThis is an array of numbers. The type is implied."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":61,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L61"},"signature":["number[]"],"initialIsOpen":false},{"id":"def-public.aStr","type":"string","label":"aStr","description":["\nA string that says hi to you!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":66,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L66"},"initialIsOpen":false},{"id":"def-public.aNum","type":"number","label":"aNum","description":["\nIt's a number. A special number."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":71,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L71"},"signature":["10"],"initialIsOpen":false},{"id":"def-public.literalString","type":"string","label":"literalString","description":["\nI'm a type of string, but more specifically, a literal string type."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L76"},"signature":["\"HI\""],"initialIsOpen":false},{"id":"def-public.StringOrUndefinedType","type":"Type","label":"StringOrUndefinedType","description":["\nHow should a potentially undefined type show up."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":15,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L15"},"signature":["undefined | string"],"initialIsOpen":false},{"id":"def-public.TypeWithGeneric","type":"Type","label":"TypeWithGeneric","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L17"},"signature":["T[]"],"initialIsOpen":false},{"id":"def-public.ImAType","type":"Type","label":"ImAType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":19,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L19"},"signature":["string | number | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAFooPluginApi","section":"def-public.FooType","text":"FooType"}," | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"}," | ",{"pluginId":"pluginA","scope":"common","docId":"kibPluginAPluginApi","section":"def-common.ImACommonType","text":"ImACommonType"}],"initialIsOpen":false},{"id":"def-public.FnWithGeneric","type":"Type","label":"FnWithGeneric","description":["\nThis is a type that defines a function.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L26"},"signature":["(t: T) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"initialIsOpen":false},{"id":"def-public.MultipleDeclarationsType","type":"Type","label":"MultipleDeclarationsType","description":["\nCalling node.getSymbol().getDeclarations() will return > 1 declaration."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L40"},"signature":["(typeof DayOfWeek)[]"],"initialIsOpen":false},{"id":"def-public.IRefANotExportedType","type":"Type","label":"IRefANotExportedType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":42,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L42"},"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAFooPluginApi","section":"def-public.ImNotExportedFromIndex","text":"ImNotExportedFromIndex"}," | { zed: \"hi\"; }"],"initialIsOpen":false}],"objects":[{"id":"def-public.aPretendNamespaceObj","type":"Object","children":[{"id":"def-public.aPretendNamespaceObj.notAnArrowFn","type":"Function","label":"notAnArrowFn","description":["/**\n * The docs should show this inline comment.\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L21"},"signature":["typeof ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.notAnArrowFn","text":"notAnArrowFn"}]},{"id":"def-public.aPretendNamespaceObj.aPropertyMisdirection","type":"Function","label":"aPropertyMisdirection","description":["/**\n * Should this comment show up?\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L26"},"signature":["typeof ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.notAnArrowFn","text":"notAnArrowFn"}]},{"id":"def-public.aPretendNamespaceObj.aPropertyInlineFn","type":"Function","children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31"}}],"signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["/**\n * I'm a property inline fun.\n */"],"label":"aPropertyInlineFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31"},"returnComment":[]},{"id":"def-public.aPretendNamespaceObj.aPropertyStr","type":"string","label":"aPropertyStr","description":["/**\n * The only way for this to have a comment is to grab this.\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":38,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L38"}},{"id":"def-public.aPretendNamespaceObj.nestedObj","type":"Object","children":[{"id":"def-public.aPretendNamespaceObj.nestedObj.foo","type":"string","label":"foo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L44"}}],"description":["/**\n * Will this nested object have it's children extracted appropriately?\n */"],"label":"nestedObj","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L43"}}],"description":["\nSome of the plugins wrap static exports in an object to create\na namespace like this."],"label":"aPretendNamespaceObj","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L17"},"initialIsOpen":false}],"setup":{"id":"def-public.Setup","type":"Interface","label":"Setup","description":["\nAccess setup functionality from your plugin's setup function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n setup(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchService();\n }\n}\n```"],"children":[{"id":"def-public.Setup.getSearchService","type":"Function","label":"getSearchService","description":["\nA factory function that returns a new instance of Foo based\non the spec. We aren't sure if this is a good function so it's marked\nbeta. That should be clear in the docs because of the js doc tag.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":96,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L96"},"signature":["(searchSpec: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.SearchSpec","text":"SearchSpec"},") => string"]},{"id":"def-public.Setup.getSearchService2","type":"Function","label":"getSearchService2","description":["\nThis uses an inlined object type rather than referencing an exported type, which is discouraged.\nprefer the way {@link getSearchService} is typed.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":104,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L104"},"signature":["(searchSpec: { username: string; password: string; }) => string"]},{"id":"def-public.Setup.doTheThing","type":"Function","label":"doTheThing","description":["\nThis function does the thing and it's so good at it! But we decided to deprecate it\nanyway. I hope that's clear to developers in the docs!\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":117,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L117"},"signature":["(thingOne: number, thingTwo: string, thingThree: { nestedVar: number; }) => void"]},{"id":"def-public.Setup.fnWithInlineParams","type":"Function","label":"fnWithInlineParams","description":["\nWho would write such a complicated function?? Ew, how will the obj parameter appear in docs?\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":128,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L128"},"signature":["(obj: { fn: (foo: { param: string; }) => number; }) => () => { retFoo: () => string; }"]},{"id":"def-public.Setup.id","type":"string","label":"id","description":["\nHi, I'm a comment for an id string!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":135,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L135"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":84,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L84"},"lifecycle":"setup","initialIsOpen":true},"start":{"id":"def-public.Start","type":"Interface","label":"Start","description":["\nAccess start functionality from your plugin's start function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n start(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchLanguage();\n }\n}\n```"],"children":[{"id":"def-public.Start.getSearchLanguage","type":"Function","label":"getSearchLanguage","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L68"},"signature":["() => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.SearchLanguage","text":"SearchLanguage"}]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":64,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L64"},"lifecycle":"start","initialIsOpen":true}},"server":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[],"objects":[]},"common":{"classes":[],"functions":[],"interfaces":[{"id":"def-common.ImACommonType","type":"Interface","label":"ImACommonType","description":[],"children":[{"id":"def-common.ImACommonType.goo","type":"number","label":"goo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts","lineNumber":12,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L12"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts","lineNumber":11,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L11"},"initialIsOpen":false}],"enums":[],"misc":[],"objects":[]}} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.mdx b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.mdx new file mode 100644 index 00000000000000..615bf7cb2460d1 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.mdx @@ -0,0 +1,34 @@ +--- +id: kibPluginAPluginApi +slug: /kibana-dev-docs/pluginAPluginApi +title: pluginA +image: https://source.unsplash.com/400x175/?github +summary: API docs for the pluginA plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA'] +--- + +import pluginAObj from './plugin_a.json'; + +## Client + +### Setup + +### Start + +### Objects + +### Functions + +### Classes + +### Interfaces + +### Enums + +### Consts, variables and types + +## Common + +### Interfaces + diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json new file mode 100644 index 00000000000000..8b5ec5f3da960b --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json @@ -0,0 +1 @@ +{"id":"pluginA.foo","client":{"classes":[],"functions":[{"id":"def-public.doTheFooFnThing","type":"Function","children":[],"signature":["() => void"],"description":[],"label":"doTheFooFnThing","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L9"},"returnComment":[],"initialIsOpen":false}],"interfaces":[],"enums":[],"misc":[{"id":"def-public.FooType","type":"Type","label":"FooType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":11,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L11"},"signature":["() => \"foo\""],"initialIsOpen":false}],"objects":[]},"server":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[],"objects":[]},"common":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[{"id":"def-common.commonFoo","type":"string","label":"commonFoo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts#L9"},"signature":["\"COMMON VAR!\""],"initialIsOpen":false}],"objects":[]}} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.mdx b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.mdx new file mode 100644 index 00000000000000..a4f05fdeb20763 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.mdx @@ -0,0 +1,22 @@ +--- +id: kibPluginAFooPluginApi +slug: /kibana-dev-docs/pluginA.fooPluginApi +title: pluginA.foo +image: https://source.unsplash.com/400x175/?github +summary: API docs for the pluginA.foo plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA.foo'] +--- + +import pluginAFooObj from './plugin_a_foo.json'; + +## Client + +### Functions + +### Consts, variables and types + +## Common + +### Consts, variables and types + diff --git a/packages/kbn-docs-utils/src/api_docs/tsmorph_utils.ts b/packages/kbn-docs-utils/src/api_docs/tsmorph_utils.ts new file mode 100644 index 00000000000000..f78c538019e2f1 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/tsmorph_utils.ts @@ -0,0 +1,38 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Node, SourceFile, Project } from 'ts-morph'; + +export interface NamedNode extends Node { + getName(): string; +} + +/** + * ts-morph has a Node.isNamedNode fn but it isn't returning true for all types + * that will have node.getName. + */ +export function isNamedNode(node: Node | NamedNode): node is NamedNode { + return (node as NamedNode).getName !== undefined; +} + +/** + * Helper function to find a source file at a given location. Used to extract + * index.ts files at a given scope. + * + * @param project The ts morph project which contains all the source files + * @param absolutePath The absolute path of the file we want to find + * @returns a source file that exists at the location of the relative path. + */ +export function getSourceFileMatching( + project: Project, + absolutePath: string +): SourceFile | undefined { + return project.getSourceFiles().find((file) => { + return file.getFilePath().startsWith(absolutePath); + }); +} diff --git a/packages/kbn-docs-utils/src/api_docs/types.ts b/packages/kbn-docs-utils/src/api_docs/types.ts new file mode 100644 index 00000000000000..c41cd42e6b4244 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/types.ts @@ -0,0 +1,200 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface AnchorLink { + /** + * The plugin that contains the API being referenced. + */ + pluginName: string; + /** + * It's possible the client and the server both emit an API with + * the same name so we need scope in here to add uniqueness. + */ + scope: ApiScope; + /** + * The name of the api. + */ + apiName: string; +} + +/** + * The kinds of typescript types we want to show in the docs. `Unknown` is used if + * we aren't accounting for a particular type. See {@link getPropertyTypeKind} + */ +export enum TypeKind { + ClassKind = 'Class', + FunctionKind = 'Function', + ObjectKind = 'Object', + EnumKind = 'Enum', + InterfaceKind = 'Interface', + /** + * Maps to the typescript syntax kind `TypeReferences`. For example, + * export type FooFn = () => string will be a TypeKind, not a FunctionKind. + */ + TypeKind = 'Type', + /** + * Uncategorized is used if a type is encountered that isn't handled. + */ + Uncategorized = 'Uncategorized', + UnknownKind = 'Unknown', // Maps to the unknown typescript type + AnyKind = 'Any', // Maps to the any typescript type + StringKind = 'string', + NumberKind = 'number', + BooleanKind = 'boolean', + ArrayKind = 'Array', + /** + * This will cover things like string | number, or A & B, for lack of something better to put here. + */ + CompoundTypeKind = 'CompoundType', +} + +export interface ScopeApi { + setup?: ApiDeclaration; + start?: ApiDeclaration; + functions: ApiDeclaration[]; + objects: ApiDeclaration[]; + classes: ApiDeclaration[]; + interfaces: ApiDeclaration[]; + enums: ApiDeclaration[]; + misc: ApiDeclaration[]; +} + +export interface PluginApi { + id: string; + serviceFolders?: readonly string[]; + client: ApiDeclaration[]; + server: ApiDeclaration[]; + common: ApiDeclaration[]; +} + +/** + * This is used for displaying code or comments that may contain reference links. For example, a function + * signature that is `(a: import("src/plugin_b").Bar) => void` will be parsed into the following Array: + * + * ```ts + * [ + * '(a: ', + * { docId: 'pluginB', section: 'Bar', text: 'Bar' }, + * ') => void' + * ] + * ``` + * + * This is then used to render text with nested DocLinks so it looks like this: + * + * `(a: => ) => void` + */ +export type TextWithLinks = Array; + +/** + * The information neccessary to build a DocLink. + */ +export interface Reference { + pluginId: string; + scope: ApiScope; + docId: string; + section: string; + text: string; +} + +/** + * This type should eventually be replaced by something inside elastic-docs. + * It's what will be passed to an elastic-docs supplied component to make + * the API docs pretty. + */ +export interface ApiDeclaration { + /** + * Used for an anchor link to this Api. Can't use label as there can be two labels with the same + * text within the Client section and the Server section. + */ + id?: string; + + /** + * The name of the api. + */ + label: string; + + /** + * Should the list be expanded or collapsed initially? + */ + initialIsOpen?: boolean; + + /** + * The kind of type this API represents, e.g. string, number, Object, Interface, Class. + */ + type: TypeKind; + + /** + * Certain types have children. For instance classes have class members, functions will list + * their parameters here, classes will list their class members here, and objects and interfaces + * will list their properties. + */ + children?: ApiDeclaration[]; + + /** + * TODO + */ + isRequired?: boolean; + + /** + * Api node comment. + */ + description?: TextWithLinks; + + /** + * If the type is a function, it's signature should be displayed. Currently this overlaps with type + * sometimes, and will sometimes be left empty for large types (like classes and interfaces). + */ + signature?: TextWithLinks; + + /** + * Relevant for functions with @returns comments. + */ + returnComment?: TextWithLinks; + + /** + * Will contain the tags on a comment, like `beta` or `deprecated`. + * Won't include param or returns tags. + */ + tags?: string[]; + + /** + * Every plugn that exposes functionality from their setup and start contract + * should have a single exported type for each. These get pulled to the top because + * they are accessed differently than other exported functionality and types. + */ + lifecycle?: Lifecycle; + + /** + * Used to create links to github to view the code for this API. + */ + source: SourceLink; +} + +export interface SourceLink { + path: string; + lineNumber: number; + link: string; +} + +/** + * Developers will need to know whether these APIs are available on the client, server, or both. + */ +export enum ApiScope { + CLIENT = 'public', + SERVER = 'server', + COMMON = 'common', +} + +/** + * Start and Setup interfaces are special - their functionality is not imported statically but + * accessible via the dependent plugins start and setup functions. + */ +export enum Lifecycle { + START = 'start', + SETUP = 'setup', +} diff --git a/packages/kbn-docs-utils/src/api_docs/utils.test.ts b/packages/kbn-docs-utils/src/api_docs/utils.test.ts new file mode 100644 index 00000000000000..a506405616a470 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/utils.test.ts @@ -0,0 +1,83 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; +import Path from 'path'; +import { Project } from 'ts-morph'; +import { findPlugins } from './find_plugins'; +import { getPluginApi } from './get_plugin_api'; +import { getKibanaPlatformPlugin } from './tests/kibana_platform_plugin_mock'; +import { PluginApi } from './types'; +import { getPluginForPath, getServiceForPath, removeBrokenLinks } from './utils'; + +const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, +}); + +it('test getPluginForPath', () => { + const plugins = findPlugins(); + const path = Path.resolve(__dirname, '../../../../src/plugins/embeddable/public/service/file.ts'); + expect(getPluginForPath(path, plugins)).toBeDefined(); +}); + +it('test getServiceForPath', () => { + expect(getServiceForPath('src/plugins/embed/public/service/file.ts', 'src/plugins/embed')).toBe( + 'service' + ); + expect( + getServiceForPath('src/plugins/embed/public/service/subfolder/file.ts', 'src/plugins/embed') + ).toBe('service'); + expect( + getServiceForPath('src/plugins/embed/public/file.ts', 'src/plugins/embed') + ).toBeUndefined(); + expect( + getServiceForPath('/src/plugins/embed/server/another_service/index', '/src/plugins/embed') + ).toBe('another_service'); + expect(getServiceForPath('src/plugins/embed/server/no_ending', 'src/plugins/embed')).toBe( + undefined + ); + expect( + getServiceForPath('src/plugins/embed/server/routes/public/foo/index.ts', 'src/plugins/embed') + ).toBe('routes'); + expect(getServiceForPath('src/plugins/embed/server/f.ts', 'src/plugins/embed')).toBeUndefined(); + + expect( + getServiceForPath( + '/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index', + '/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a' + ) + ).toBe('foo'); +}); + +it('test removeBrokenLinks', () => { + const tsConfigFilePath = Path.resolve(__dirname, 'tests/__fixtures__/src/tsconfig.json'); + const project = new Project({ + tsConfigFilePath, + }); + + expect(project.getSourceFiles().length).toBeGreaterThan(0); + + const pluginA = getKibanaPlatformPlugin('pluginA'); + pluginA.manifest.serviceFolders = ['foo']; + const plugins: KibanaPlatformPlugin[] = [pluginA]; + + const pluginApiMap: { [key: string]: PluginApi } = {}; + plugins.map((plugin) => { + pluginApiMap[plugin.manifest.id] = getPluginApi(project, plugin, plugins, log); + }); + + const missingApiItems: { [key: string]: string[] } = {}; + + plugins.forEach((plugin) => { + const id = plugin.manifest.id; + const pluginApi = pluginApiMap[id]; + removeBrokenLinks(pluginApi, missingApiItems, pluginApiMap); + }); + expect(missingApiItems.pluginA.indexOf('public.ImNotExportedFromIndex')).toBeGreaterThan(-1); +}); diff --git a/packages/kbn-docs-utils/src/api_docs/utils.ts b/packages/kbn-docs-utils/src/api_docs/utils.ts new file mode 100644 index 00000000000000..34162aa330911f --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/utils.ts @@ -0,0 +1,208 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; +import { + AnchorLink, + ApiDeclaration, + ScopeApi, + TypeKind, + Lifecycle, + PluginApi, + ApiScope, +} from './types'; + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export const camelToSnake = (str: string): string => str.replace(/([A-Z])/g, '_$1').toLowerCase(); + +export const snakeToCamel = (str: string): string => + str.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')); + +/** + * Returns the plugin that the file belongs to. + * @param path An absolute file path that can point to a file nested inside a plugin + * @param plugins A list of plugins to search through. + */ +export function getPluginForPath( + path: string, + plugins: KibanaPlatformPlugin[] +): KibanaPlatformPlugin | undefined { + return plugins.find((plugin) => path.startsWith(plugin.directory)); +} + +/** + * Groups ApiDeclarations by typescript kind - classes, functions, enums, etc, so they + * can be displayed separately in the mdx files. + */ +export function groupPluginApi(declarations: ApiDeclaration[]): ScopeApi { + const scope = createEmptyScope(); + + declarations.forEach((declaration) => { + addApiDeclarationToScope(declaration, scope); + }); + + return scope; +} + +function escapeRegExp(regexp: string) { + return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * If the file is at the top level, returns undefined, otherwise returns the + * name of the first nested folder in the plugin. For example a path of + * 'src/plugins/data/public/search_services/file.ts' would return 'search_service' while + * 'src/plugin/data/server/file.ts' would return undefined. + * @param path + */ +export function getServiceForPath(path: string, pluginDirectory: string): string | undefined { + const dir = escapeRegExp(pluginDirectory); + const publicMatchGroups = path.match(`${dir}\/public\/([^\/]*)\/`); + const serverMatchGroups = path.match(`${dir}\/server\/([^\/]*)\/`); + const commonMatchGroups = path.match(`${dir}\/common\/([^\/]*)\/`); + + if (publicMatchGroups && publicMatchGroups.length > 1) { + return publicMatchGroups[1]; + } else if (serverMatchGroups && serverMatchGroups.length > 1) { + return serverMatchGroups[1]; + } else if (commonMatchGroups && commonMatchGroups.length > 1) { + return commonMatchGroups[1]; + } +} + +export function getPluginApiDocId( + id: string, + log: ToolingLog, + serviceInfo?: { + serviceFolders: readonly string[]; + apiPath: string; + directory: string; + } +) { + let service = ''; + const cleanName = id.replace('.', '_'); + if (serviceInfo) { + const serviceName = getServiceForPath(serviceInfo.apiPath, serviceInfo.directory); + log.debug( + `Service for path ${serviceInfo.apiPath} and ${serviceInfo.directory} is ${serviceName}` + ); + const serviceFolder = serviceInfo.serviceFolders?.find((f) => f === serviceName); + + if (serviceFolder) { + service = snakeToCamel(serviceFolder); + } + } + + return `kib${capitalize(snakeToCamel(cleanName)) + capitalize(service)}PluginApi`; +} + +export function getApiSectionId(link: AnchorLink) { + const id = `def-${link.scope}.${link.apiName}`.replace(' ', '-'); + return id; +} + +export function countScopeApi(api: ScopeApi): number { + return ( + (api.setup ? 1 : 0) + + (api.start ? 1 : 0) + + api.classes.length + + api.interfaces.length + + api.functions.length + + api.objects.length + + api.enums.length + + api.misc.length + ); +} + +export function createEmptyScope(): ScopeApi { + return { + classes: [], + functions: [], + interfaces: [], + enums: [], + misc: [], + objects: [], + }; +} + +/** + * Takes the ApiDeclaration and puts it in the appropriate section of the ScopeApi based + * on its TypeKind. + */ +export function addApiDeclarationToScope(declaration: ApiDeclaration, scope: ScopeApi): void { + if (declaration.lifecycle === Lifecycle.SETUP) { + scope.setup = declaration; + } else if (declaration.lifecycle === Lifecycle.START) { + scope.start = declaration; + } else { + switch (declaration.type) { + case TypeKind.ClassKind: + scope.classes.push(declaration); + break; + case TypeKind.InterfaceKind: + scope.interfaces.push(declaration); + break; + case TypeKind.EnumKind: + scope.enums.push(declaration); + break; + case TypeKind.FunctionKind: + scope.functions.push(declaration); + break; + case TypeKind.ObjectKind: + scope.objects.push(declaration); + break; + default: + scope.misc.push(declaration); + } + } +} + +export function removeBrokenLinks( + pluginApi: PluginApi, + missingApiItems: { [key: string]: string[] }, + pluginApiMap: { [key: string]: PluginApi } +) { + (['client', 'common', 'server'] as Array<'client' | 'server' | 'common'>).forEach((scope) => { + pluginApi[scope].forEach((api) => { + if (api.signature) { + api.signature = api.signature.map((sig) => { + if (typeof sig !== 'string') { + if (apiItemExists(sig.text, sig.scope, pluginApiMap[sig.pluginId]) === false) { + if (missingApiItems[sig.pluginId] === undefined) { + missingApiItems[sig.pluginId] = []; + } + missingApiItems[sig.pluginId].push(`${sig.scope}.${sig.text}`); + return sig.text; + } + } + return sig; + }); + } + }); + }); +} + +function apiItemExists(name: string, scope: ApiScope, pluginApi: PluginApi): boolean { + return ( + pluginApi[scopeAccessor(scope)].findIndex((dec: ApiDeclaration) => dec.label === name) >= 0 + ); +} + +function scopeAccessor(scope: ApiScope): 'server' | 'common' | 'client' { + switch (scope) { + case ApiScope.CLIENT: + return 'client'; + case ApiScope.SERVER: + return 'server'; + default: + return 'common'; + } +} diff --git a/packages/kbn-docs-utils/src/index.ts b/packages/kbn-docs-utils/src/index.ts new file mode 100644 index 00000000000000..24aef1bf891f6f --- /dev/null +++ b/packages/kbn-docs-utils/src/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './release_notes'; +export * from './api_docs'; diff --git a/packages/kbn-release-notes/src/cli.ts b/packages/kbn-docs-utils/src/release_notes/cli.ts similarity index 100% rename from packages/kbn-release-notes/src/cli.ts rename to packages/kbn-docs-utils/src/release_notes/cli.ts diff --git a/packages/kbn-release-notes/src/formats/asciidoc.ts b/packages/kbn-docs-utils/src/release_notes/formats/asciidoc.ts similarity index 100% rename from packages/kbn-release-notes/src/formats/asciidoc.ts rename to packages/kbn-docs-utils/src/release_notes/formats/asciidoc.ts diff --git a/packages/kbn-release-notes/src/formats/csv.ts b/packages/kbn-docs-utils/src/release_notes/formats/csv.ts similarity index 100% rename from packages/kbn-release-notes/src/formats/csv.ts rename to packages/kbn-docs-utils/src/release_notes/formats/csv.ts diff --git a/packages/kbn-release-notes/src/formats/format.ts b/packages/kbn-docs-utils/src/release_notes/formats/format.ts similarity index 100% rename from packages/kbn-release-notes/src/formats/format.ts rename to packages/kbn-docs-utils/src/release_notes/formats/format.ts diff --git a/packages/kbn-release-notes/src/formats/index.ts b/packages/kbn-docs-utils/src/release_notes/formats/index.ts similarity index 100% rename from packages/kbn-release-notes/src/formats/index.ts rename to packages/kbn-docs-utils/src/release_notes/formats/index.ts diff --git a/packages/kbn-release-notes/src/index.ts b/packages/kbn-docs-utils/src/release_notes/index.ts similarity index 100% rename from packages/kbn-release-notes/src/index.ts rename to packages/kbn-docs-utils/src/release_notes/index.ts diff --git a/packages/kbn-release-notes/src/lib/classify_pr.ts b/packages/kbn-docs-utils/src/release_notes/lib/classify_pr.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/classify_pr.ts rename to packages/kbn-docs-utils/src/release_notes/lib/classify_pr.ts diff --git a/packages/kbn-release-notes/src/lib/get_fix_references.test.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.test.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/get_fix_references.test.ts rename to packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.test.ts diff --git a/packages/kbn-release-notes/src/lib/get_fix_references.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/get_fix_references.ts rename to packages/kbn-docs-utils/src/release_notes/lib/get_fix_references.ts diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.test.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/get_note_from_description.test.ts rename to packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.test.ts diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.ts b/packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/get_note_from_description.ts rename to packages/kbn-docs-utils/src/release_notes/lib/get_note_from_description.ts diff --git a/packages/kbn-release-notes/src/lib/index.ts b/packages/kbn-docs-utils/src/release_notes/lib/index.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/index.ts rename to packages/kbn-docs-utils/src/release_notes/lib/index.ts diff --git a/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts b/packages/kbn-docs-utils/src/release_notes/lib/irrelevant_pr_summary.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts rename to packages/kbn-docs-utils/src/release_notes/lib/irrelevant_pr_summary.ts diff --git a/packages/kbn-release-notes/src/lib/is_pr_relevant.ts b/packages/kbn-docs-utils/src/release_notes/lib/is_pr_relevant.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/is_pr_relevant.ts rename to packages/kbn-docs-utils/src/release_notes/lib/is_pr_relevant.ts diff --git a/packages/kbn-release-notes/src/lib/pr_api.ts b/packages/kbn-docs-utils/src/release_notes/lib/pr_api.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/pr_api.ts rename to packages/kbn-docs-utils/src/release_notes/lib/pr_api.ts diff --git a/packages/kbn-release-notes/src/lib/streams.ts b/packages/kbn-docs-utils/src/release_notes/lib/streams.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/streams.ts rename to packages/kbn-docs-utils/src/release_notes/lib/streams.ts diff --git a/packages/kbn-release-notes/src/lib/type_helpers.ts b/packages/kbn-docs-utils/src/release_notes/lib/type_helpers.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/type_helpers.ts rename to packages/kbn-docs-utils/src/release_notes/lib/type_helpers.ts diff --git a/packages/kbn-release-notes/src/lib/version.test.ts b/packages/kbn-docs-utils/src/release_notes/lib/version.test.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/version.test.ts rename to packages/kbn-docs-utils/src/release_notes/lib/version.test.ts diff --git a/packages/kbn-release-notes/src/lib/version.ts b/packages/kbn-docs-utils/src/release_notes/lib/version.ts similarity index 100% rename from packages/kbn-release-notes/src/lib/version.ts rename to packages/kbn-docs-utils/src/release_notes/lib/version.ts diff --git a/packages/kbn-release-notes/src/release_notes_config.ts b/packages/kbn-docs-utils/src/release_notes/release_notes_config.ts similarity index 100% rename from packages/kbn-release-notes/src/release_notes_config.ts rename to packages/kbn-docs-utils/src/release_notes/release_notes_config.ts diff --git a/packages/kbn-release-notes/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json similarity index 81% rename from packages/kbn-release-notes/tsconfig.json rename to packages/kbn-docs-utils/tsconfig.json index 02209a29e58171..3c683f487b9f24 100644 --- a/packages/kbn-release-notes/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -8,5 +8,8 @@ }, "include": [ "src/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*" ] } diff --git a/packages/kbn-logging/src/appenders.ts b/packages/kbn-logging/src/appenders.ts index 1b128c0f292018..48422db34b3365 100644 --- a/packages/kbn-logging/src/appenders.ts +++ b/packages/kbn-logging/src/appenders.ts @@ -15,6 +15,24 @@ import { LogRecord } from './log_record'; */ export interface Appender { append(record: LogRecord): void; + /** + * Appenders can be "attached" to one another so that they are able to act + * as a sort of middleware by calling `append` on a different appender. + * + * As appenders cannot be attached to each other until they are configured, + * the `addAppender` method can be used to pass in a newly configured appender + * to attach. + */ + addAppender?(appenderRef: string, appender: Appender): void; + /** + * For appenders which implement `addAppender`, they should declare a list of + * `appenderRefs`, which specify the names of the appenders that their configuration + * depends on. + * + * Note that these are the appender key names that the user specifies in their + * config, _not_ the names of the appender types themselves. + */ + appenderRefs?: string[]; } /** diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 51e8a617e24469..f9ddbcac1e09d8 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(519); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(518); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildBazelProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); @@ -108,7 +108,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(251); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(518); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(517); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -59481,7 +59481,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(365); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(518); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(517); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59638,9 +59638,9 @@ class Kibana { "use strict"; const minimatch = __webpack_require__(150); -const arrayUnion = __webpack_require__(515); -const arrayDiffer = __webpack_require__(516); -const arrify = __webpack_require__(517); +const arrayUnion = __webpack_require__(145); +const arrayDiffer = __webpack_require__(515); +const arrify = __webpack_require__(516); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -59670,18 +59670,6 @@ module.exports = (list, patterns, options = {}) => { "use strict"; -module.exports = (...arguments_) => { - return [...new Set([].concat(...arguments_))]; -}; - - -/***/ }), -/* 516 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - const arrayDiffer = (array, ...values) => { const rest = new Set([].concat(...values)); return array.filter(element => !rest.has(element)); @@ -59691,7 +59679,7 @@ module.exports = arrayDiffer; /***/ }), -/* 517 */ +/* 516 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59721,7 +59709,7 @@ module.exports = arrify; /***/ }), -/* 518 */ +/* 517 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59780,12 +59768,12 @@ function getProjectPaths({ } /***/ }), -/* 519 */ +/* 518 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(520); +/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(519); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); /* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(745); @@ -59802,15 +59790,15 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 520 */ +/* 519 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(521); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(520); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(737); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(736); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -59910,7 +59898,7 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { } /***/ }), -/* 521 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59918,14 +59906,14 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(156); const path = __webpack_require__(4); const os = __webpack_require__(121); -const pMap = __webpack_require__(522); -const arrify = __webpack_require__(517); -const globby = __webpack_require__(525); -const hasGlob = __webpack_require__(721); -const cpFile = __webpack_require__(723); -const junk = __webpack_require__(733); -const pFilter = __webpack_require__(734); -const CpyError = __webpack_require__(736); +const pMap = __webpack_require__(521); +const arrify = __webpack_require__(516); +const globby = __webpack_require__(524); +const hasGlob = __webpack_require__(720); +const cpFile = __webpack_require__(722); +const junk = __webpack_require__(732); +const pFilter = __webpack_require__(733); +const CpyError = __webpack_require__(735); const defaultOptions = { ignoreJunk: true @@ -60076,12 +60064,12 @@ module.exports = (source, destination, { /***/ }), -/* 522 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(523); +const AggregateError = __webpack_require__(522); module.exports = async ( iterable, @@ -60164,12 +60152,12 @@ module.exports = async ( /***/ }), -/* 523 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(524); +const indentString = __webpack_require__(523); const cleanStack = __webpack_require__(244); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -60218,7 +60206,7 @@ module.exports = AggregateError; /***/ }), -/* 524 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60260,17 +60248,17 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 525 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(526); +const arrayUnion = __webpack_require__(525); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(528); -const dirGlob = __webpack_require__(714); -const gitignore = __webpack_require__(717); +const fastGlob = __webpack_require__(527); +const dirGlob = __webpack_require__(713); +const gitignore = __webpack_require__(716); const DEFAULT_FILTER = () => false; @@ -60415,12 +60403,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 526 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(527); +var arrayUniq = __webpack_require__(526); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -60428,7 +60416,7 @@ module.exports = function () { /***/ }), -/* 527 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60497,10 +60485,10 @@ if ('Set' in global) { /***/ }), -/* 528 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(529); +const pkg = __webpack_require__(528); module.exports = pkg.async; module.exports.default = pkg.async; @@ -60513,19 +60501,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 529 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(530); -var taskManager = __webpack_require__(531); -var reader_async_1 = __webpack_require__(685); -var reader_stream_1 = __webpack_require__(709); -var reader_sync_1 = __webpack_require__(710); -var arrayUtils = __webpack_require__(712); -var streamUtils = __webpack_require__(713); +var optionsManager = __webpack_require__(529); +var taskManager = __webpack_require__(530); +var reader_async_1 = __webpack_require__(684); +var reader_stream_1 = __webpack_require__(708); +var reader_sync_1 = __webpack_require__(709); +var arrayUtils = __webpack_require__(711); +var streamUtils = __webpack_require__(712); /** * Synchronous API. */ @@ -60591,7 +60579,7 @@ function isString(source) { /***/ }), -/* 530 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60629,13 +60617,13 @@ exports.prepare = prepare; /***/ }), -/* 531 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(532); +var patternUtils = __webpack_require__(531); /** * Generate tasks based on parent directory of each pattern. */ @@ -60726,16 +60714,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 532 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(533); +var globParent = __webpack_require__(532); var isGlob = __webpack_require__(172); -var micromatch = __webpack_require__(536); +var micromatch = __webpack_require__(535); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -60881,15 +60869,15 @@ exports.matchAny = matchAny; /***/ }), -/* 533 */ +/* 532 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(534); -var pathDirname = __webpack_require__(535); +var isglob = __webpack_require__(533); +var pathDirname = __webpack_require__(534); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -60912,7 +60900,7 @@ module.exports = function globParent(str) { /***/ }), -/* 534 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -60943,7 +60931,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 535 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61093,7 +61081,7 @@ module.exports.win32 = win32; /***/ }), -/* 536 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61104,18 +61092,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(112); -var braces = __webpack_require__(537); -var toRegex = __webpack_require__(538); -var extend = __webpack_require__(651); +var braces = __webpack_require__(536); +var toRegex = __webpack_require__(537); +var extend = __webpack_require__(650); /** * Local dependencies */ -var compilers = __webpack_require__(653); -var parsers = __webpack_require__(680); -var cache = __webpack_require__(681); -var utils = __webpack_require__(682); +var compilers = __webpack_require__(652); +var parsers = __webpack_require__(679); +var cache = __webpack_require__(680); +var utils = __webpack_require__(681); var MAX_LENGTH = 1024 * 64; /** @@ -61977,7 +61965,7 @@ module.exports = micromatch; /***/ }), -/* 537 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61987,18 +61975,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(538); -var unique = __webpack_require__(560); -var extend = __webpack_require__(561); +var toRegex = __webpack_require__(537); +var unique = __webpack_require__(559); +var extend = __webpack_require__(560); /** * Local dependencies */ -var compilers = __webpack_require__(563); -var parsers = __webpack_require__(576); -var Braces = __webpack_require__(580); -var utils = __webpack_require__(564); +var compilers = __webpack_require__(562); +var parsers = __webpack_require__(575); +var Braces = __webpack_require__(579); +var utils = __webpack_require__(563); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -62302,16 +62290,16 @@ module.exports = braces; /***/ }), -/* 538 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(539); -var define = __webpack_require__(545); -var extend = __webpack_require__(553); -var not = __webpack_require__(557); +var safe = __webpack_require__(538); +var define = __webpack_require__(544); +var extend = __webpack_require__(552); +var not = __webpack_require__(556); var MAX_LENGTH = 1024 * 64; /** @@ -62464,10 +62452,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 539 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(540); +var parse = __webpack_require__(539); var types = parse.types; module.exports = function (re, opts) { @@ -62513,13 +62501,13 @@ function isRegExp (x) { /***/ }), -/* 540 */ +/* 539 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(541); -var types = __webpack_require__(542); -var sets = __webpack_require__(543); -var positions = __webpack_require__(544); +var util = __webpack_require__(540); +var types = __webpack_require__(541); +var sets = __webpack_require__(542); +var positions = __webpack_require__(543); module.exports = function(regexpStr) { @@ -62801,11 +62789,11 @@ module.exports.types = types; /***/ }), -/* 541 */ +/* 540 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(542); -var sets = __webpack_require__(543); +var types = __webpack_require__(541); +var sets = __webpack_require__(542); // All of these are private and only used by randexp. @@ -62918,7 +62906,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 542 */ +/* 541 */ /***/ (function(module, exports) { module.exports = { @@ -62934,10 +62922,10 @@ module.exports = { /***/ }), -/* 543 */ +/* 542 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(542); +var types = __webpack_require__(541); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -63022,10 +63010,10 @@ exports.anyChar = function() { /***/ }), -/* 544 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(542); +var types = __webpack_require__(541); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -63045,7 +63033,7 @@ exports.end = function() { /***/ }), -/* 545 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63058,8 +63046,8 @@ exports.end = function() { -var isobject = __webpack_require__(546); -var isDescriptor = __webpack_require__(547); +var isobject = __webpack_require__(545); +var isDescriptor = __webpack_require__(546); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -63090,7 +63078,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 546 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63109,7 +63097,7 @@ module.exports = function isObject(val) { /***/ }), -/* 547 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63122,9 +63110,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(548); -var isAccessor = __webpack_require__(549); -var isData = __webpack_require__(551); +var typeOf = __webpack_require__(547); +var isAccessor = __webpack_require__(548); +var isData = __webpack_require__(550); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -63138,7 +63126,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 548 */ +/* 547 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63273,7 +63261,7 @@ function isBuffer(val) { /***/ }), -/* 549 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63286,7 +63274,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(550); +var typeOf = __webpack_require__(549); // accessor descriptor properties var accessor = { @@ -63349,7 +63337,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 550 */ +/* 549 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63484,7 +63472,7 @@ function isBuffer(val) { /***/ }), -/* 551 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63497,7 +63485,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(552); +var typeOf = __webpack_require__(551); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -63540,7 +63528,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 552 */ +/* 551 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63675,14 +63663,14 @@ function isBuffer(val) { /***/ }), -/* 553 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(554); -var assignSymbols = __webpack_require__(556); +var isExtendable = __webpack_require__(553); +var assignSymbols = __webpack_require__(555); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63742,7 +63730,7 @@ function isEnum(obj, key) { /***/ }), -/* 554 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63755,7 +63743,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(555); +var isPlainObject = __webpack_require__(554); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63763,7 +63751,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 555 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63776,7 +63764,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(546); +var isObject = __webpack_require__(545); function isObjectObject(o) { return isObject(o) === true @@ -63807,7 +63795,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 556 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63854,14 +63842,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 557 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(558); -var safe = __webpack_require__(539); +var extend = __webpack_require__(557); +var safe = __webpack_require__(538); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -63933,14 +63921,14 @@ module.exports = toRegex; /***/ }), -/* 558 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(559); -var assignSymbols = __webpack_require__(556); +var isExtendable = __webpack_require__(558); +var assignSymbols = __webpack_require__(555); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -64000,7 +63988,7 @@ function isEnum(obj, key) { /***/ }), -/* 559 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64013,7 +64001,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(555); +var isPlainObject = __webpack_require__(554); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -64021,7 +64009,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 560 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64071,13 +64059,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 561 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(562); +var isObject = __webpack_require__(561); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -64111,7 +64099,7 @@ function hasOwn(obj, key) { /***/ }), -/* 562 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64131,13 +64119,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 563 */ +/* 562 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(564); +var utils = __webpack_require__(563); module.exports = function(braces, options) { braces.compiler @@ -64420,25 +64408,25 @@ function hasQueue(node) { /***/ }), -/* 564 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(565); +var splitString = __webpack_require__(564); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(561); -utils.flatten = __webpack_require__(568); -utils.isObject = __webpack_require__(546); -utils.fillRange = __webpack_require__(569); -utils.repeat = __webpack_require__(575); -utils.unique = __webpack_require__(560); +utils.extend = __webpack_require__(560); +utils.flatten = __webpack_require__(567); +utils.isObject = __webpack_require__(545); +utils.fillRange = __webpack_require__(568); +utils.repeat = __webpack_require__(574); +utils.unique = __webpack_require__(559); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -64770,7 +64758,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 565 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64783,7 +64771,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(566); +var extend = __webpack_require__(565); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -64948,14 +64936,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 566 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(567); -var assignSymbols = __webpack_require__(556); +var isExtendable = __webpack_require__(566); +var assignSymbols = __webpack_require__(555); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -65015,7 +65003,7 @@ function isEnum(obj, key) { /***/ }), -/* 567 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65028,7 +65016,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(555); +var isPlainObject = __webpack_require__(554); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -65036,7 +65024,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 568 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65065,7 +65053,7 @@ function flat(arr, res) { /***/ }), -/* 569 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65079,10 +65067,10 @@ function flat(arr, res) { var util = __webpack_require__(112); -var isNumber = __webpack_require__(570); -var extend = __webpack_require__(561); -var repeat = __webpack_require__(573); -var toRegex = __webpack_require__(574); +var isNumber = __webpack_require__(569); +var extend = __webpack_require__(560); +var repeat = __webpack_require__(572); +var toRegex = __webpack_require__(573); /** * Return a range of numbers or letters. @@ -65280,7 +65268,7 @@ module.exports = fillRange; /***/ }), -/* 570 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65293,7 +65281,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(571); +var typeOf = __webpack_require__(570); module.exports = function isNumber(num) { var type = typeOf(num); @@ -65309,10 +65297,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 571 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(572); +var isBuffer = __webpack_require__(571); var toString = Object.prototype.toString; /** @@ -65431,7 +65419,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 572 */ +/* 571 */ /***/ (function(module, exports) { /*! @@ -65458,7 +65446,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 573 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65535,7 +65523,7 @@ function repeat(str, num) { /***/ }), -/* 574 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65548,8 +65536,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(573); -var isNumber = __webpack_require__(570); +var repeat = __webpack_require__(572); +var isNumber = __webpack_require__(569); var cache = {}; function toRegexRange(min, max, options) { @@ -65836,7 +65824,7 @@ module.exports = toRegexRange; /***/ }), -/* 575 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65861,14 +65849,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 576 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(577); -var utils = __webpack_require__(564); +var Node = __webpack_require__(576); +var utils = __webpack_require__(563); /** * Braces parsers @@ -66228,15 +66216,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 577 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(546); -var define = __webpack_require__(578); -var utils = __webpack_require__(579); +var isObject = __webpack_require__(545); +var define = __webpack_require__(577); +var utils = __webpack_require__(578); var ownNames; /** @@ -66727,7 +66715,7 @@ exports = module.exports = Node; /***/ }), -/* 578 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66740,7 +66728,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(547); +var isDescriptor = __webpack_require__(546); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -66765,13 +66753,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 579 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(571); +var typeOf = __webpack_require__(570); var utils = module.exports; /** @@ -67791,17 +67779,17 @@ function assert(val, message) { /***/ }), -/* 580 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(561); -var Snapdragon = __webpack_require__(581); -var compilers = __webpack_require__(563); -var parsers = __webpack_require__(576); -var utils = __webpack_require__(564); +var extend = __webpack_require__(560); +var Snapdragon = __webpack_require__(580); +var compilers = __webpack_require__(562); +var parsers = __webpack_require__(575); +var utils = __webpack_require__(563); /** * Customize Snapdragon parser and renderer @@ -67902,17 +67890,17 @@ module.exports = Braces; /***/ }), -/* 581 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(582); -var define = __webpack_require__(609); -var Compiler = __webpack_require__(619); -var Parser = __webpack_require__(648); -var utils = __webpack_require__(628); +var Base = __webpack_require__(581); +var define = __webpack_require__(608); +var Compiler = __webpack_require__(618); +var Parser = __webpack_require__(647); +var utils = __webpack_require__(627); var regexCache = {}; var cache = {}; @@ -68083,20 +68071,20 @@ module.exports.Parser = Parser; /***/ }), -/* 582 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(583); -var CacheBase = __webpack_require__(584); -var Emitter = __webpack_require__(585); -var isObject = __webpack_require__(546); -var merge = __webpack_require__(603); -var pascal = __webpack_require__(606); -var cu = __webpack_require__(607); +var define = __webpack_require__(582); +var CacheBase = __webpack_require__(583); +var Emitter = __webpack_require__(584); +var isObject = __webpack_require__(545); +var merge = __webpack_require__(602); +var pascal = __webpack_require__(605); +var cu = __webpack_require__(606); /** * Optionally define a custom `cache` namespace to use. @@ -68525,7 +68513,7 @@ module.exports.namespace = namespace; /***/ }), -/* 583 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68538,7 +68526,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(547); +var isDescriptor = __webpack_require__(546); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -68563,21 +68551,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 584 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(546); -var Emitter = __webpack_require__(585); -var visit = __webpack_require__(586); -var toPath = __webpack_require__(589); -var union = __webpack_require__(590); -var del = __webpack_require__(594); -var get = __webpack_require__(592); -var has = __webpack_require__(599); -var set = __webpack_require__(602); +var isObject = __webpack_require__(545); +var Emitter = __webpack_require__(584); +var visit = __webpack_require__(585); +var toPath = __webpack_require__(588); +var union = __webpack_require__(589); +var del = __webpack_require__(593); +var get = __webpack_require__(591); +var has = __webpack_require__(598); +var set = __webpack_require__(601); /** * Create a `Cache` constructor that when instantiated will @@ -68831,7 +68819,7 @@ module.exports.namespace = namespace; /***/ }), -/* 585 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { @@ -69000,7 +68988,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 586 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69013,8 +69001,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(587); -var mapVisit = __webpack_require__(588); +var visit = __webpack_require__(586); +var mapVisit = __webpack_require__(587); module.exports = function(collection, method, val) { var result; @@ -69037,7 +69025,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 587 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69050,7 +69038,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(546); +var isObject = __webpack_require__(545); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -69077,14 +69065,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 588 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(587); +var visit = __webpack_require__(586); /** * Map `visit` over an array of objects. @@ -69121,7 +69109,7 @@ function isObject(val) { /***/ }), -/* 589 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69134,7 +69122,7 @@ function isObject(val) { -var typeOf = __webpack_require__(571); +var typeOf = __webpack_require__(570); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -69161,16 +69149,16 @@ function filter(arr) { /***/ }), -/* 590 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(562); -var union = __webpack_require__(591); -var get = __webpack_require__(592); -var set = __webpack_require__(593); +var isObject = __webpack_require__(561); +var union = __webpack_require__(590); +var get = __webpack_require__(591); +var set = __webpack_require__(592); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -69198,7 +69186,7 @@ function arrayify(val) { /***/ }), -/* 591 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69234,7 +69222,7 @@ module.exports = function union(init) { /***/ }), -/* 592 */ +/* 591 */ /***/ (function(module, exports) { /*! @@ -69290,7 +69278,7 @@ function toString(val) { /***/ }), -/* 593 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69303,10 +69291,10 @@ function toString(val) { -var split = __webpack_require__(565); -var extend = __webpack_require__(561); -var isPlainObject = __webpack_require__(555); -var isObject = __webpack_require__(562); +var split = __webpack_require__(564); +var extend = __webpack_require__(560); +var isPlainObject = __webpack_require__(554); +var isObject = __webpack_require__(561); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69352,7 +69340,7 @@ function isValidKey(key) { /***/ }), -/* 594 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69365,8 +69353,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(546); -var has = __webpack_require__(595); +var isObject = __webpack_require__(545); +var has = __webpack_require__(594); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -69391,7 +69379,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 595 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69404,9 +69392,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(596); -var hasValues = __webpack_require__(598); -var get = __webpack_require__(592); +var isObject = __webpack_require__(595); +var hasValues = __webpack_require__(597); +var get = __webpack_require__(591); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -69417,7 +69405,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 596 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69430,7 +69418,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(597); +var isArray = __webpack_require__(596); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -69438,7 +69426,7 @@ module.exports = function isObject(val) { /***/ }), -/* 597 */ +/* 596 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -69449,7 +69437,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 598 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69492,7 +69480,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 599 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69505,9 +69493,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(546); -var hasValues = __webpack_require__(600); -var get = __webpack_require__(592); +var isObject = __webpack_require__(545); +var hasValues = __webpack_require__(599); +var get = __webpack_require__(591); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -69515,7 +69503,7 @@ module.exports = function(val, prop) { /***/ }), -/* 600 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69528,8 +69516,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(601); -var isNumber = __webpack_require__(570); +var typeOf = __webpack_require__(600); +var isNumber = __webpack_require__(569); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -69582,10 +69570,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 601 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(572); +var isBuffer = __webpack_require__(571); var toString = Object.prototype.toString; /** @@ -69707,7 +69695,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 602 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69720,10 +69708,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(565); -var extend = __webpack_require__(561); -var isPlainObject = __webpack_require__(555); -var isObject = __webpack_require__(562); +var split = __webpack_require__(564); +var extend = __webpack_require__(560); +var isPlainObject = __webpack_require__(554); +var isObject = __webpack_require__(561); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69769,14 +69757,14 @@ function isValidKey(key) { /***/ }), -/* 603 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(604); -var forIn = __webpack_require__(605); +var isExtendable = __webpack_require__(603); +var forIn = __webpack_require__(604); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69840,7 +69828,7 @@ module.exports = mixinDeep; /***/ }), -/* 604 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69853,7 +69841,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(555); +var isPlainObject = __webpack_require__(554); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -69861,7 +69849,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 605 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69884,7 +69872,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 606 */ +/* 605 */ /***/ (function(module, exports) { /*! @@ -69911,14 +69899,14 @@ module.exports = pascalcase; /***/ }), -/* 607 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(608); +var utils = __webpack_require__(607); /** * Expose class utils @@ -70283,7 +70271,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 608 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70297,10 +70285,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(591); -utils.define = __webpack_require__(609); -utils.isObj = __webpack_require__(546); -utils.staticExtend = __webpack_require__(616); +utils.union = __webpack_require__(590); +utils.define = __webpack_require__(608); +utils.isObj = __webpack_require__(545); +utils.staticExtend = __webpack_require__(615); /** @@ -70311,7 +70299,7 @@ module.exports = utils; /***/ }), -/* 609 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70324,7 +70312,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(610); +var isDescriptor = __webpack_require__(609); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70349,7 +70337,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 610 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70362,9 +70350,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(611); -var isAccessor = __webpack_require__(612); -var isData = __webpack_require__(614); +var typeOf = __webpack_require__(610); +var isAccessor = __webpack_require__(611); +var isData = __webpack_require__(613); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -70378,7 +70366,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 611 */ +/* 610 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -70531,7 +70519,7 @@ function isBuffer(val) { /***/ }), -/* 612 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70544,7 +70532,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(613); +var typeOf = __webpack_require__(612); // accessor descriptor properties var accessor = { @@ -70607,10 +70595,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 613 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(572); +var isBuffer = __webpack_require__(571); var toString = Object.prototype.toString; /** @@ -70729,7 +70717,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 614 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70742,7 +70730,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(615); +var typeOf = __webpack_require__(614); // data descriptor properties var data = { @@ -70791,10 +70779,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 615 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(572); +var isBuffer = __webpack_require__(571); var toString = Object.prototype.toString; /** @@ -70913,7 +70901,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 616 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70926,8 +70914,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(617); -var define = __webpack_require__(609); +var copy = __webpack_require__(616); +var define = __webpack_require__(608); var util = __webpack_require__(112); /** @@ -71010,15 +70998,15 @@ module.exports = extend; /***/ }), -/* 617 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(571); -var copyDescriptor = __webpack_require__(618); -var define = __webpack_require__(609); +var typeOf = __webpack_require__(570); +var copyDescriptor = __webpack_require__(617); +var define = __webpack_require__(608); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -71191,7 +71179,7 @@ module.exports.has = has; /***/ }), -/* 618 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71279,16 +71267,16 @@ function isObject(val) { /***/ }), -/* 619 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(620); -var define = __webpack_require__(609); -var debug = __webpack_require__(622)('snapdragon:compiler'); -var utils = __webpack_require__(628); +var use = __webpack_require__(619); +var define = __webpack_require__(608); +var debug = __webpack_require__(621)('snapdragon:compiler'); +var utils = __webpack_require__(627); /** * Create a new `Compiler` with the given `options`. @@ -71442,7 +71430,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(647); + var sourcemaps = __webpack_require__(646); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -71463,7 +71451,7 @@ module.exports = Compiler; /***/ }), -/* 620 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71476,7 +71464,7 @@ module.exports = Compiler; -var utils = __webpack_require__(621); +var utils = __webpack_require__(620); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -71591,7 +71579,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 621 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71605,8 +71593,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(609); -utils.isObject = __webpack_require__(546); +utils.define = __webpack_require__(608); +utils.isObject = __webpack_require__(545); utils.isString = function(val) { @@ -71621,7 +71609,7 @@ module.exports = utils; /***/ }), -/* 622 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71630,14 +71618,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(623); + module.exports = __webpack_require__(622); } else { - module.exports = __webpack_require__(626); + module.exports = __webpack_require__(625); } /***/ }), -/* 623 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71646,7 +71634,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(624); +exports = module.exports = __webpack_require__(623); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71828,7 +71816,7 @@ function localstorage() { /***/ }), -/* 624 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { @@ -71844,7 +71832,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(625); +exports.humanize = __webpack_require__(624); /** * The currently active debug mode names, and names to skip. @@ -72036,7 +72024,7 @@ function coerce(val) { /***/ }), -/* 625 */ +/* 624 */ /***/ (function(module, exports) { /** @@ -72194,7 +72182,7 @@ function plural(ms, n, name) { /***/ }), -/* 626 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -72210,7 +72198,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(624); +exports = module.exports = __webpack_require__(623); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -72389,7 +72377,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(627); + var net = __webpack_require__(626); stream = new net.Socket({ fd: fd, readable: false, @@ -72448,13 +72436,13 @@ exports.enable(load()); /***/ }), -/* 627 */ +/* 626 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 628 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72464,9 +72452,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(561); -exports.SourceMap = __webpack_require__(629); -exports.sourceMapResolve = __webpack_require__(640); +exports.extend = __webpack_require__(560); +exports.SourceMap = __webpack_require__(628); +exports.sourceMapResolve = __webpack_require__(639); /** * Convert backslash in the given string to forward slashes @@ -72509,7 +72497,7 @@ exports.last = function(arr, n) { /***/ }), -/* 629 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72517,13 +72505,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(630).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(636).SourceMapConsumer; -exports.SourceNode = __webpack_require__(639).SourceNode; +exports.SourceMapGenerator = __webpack_require__(629).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(635).SourceMapConsumer; +exports.SourceNode = __webpack_require__(638).SourceNode; /***/ }), -/* 630 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72533,10 +72521,10 @@ exports.SourceNode = __webpack_require__(639).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(631); -var util = __webpack_require__(633); -var ArraySet = __webpack_require__(634).ArraySet; -var MappingList = __webpack_require__(635).MappingList; +var base64VLQ = __webpack_require__(630); +var util = __webpack_require__(632); +var ArraySet = __webpack_require__(633).ArraySet; +var MappingList = __webpack_require__(634).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72945,7 +72933,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 631 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72985,7 +72973,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(632); +var base64 = __webpack_require__(631); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -73091,7 +73079,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 632 */ +/* 631 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73164,7 +73152,7 @@ exports.decode = function (charCode) { /***/ }), -/* 633 */ +/* 632 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73587,7 +73575,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 634 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73597,7 +73585,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(633); +var util = __webpack_require__(632); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73714,7 +73702,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 635 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73724,7 +73712,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(633); +var util = __webpack_require__(632); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73799,7 +73787,7 @@ exports.MappingList = MappingList; /***/ }), -/* 636 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73809,11 +73797,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(633); -var binarySearch = __webpack_require__(637); -var ArraySet = __webpack_require__(634).ArraySet; -var base64VLQ = __webpack_require__(631); -var quickSort = __webpack_require__(638).quickSort; +var util = __webpack_require__(632); +var binarySearch = __webpack_require__(636); +var ArraySet = __webpack_require__(633).ArraySet; +var base64VLQ = __webpack_require__(630); +var quickSort = __webpack_require__(637).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74887,7 +74875,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 637 */ +/* 636 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75004,7 +74992,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 638 */ +/* 637 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75124,7 +75112,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 639 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75134,8 +75122,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(630).SourceMapGenerator; -var util = __webpack_require__(633); +var SourceMapGenerator = __webpack_require__(629).SourceMapGenerator; +var util = __webpack_require__(632); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75543,17 +75531,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 640 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(641) -var resolveUrl = __webpack_require__(642) -var decodeUriComponent = __webpack_require__(643) -var urix = __webpack_require__(645) -var atob = __webpack_require__(646) +var sourceMappingURL = __webpack_require__(640) +var resolveUrl = __webpack_require__(641) +var decodeUriComponent = __webpack_require__(642) +var urix = __webpack_require__(644) +var atob = __webpack_require__(645) @@ -75851,7 +75839,7 @@ module.exports = { /***/ }), -/* 641 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75914,7 +75902,7 @@ void (function(root, factory) { /***/ }), -/* 642 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75932,13 +75920,13 @@ module.exports = resolveUrl /***/ }), -/* 643 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(644) +var decodeUriComponent = __webpack_require__(643) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75949,7 +75937,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 644 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76050,7 +76038,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 645 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -76073,7 +76061,7 @@ module.exports = urix /***/ }), -/* 646 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76087,7 +76075,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 647 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76095,8 +76083,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(609); -var utils = __webpack_require__(628); +var define = __webpack_require__(608); +var utils = __webpack_require__(627); /** * Expose `mixin()`. @@ -76239,19 +76227,19 @@ exports.comment = function(node) { /***/ }), -/* 648 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(620); +var use = __webpack_require__(619); var util = __webpack_require__(112); -var Cache = __webpack_require__(649); -var define = __webpack_require__(609); -var debug = __webpack_require__(622)('snapdragon:parser'); -var Position = __webpack_require__(650); -var utils = __webpack_require__(628); +var Cache = __webpack_require__(648); +var define = __webpack_require__(608); +var debug = __webpack_require__(621)('snapdragon:parser'); +var Position = __webpack_require__(649); +var utils = __webpack_require__(627); /** * Create a new `Parser` with the given `input` and `options`. @@ -76779,7 +76767,7 @@ module.exports = Parser; /***/ }), -/* 649 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76886,13 +76874,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 650 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(609); +var define = __webpack_require__(608); /** * Store position for a node @@ -76907,14 +76895,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 651 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(652); -var assignSymbols = __webpack_require__(556); +var isExtendable = __webpack_require__(651); +var assignSymbols = __webpack_require__(555); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -76974,7 +76962,7 @@ function isEnum(obj, key) { /***/ }), -/* 652 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76987,7 +76975,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(555); +var isPlainObject = __webpack_require__(554); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -76995,14 +76983,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 653 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(654); -var extglob = __webpack_require__(669); +var nanomatch = __webpack_require__(653); +var extglob = __webpack_require__(668); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -77079,7 +77067,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 654 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77090,17 +77078,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(112); -var toRegex = __webpack_require__(538); -var extend = __webpack_require__(655); +var toRegex = __webpack_require__(537); +var extend = __webpack_require__(654); /** * Local dependencies */ -var compilers = __webpack_require__(657); -var parsers = __webpack_require__(658); -var cache = __webpack_require__(661); -var utils = __webpack_require__(663); +var compilers = __webpack_require__(656); +var parsers = __webpack_require__(657); +var cache = __webpack_require__(660); +var utils = __webpack_require__(662); var MAX_LENGTH = 1024 * 64; /** @@ -77924,14 +77912,14 @@ module.exports = nanomatch; /***/ }), -/* 655 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(656); -var assignSymbols = __webpack_require__(556); +var isExtendable = __webpack_require__(655); +var assignSymbols = __webpack_require__(555); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -77991,7 +77979,7 @@ function isEnum(obj, key) { /***/ }), -/* 656 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78004,7 +77992,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(555); +var isPlainObject = __webpack_require__(554); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -78012,7 +78000,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 657 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78358,15 +78346,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 658 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(557); -var toRegex = __webpack_require__(538); -var isOdd = __webpack_require__(659); +var regexNot = __webpack_require__(556); +var toRegex = __webpack_require__(537); +var isOdd = __webpack_require__(658); /** * Characters to use in negation regex (we want to "not" match @@ -78752,7 +78740,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 659 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78765,7 +78753,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(660); +var isNumber = __webpack_require__(659); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78779,7 +78767,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 660 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78807,14 +78795,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 661 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(662))(); +module.exports = new (__webpack_require__(661))(); /***/ }), -/* 662 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78827,7 +78815,7 @@ module.exports = new (__webpack_require__(662))(); -var MapCache = __webpack_require__(649); +var MapCache = __webpack_require__(648); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78949,7 +78937,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 663 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78962,14 +78950,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(664)(); -var Snapdragon = __webpack_require__(581); -utils.define = __webpack_require__(665); -utils.diff = __webpack_require__(666); -utils.extend = __webpack_require__(655); -utils.pick = __webpack_require__(667); -utils.typeOf = __webpack_require__(668); -utils.unique = __webpack_require__(560); +var isWindows = __webpack_require__(663)(); +var Snapdragon = __webpack_require__(580); +utils.define = __webpack_require__(664); +utils.diff = __webpack_require__(665); +utils.extend = __webpack_require__(654); +utils.pick = __webpack_require__(666); +utils.typeOf = __webpack_require__(667); +utils.unique = __webpack_require__(559); /** * Returns true if the given value is effectively an empty string @@ -79335,7 +79323,7 @@ utils.unixify = function(options) { /***/ }), -/* 664 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -79363,7 +79351,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 665 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79376,8 +79364,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(546); -var isDescriptor = __webpack_require__(547); +var isobject = __webpack_require__(545); +var isDescriptor = __webpack_require__(546); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -79408,7 +79396,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 666 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79462,7 +79450,7 @@ function diffArray(one, two) { /***/ }), -/* 667 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79475,7 +79463,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(546); +var isObject = __webpack_require__(545); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -79504,7 +79492,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 668 */ +/* 667 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79639,7 +79627,7 @@ function isBuffer(val) { /***/ }), -/* 669 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79649,18 +79637,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(561); -var unique = __webpack_require__(560); -var toRegex = __webpack_require__(538); +var extend = __webpack_require__(560); +var unique = __webpack_require__(559); +var toRegex = __webpack_require__(537); /** * Local dependencies */ -var compilers = __webpack_require__(670); -var parsers = __webpack_require__(676); -var Extglob = __webpack_require__(679); -var utils = __webpack_require__(678); +var compilers = __webpack_require__(669); +var parsers = __webpack_require__(675); +var Extglob = __webpack_require__(678); +var utils = __webpack_require__(677); var MAX_LENGTH = 1024 * 64; /** @@ -79977,13 +79965,13 @@ module.exports = extglob; /***/ }), -/* 670 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(671); +var brackets = __webpack_require__(670); /** * Extglob compilers @@ -80153,7 +80141,7 @@ module.exports = function(extglob) { /***/ }), -/* 671 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80163,17 +80151,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(672); -var parsers = __webpack_require__(674); +var compilers = __webpack_require__(671); +var parsers = __webpack_require__(673); /** * Module dependencies */ -var debug = __webpack_require__(622)('expand-brackets'); -var extend = __webpack_require__(561); -var Snapdragon = __webpack_require__(581); -var toRegex = __webpack_require__(538); +var debug = __webpack_require__(621)('expand-brackets'); +var extend = __webpack_require__(560); +var Snapdragon = __webpack_require__(580); +var toRegex = __webpack_require__(537); /** * Parses the given POSIX character class `pattern` and returns a @@ -80371,13 +80359,13 @@ module.exports = brackets; /***/ }), -/* 672 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(673); +var posix = __webpack_require__(672); module.exports = function(brackets) { brackets.compiler @@ -80465,7 +80453,7 @@ module.exports = function(brackets) { /***/ }), -/* 673 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80494,14 +80482,14 @@ module.exports = { /***/ }), -/* 674 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(675); -var define = __webpack_require__(609); +var utils = __webpack_require__(674); +var define = __webpack_require__(608); /** * Text regex @@ -80720,14 +80708,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 675 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(538); -var regexNot = __webpack_require__(557); +var toRegex = __webpack_require__(537); +var regexNot = __webpack_require__(556); var cached; /** @@ -80761,15 +80749,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 676 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(671); -var define = __webpack_require__(677); -var utils = __webpack_require__(678); +var brackets = __webpack_require__(670); +var define = __webpack_require__(676); +var utils = __webpack_require__(677); /** * Characters to use in text regex (we want to "not" match @@ -80924,7 +80912,7 @@ module.exports = parsers; /***/ }), -/* 677 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80937,7 +80925,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(547); +var isDescriptor = __webpack_require__(546); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -80962,14 +80950,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 678 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(557); -var Cache = __webpack_require__(662); +var regex = __webpack_require__(556); +var Cache = __webpack_require__(661); /** * Utils @@ -81038,7 +81026,7 @@ utils.createRegex = function(str) { /***/ }), -/* 679 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81048,16 +81036,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(581); -var define = __webpack_require__(677); -var extend = __webpack_require__(561); +var Snapdragon = __webpack_require__(580); +var define = __webpack_require__(676); +var extend = __webpack_require__(560); /** * Local dependencies */ -var compilers = __webpack_require__(670); -var parsers = __webpack_require__(676); +var compilers = __webpack_require__(669); +var parsers = __webpack_require__(675); /** * Customize Snapdragon parser and renderer @@ -81123,16 +81111,16 @@ module.exports = Extglob; /***/ }), -/* 680 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(669); -var nanomatch = __webpack_require__(654); -var regexNot = __webpack_require__(557); -var toRegex = __webpack_require__(538); +var extglob = __webpack_require__(668); +var nanomatch = __webpack_require__(653); +var regexNot = __webpack_require__(556); +var toRegex = __webpack_require__(537); var not; /** @@ -81213,14 +81201,14 @@ function textRegex(pattern) { /***/ }), -/* 681 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(662))(); +module.exports = new (__webpack_require__(661))(); /***/ }), -/* 682 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81233,13 +81221,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(581); -utils.define = __webpack_require__(683); -utils.diff = __webpack_require__(666); -utils.extend = __webpack_require__(651); -utils.pick = __webpack_require__(667); -utils.typeOf = __webpack_require__(684); -utils.unique = __webpack_require__(560); +var Snapdragon = __webpack_require__(580); +utils.define = __webpack_require__(682); +utils.diff = __webpack_require__(665); +utils.extend = __webpack_require__(650); +utils.pick = __webpack_require__(666); +utils.typeOf = __webpack_require__(683); +utils.unique = __webpack_require__(559); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -81536,7 +81524,7 @@ utils.unixify = function(options) { /***/ }), -/* 683 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81549,8 +81537,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(546); -var isDescriptor = __webpack_require__(547); +var isobject = __webpack_require__(545); +var isDescriptor = __webpack_require__(546); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -81581,7 +81569,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 684 */ +/* 683 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81716,7 +81704,7 @@ function isBuffer(val) { /***/ }), -/* 685 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81735,9 +81723,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(686); -var reader_1 = __webpack_require__(699); -var fs_stream_1 = __webpack_require__(703); +var readdir = __webpack_require__(685); +var reader_1 = __webpack_require__(698); +var fs_stream_1 = __webpack_require__(702); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81798,15 +81786,15 @@ exports.default = ReaderAsync; /***/ }), -/* 686 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(687); -const readdirAsync = __webpack_require__(695); -const readdirStream = __webpack_require__(698); +const readdirSync = __webpack_require__(686); +const readdirAsync = __webpack_require__(694); +const readdirStream = __webpack_require__(697); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81890,7 +81878,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 687 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81898,11 +81886,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(688); +const DirectoryReader = __webpack_require__(687); let syncFacade = { - fs: __webpack_require__(693), - forEach: __webpack_require__(694), + fs: __webpack_require__(692), + forEach: __webpack_require__(693), sync: true }; @@ -81931,7 +81919,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 688 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81940,9 +81928,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(689); -const stat = __webpack_require__(691); -const call = __webpack_require__(692); +const normalizeOptions = __webpack_require__(688); +const stat = __webpack_require__(690); +const call = __webpack_require__(691); /** * Asynchronously reads the contents of a directory and streams the results @@ -82318,14 +82306,14 @@ module.exports = DirectoryReader; /***/ }), -/* 689 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(690); +const globToRegExp = __webpack_require__(689); module.exports = normalizeOptions; @@ -82502,7 +82490,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 690 */ +/* 689 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82639,13 +82627,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 691 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(692); +const call = __webpack_require__(691); module.exports = stat; @@ -82720,7 +82708,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 692 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82781,14 +82769,14 @@ function callOnce (fn) { /***/ }), -/* 693 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(692); +const call = __webpack_require__(691); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82852,7 +82840,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 694 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82881,7 +82869,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 695 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82889,12 +82877,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(696); -const DirectoryReader = __webpack_require__(688); +const maybe = __webpack_require__(695); +const DirectoryReader = __webpack_require__(687); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(697), + forEach: __webpack_require__(696), async: true }; @@ -82936,7 +82924,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 696 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82963,7 +82951,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 697 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82999,7 +82987,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 698 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83007,11 +82995,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(688); +const DirectoryReader = __webpack_require__(687); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(697), + forEach: __webpack_require__(696), async: true }; @@ -83031,16 +83019,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 699 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(700); -var entry_1 = __webpack_require__(702); -var pathUtil = __webpack_require__(701); +var deep_1 = __webpack_require__(699); +var entry_1 = __webpack_require__(701); +var pathUtil = __webpack_require__(700); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -83106,14 +83094,14 @@ exports.default = Reader; /***/ }), -/* 700 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(701); -var patternUtils = __webpack_require__(532); +var pathUtils = __webpack_require__(700); +var patternUtils = __webpack_require__(531); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -83196,7 +83184,7 @@ exports.default = DeepFilter; /***/ }), -/* 701 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83227,14 +83215,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 702 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(701); -var patternUtils = __webpack_require__(532); +var pathUtils = __webpack_require__(700); +var patternUtils = __webpack_require__(531); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -83319,7 +83307,7 @@ exports.default = EntryFilter; /***/ }), -/* 703 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83339,8 +83327,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(704); -var fs_1 = __webpack_require__(708); +var fsStat = __webpack_require__(703); +var fs_1 = __webpack_require__(707); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -83390,14 +83378,14 @@ exports.default = FileSystemStream; /***/ }), -/* 704 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(705); -const statProvider = __webpack_require__(707); +const optionsManager = __webpack_require__(704); +const statProvider = __webpack_require__(706); /** * Asynchronous API. */ @@ -83428,13 +83416,13 @@ exports.statSync = statSync; /***/ }), -/* 705 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(706); +const fsAdapter = __webpack_require__(705); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83447,7 +83435,7 @@ exports.prepare = prepare; /***/ }), -/* 706 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83470,7 +83458,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 707 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83522,7 +83510,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 708 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83553,7 +83541,7 @@ exports.default = FileSystem; /***/ }), -/* 709 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83573,9 +83561,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(686); -var reader_1 = __webpack_require__(699); -var fs_stream_1 = __webpack_require__(703); +var readdir = __webpack_require__(685); +var reader_1 = __webpack_require__(698); +var fs_stream_1 = __webpack_require__(702); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83643,7 +83631,7 @@ exports.default = ReaderStream; /***/ }), -/* 710 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83662,9 +83650,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(686); -var reader_1 = __webpack_require__(699); -var fs_sync_1 = __webpack_require__(711); +var readdir = __webpack_require__(685); +var reader_1 = __webpack_require__(698); +var fs_sync_1 = __webpack_require__(710); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83724,7 +83712,7 @@ exports.default = ReaderSync; /***/ }), -/* 711 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83743,8 +83731,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(704); -var fs_1 = __webpack_require__(708); +var fsStat = __webpack_require__(703); +var fs_1 = __webpack_require__(707); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83790,7 +83778,7 @@ exports.default = FileSystemSync; /***/ }), -/* 712 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83806,7 +83794,7 @@ exports.flatten = flatten; /***/ }), -/* 713 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83827,13 +83815,13 @@ exports.merge = merge; /***/ }), -/* 714 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(715); +const pathType = __webpack_require__(714); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83899,13 +83887,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 715 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(716); +const pify = __webpack_require__(715); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83948,7 +83936,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 716 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84039,17 +84027,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 717 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(528); -const gitIgnore = __webpack_require__(718); -const pify = __webpack_require__(719); -const slash = __webpack_require__(720); +const fastGlob = __webpack_require__(527); +const gitIgnore = __webpack_require__(717); +const pify = __webpack_require__(718); +const slash = __webpack_require__(719); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -84147,7 +84135,7 @@ module.exports.sync = options => { /***/ }), -/* 718 */ +/* 717 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84616,7 +84604,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 719 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84691,7 +84679,7 @@ module.exports = (input, options) => { /***/ }), -/* 720 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84709,7 +84697,7 @@ module.exports = input => { /***/ }), -/* 721 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84722,7 +84710,7 @@ module.exports = input => { -var isGlob = __webpack_require__(722); +var isGlob = __webpack_require__(721); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84742,7 +84730,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 722 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84773,17 +84761,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 723 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(724); -const CpFileError = __webpack_require__(727); -const fs = __webpack_require__(729); -const ProgressEmitter = __webpack_require__(732); +const pEvent = __webpack_require__(723); +const CpFileError = __webpack_require__(726); +const fs = __webpack_require__(728); +const ProgressEmitter = __webpack_require__(731); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84897,12 +84885,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 724 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(725); +const pTimeout = __webpack_require__(724); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -85193,12 +85181,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 725 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(726); +const pFinally = __webpack_require__(725); class TimeoutError extends Error { constructor(message) { @@ -85244,7 +85232,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 726 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85266,12 +85254,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 727 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(728); +const NestedError = __webpack_require__(727); class CpFileError extends NestedError { constructor(message, nested) { @@ -85285,7 +85273,7 @@ module.exports = CpFileError; /***/ }), -/* 728 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -85341,16 +85329,16 @@ module.exports = NestedError; /***/ }), -/* 729 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(730); -const pEvent = __webpack_require__(724); -const CpFileError = __webpack_require__(727); +const makeDir = __webpack_require__(729); +const pEvent = __webpack_require__(723); +const CpFileError = __webpack_require__(726); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85447,7 +85435,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 730 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85455,7 +85443,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(731); +const semver = __webpack_require__(730); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85610,7 +85598,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 731 */ +/* 730 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -87212,7 +87200,7 @@ function coerce (version, options) { /***/ }), -/* 732 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87253,7 +87241,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 733 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87299,12 +87287,12 @@ exports.default = module.exports; /***/ }), -/* 734 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(735); +const pMap = __webpack_require__(734); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -87321,7 +87309,7 @@ module.exports.default = pFilter; /***/ }), -/* 735 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87400,12 +87388,12 @@ module.exports.default = pMap; /***/ }), -/* 736 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(728); +const NestedError = __webpack_require__(727); class CpyError extends NestedError { constructor(message, nested) { @@ -87419,14 +87407,14 @@ module.exports = CpyError; /***/ }), -/* 737 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const arrayUnion = __webpack_require__(526); +const arrayUnion = __webpack_require__(737); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(528); +const fastGlob = __webpack_require__(527); const dirGlob = __webpack_require__(738); const gitignore = __webpack_require__(742); @@ -87553,6 +87541,19 @@ module.exports.hasMagic = (patterns, opts) => [] module.exports.gitignore = gitignore; +/***/ }), +/* 737 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +var arrayUniq = __webpack_require__(526); + +module.exports = function () { + return arrayUniq([].concat.apply([], arguments)); +}; + + /***/ }), /* 738 */ /***/ (function(module, exports, __webpack_require__) { @@ -87771,7 +87772,7 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(528); +const fastGlob = __webpack_require__(527); const gitIgnore = __webpack_require__(743); const pify = __webpack_require__(741); const slash = __webpack_require__(744); @@ -88324,13 +88325,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return buildNonBazelProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProductionProjects", function() { return getProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProject", function() { return buildProject; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(521); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(520); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(518); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(517); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); diff --git a/scripts/build_api_docs.js b/scripts/build_api_docs.js new file mode 100644 index 00000000000000..3e26a2d87895c1 --- /dev/null +++ b/scripts/build_api_docs.js @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/docs-utils').runBuildApiDocsCli(); diff --git a/scripts/release_notes.js b/scripts/release_notes.js index f22c00f4643b08..7408ce322677cd 100644 --- a/scripts/release_notes.js +++ b/scripts/release_notes.js @@ -7,4 +7,4 @@ */ require('../src/setup_node_env/no_transpilation'); -require('@kbn/release-notes').runReleaseNotesCli(); +require('@kbn/docs-utils').runReleaseNotesCli(); diff --git a/src/core/kibana.json b/src/core/kibana.json new file mode 100644 index 00000000000000..49f838dbc4ee56 --- /dev/null +++ b/src/core/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "core", + "summary": "The core plugin has core functionality", + "version": "kibana", + "serviceFolders": ["http", "saved_objects", "chrome", "application"] + } + \ No newline at end of file diff --git a/src/core/server/http/integration_tests/logging.test.ts b/src/core/server/http/integration_tests/logging.test.ts index fcf2cd2ba3372d..62cb699bc49f6b 100644 --- a/src/core/server/http/integration_tests/logging.test.ts +++ b/src/core/server/http/integration_tests/logging.test.ts @@ -251,7 +251,7 @@ describe('request logging', () => { expect(JSON.parse(meta).http.response.headers.bar).toBe('world'); }); - it('filters sensitive request headers', async () => { + it('filters sensitive request headers by default', async () => { const { http } = await root.setup(); http.createRouter('/').post( @@ -283,7 +283,139 @@ describe('request logging', () => { expect(JSON.parse(meta).http.request.headers.authorization).toBe('[REDACTED]'); }); - it('filters sensitive response headers', async () => { + it('filters sensitive request headers when RewriteAppender is configured', async () => { + root = kbnTestServer.createRoot({ + logging: { + silent: true, + appenders: { + 'test-console': { + type: 'console', + layout: { + type: 'pattern', + pattern: '%level|%logger|%message|%meta', + }, + }, + rewrite: { + type: 'rewrite', + appenders: ['test-console'], + policy: { + type: 'meta', + mode: 'update', + properties: [ + { path: 'http.request.headers.authorization', value: '[REDACTED]' }, + ], + }, + }, + }, + loggers: [ + { + name: 'http.server.response', + appenders: ['rewrite'], + level: 'debug', + }, + ], + }, + plugins: { + initialize: false, + }, + }); + const { http } = await root.setup(); + + http.createRouter('/').post( + { + path: '/ping', + validate: { + body: schema.object({ message: schema.string() }), + }, + options: { + authRequired: 'optional', + body: { + accepts: ['application/json'], + }, + timeout: { payload: 100 }, + }, + }, + (context, req, res) => res.ok({ body: { message: req.body.message } }) + ); + await root.start(); + + await kbnTestServer.request + .post(root, '/ping') + .set('content-type', 'application/json') + .set('authorization', 'abc') + .send({ message: 'hi' }) + .expect(200); + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + const [, , , meta] = mockConsoleLog.mock.calls[0][0].split('|'); + expect(JSON.parse(meta).http.request.headers.authorization).toBe('[REDACTED]'); + }); + + it('filters sensitive response headers by defaut', async () => { + const { http } = await root.setup(); + + http.createRouter('/').post( + { + path: '/ping', + validate: { + body: schema.object({ message: schema.string() }), + }, + options: { + authRequired: 'optional', + body: { + accepts: ['application/json'], + }, + timeout: { payload: 100 }, + }, + }, + (context, req, res) => + res.ok({ headers: { 'set-cookie': ['123'] }, body: { message: req.body.message } }) + ); + await root.start(); + + await kbnTestServer.request + .post(root, '/ping') + .set('Content-Type', 'application/json') + .send({ message: 'hi' }) + .expect(200); + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + const [, , , meta] = mockConsoleLog.mock.calls[0][0].split('|'); + expect(JSON.parse(meta).http.response.headers['set-cookie']).toBe('[REDACTED]'); + }); + + it('filters sensitive response headers when RewriteAppender is configured', async () => { + root = kbnTestServer.createRoot({ + logging: { + silent: true, + appenders: { + 'test-console': { + type: 'console', + layout: { + type: 'pattern', + pattern: '%level|%logger|%message|%meta', + }, + }, + rewrite: { + type: 'rewrite', + appenders: ['test-console'], + policy: { + type: 'meta', + mode: 'update', + properties: [{ path: 'http.response.headers.set-cookie', value: '[REDACTED]' }], + }, + }, + }, + loggers: [ + { + name: 'http.server.response', + appenders: ['rewrite'], + level: 'debug', + }, + ], + }, + plugins: { + initialize: false, + }, + }); const { http } = await root.setup(); http.createRouter('/').post( diff --git a/src/core/server/http/logging/get_response_log.test.ts b/src/core/server/http/logging/get_response_log.test.ts index 46c4f1d95e3be0..64241ff44fc6ba 100644 --- a/src/core/server/http/logging/get_response_log.test.ts +++ b/src/core/server/http/logging/get_response_log.test.ts @@ -171,6 +171,53 @@ describe('getEcsResponseLog', () => { }); test('does not mutate original headers', () => { + const reqHeaders = { a: 'foo', b: ['hello', 'world'] }; + const resHeaders = { headers: { c: 'bar' } }; + const req = createMockHapiRequest({ + headers: reqHeaders, + response: { headers: resHeaders }, + }); + + const responseLog = getEcsResponseLog(req, logger); + expect(reqHeaders).toMatchInlineSnapshot(` + Object { + "a": "foo", + "b": Array [ + "hello", + "world", + ], + } + `); + expect(resHeaders).toMatchInlineSnapshot(` + Object { + "headers": Object { + "c": "bar", + }, + } + `); + + responseLog.http.request.headers.a = 'testA'; + responseLog.http.request.headers.b[1] = 'testB'; + responseLog.http.request.headers.c = 'testC'; + expect(reqHeaders).toMatchInlineSnapshot(` + Object { + "a": "foo", + "b": Array [ + "hello", + "world", + ], + } + `); + expect(resHeaders).toMatchInlineSnapshot(` + Object { + "headers": Object { + "c": "bar", + }, + } + `); + }); + + test('does not mutate original headers when redacting sensitive data', () => { const reqHeaders = { authorization: 'a', cookie: 'b', 'user-agent': 'hi' }; const resHeaders = { headers: { 'content-length': 123, 'set-cookie': 'c' } }; const req = createMockHapiRequest({ diff --git a/src/core/server/http/logging/get_response_log.ts b/src/core/server/http/logging/get_response_log.ts index f75acde93bf405..57c02e05bebff2 100644 --- a/src/core/server/http/logging/get_response_log.ts +++ b/src/core/server/http/logging/get_response_log.ts @@ -18,14 +18,22 @@ const ECS_VERSION = '1.7.0'; const FORBIDDEN_HEADERS = ['authorization', 'cookie', 'set-cookie']; const REDACTED_HEADER_TEXT = '[REDACTED]'; +type HapiHeaders = Record; + // We are excluding sensitive headers by default, until we have a log filtering mechanism. -function redactSensitiveHeaders( - headers?: Record -): Record { - const result = {} as Record; +function redactSensitiveHeaders(key: string, value: string | string[]): string | string[] { + return FORBIDDEN_HEADERS.includes(key) ? REDACTED_HEADER_TEXT : value; +} + +// Shallow clone the headers so they are not mutated if filtered by a RewriteAppender. +function cloneAndFilterHeaders(headers?: HapiHeaders) { + const result = {} as HapiHeaders; if (headers) { for (const key of Object.keys(headers)) { - result[key] = FORBIDDEN_HEADERS.includes(key) ? REDACTED_HEADER_TEXT : headers[key]; + result[key] = redactSensitiveHeaders( + key, + Array.isArray(headers[key]) ? [...headers[key]] : headers[key] + ); } } return result; @@ -45,7 +53,11 @@ export function getEcsResponseLog(request: Request, log: Logger): LogMeta { // eslint-disable-next-line @typescript-eslint/naming-convention const status_code = isBoom(response) ? response.output.statusCode : response.statusCode; - const responseHeaders = isBoom(response) ? response.output.headers : response.headers; + + const requestHeaders = cloneAndFilterHeaders(request.headers); + const responseHeaders = cloneAndFilterHeaders( + isBoom(response) ? (response.output.headers as HapiHeaders) : response.headers + ); // borrowed from the hapi/good implementation const responseTime = (request.info.completed || request.info.responded) - request.info.received; @@ -66,7 +78,7 @@ export function getEcsResponseLog(request: Request, log: Logger): LogMeta { mime_type: request.mime, referrer: request.info.referrer, // @ts-expect-error Headers are not yet part of ECS: https://github.com/elastic/ecs/issues/232. - headers: redactSensitiveHeaders(request.headers), + headers: requestHeaders, }, response: { body: { @@ -74,7 +86,7 @@ export function getEcsResponseLog(request: Request, log: Logger): LogMeta { }, status_code, // @ts-expect-error Headers are not yet part of ECS: https://github.com/elastic/ecs/issues/232. - headers: redactSensitiveHeaders(responseHeaders), + headers: responseHeaders, // responseTime is a custom non-ECS field responseTime: !isNaN(responseTime) ? responseTime : undefined, }, diff --git a/src/core/server/logging/README.mdx b/src/core/server/logging/README.mdx index 8c093d02315859..1575e67d7b8ee0 100644 --- a/src/core/server/logging/README.mdx +++ b/src/core/server/logging/README.mdx @@ -278,6 +278,124 @@ The maximum number of files to keep. Once this number is reached, oldest files w The default value is `7` +### Rewrite Appender + +*This appender is currently considered experimental and is not intended +for public consumption. The API is subject to change at any time.* + +Similar to log4j's `RewriteAppender`, this appender serves as a sort of middleware, +modifying the provided log events before passing them along to another +appender. + +```yaml +logging: + appenders: + my-rewrite-appender: + type: rewrite + appenders: [console, file] # name of "destination" appender(s) + policy: + # ... +``` + +The most common use case for the `RewriteAppender` is when you want to +filter or censor sensitive data that may be contained in a log entry. +In fact, with a default configuration, Kibana will automatically redact +any `authorization`, `cookie`, or `set-cookie` headers when logging http +requests & responses. + +To configure additional rewrite rules, you'll need to specify a `RewritePolicy`. + +#### Rewrite Policies + +Rewrite policies exist to indicate which parts of a log record can be +modified within the rewrite appender. + +**Meta** + +The `meta` rewrite policy can read and modify any data contained in the +`LogMeta` before passing it along to a destination appender. + +Meta policies must specify one of three modes, which indicate which action +to perform on the configured properties: +- `update` updates an existing property at the provided `path`. +- `remove` removes an existing property at the provided `path`. + +The `properties` are listed as a `path` and `value` pair, where `path` is +the dot-delimited path to the target property in the `LogMeta` object, and +`value` is the value to add or update in that target property. When using +the `remove` mode, a `value` is not necessary. + +Here's an example of how you would replace any `cookie` header values with `[REDACTED]`: + +```yaml +logging: + appenders: + my-rewrite-appender: + type: rewrite + appenders: [console] + policy: + type: meta # indicates that we want to rewrite the LogMeta + mode: update # will update an existing property only + properties: + - path: "http.request.headers.cookie" # path to property + value: "[REDACTED]" # value to replace at path +``` + +Rewrite appenders can even be passed to other rewrite appenders to apply +multiple filter policies/modes, as long as it doesn't create a circular +reference. Each rewrite appender is applied sequentially (one after the other). +```yaml +logging: + appenders: + remove-request-headers: + type: rewrite + appenders: [censor-response-headers] # redirect to the next rewrite appender + policy: + type: meta + mode: remove + properties: + - path: "http.request.headers" # remove all request headers + censor-response-headers: + type: rewrite + appenders: [console] # output to console + policy: + type: meta + mode: update + properties: + - path: "http.response.headers.set-cookie" + value: "[REDACTED]" +``` + +#### Complete Example +```yaml +logging: + appenders: + console: + type: console + layout: + type: pattern + highlight: true + pattern: "[%date][%level][%logger] %message %meta" + file: + type: file + fileName: ./kibana.log + layout: + type: json + censor: + type: rewrite + appenders: [console, file] + policy: + type: meta + mode: update + properties: + - path: "http.request.headers.cookie" + value: "[REDACTED]" + loggers: + - name: http.server.response + appenders: [censor] # pass these logs to our rewrite appender + level: debug +``` + ## Configuration As any configuration in the platform, logging configuration is validated against the predefined schema and if there are diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index a41a6a2f68fa1b..88df355bd5ebef 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -17,6 +17,7 @@ import { import { Layouts } from '../layouts/layouts'; import { ConsoleAppender, ConsoleAppenderConfig } from './console/console_appender'; import { FileAppender, FileAppenderConfig } from './file/file_appender'; +import { RewriteAppender, RewriteAppenderConfig } from './rewrite/rewrite_appender'; import { RollingFileAppender, RollingFileAppenderConfig, @@ -32,6 +33,7 @@ export const appendersSchema = schema.oneOf([ ConsoleAppender.configSchema, FileAppender.configSchema, LegacyAppender.configSchema, + RewriteAppender.configSchema, RollingFileAppender.configSchema, ]); @@ -40,6 +42,7 @@ export type AppenderConfigType = | ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig + | RewriteAppenderConfig | RollingFileAppenderConfig; /** @internal */ @@ -57,6 +60,8 @@ export class Appenders { return new ConsoleAppender(Layouts.create(config.layout)); case 'file': return new FileAppender(Layouts.create(config.layout), config.fileName); + case 'rewrite': + return new RewriteAppender(config); case 'rolling-file': return new RollingFileAppender(config); case 'legacy-appender': diff --git a/src/core/server/logging/appenders/rewrite/mocks.ts b/src/core/server/logging/appenders/rewrite/mocks.ts new file mode 100644 index 00000000000000..a19756e25bf8ea --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/mocks.ts @@ -0,0 +1,20 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { RewritePolicy } from './policies/policy'; + +const createPolicyMock = () => { + const mock: jest.Mocked = { + rewrite: jest.fn((x) => x), + }; + return mock; +}; + +export const rewriteAppenderMocks = { + createPolicy: createPolicyMock, +}; diff --git a/src/core/server/logging/appenders/rewrite/policies/index.ts b/src/core/server/logging/appenders/rewrite/policies/index.ts new file mode 100644 index 00000000000000..ae3be1e4de916a --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/policies/index.ts @@ -0,0 +1,30 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { assertNever } from '@kbn/std'; +import { RewritePolicy } from './policy'; +import { MetaRewritePolicy, MetaRewritePolicyConfig, metaRewritePolicyConfigSchema } from './meta'; + +export { RewritePolicy }; + +/** + * Available rewrite policies which specify what part of a {@link LogRecord} + * can be modified. + */ +export type RewritePolicyConfig = MetaRewritePolicyConfig; + +export const rewritePolicyConfigSchema = metaRewritePolicyConfigSchema; + +export const createRewritePolicy = (config: RewritePolicyConfig): RewritePolicy => { + switch (config.type) { + case 'meta': + return new MetaRewritePolicy(config); + default: + return assertNever(config.type); + } +}; diff --git a/src/core/server/logging/appenders/rewrite/policies/meta/index.ts b/src/core/server/logging/appenders/rewrite/policies/meta/index.ts new file mode 100644 index 00000000000000..afdfd6fb709d3e --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/policies/meta/index.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + MetaRewritePolicy, + MetaRewritePolicyConfig, + metaRewritePolicyConfigSchema, +} from './meta_policy'; diff --git a/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.test.ts b/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.test.ts new file mode 100644 index 00000000000000..52b88331a75bee --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.test.ts @@ -0,0 +1,154 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { LogRecord, LogLevel, LogMeta } from '@kbn/logging'; +import { MetaRewritePolicy, MetaRewritePolicyConfig } from './meta_policy'; + +describe('MetaRewritePolicy', () => { + const createPolicy = ( + mode: MetaRewritePolicyConfig['mode'], + properties: MetaRewritePolicyConfig['properties'] + ) => new MetaRewritePolicy({ type: 'meta', mode, properties }); + + const createLogRecord = (meta: LogMeta = {}): LogRecord => ({ + timestamp: new Date(Date.UTC(2012, 1, 1, 14, 30, 22, 11)), + level: LogLevel.Info, + context: 'context', + message: 'just a log', + pid: 42, + meta, + }); + + describe('mode: update', () => { + it('updates existing properties in LogMeta', () => { + const log = createLogRecord({ a: 'before' }); + const policy = createPolicy('update', [{ path: 'a', value: 'after' }]); + expect(policy.rewrite(log).meta!.a).toBe('after'); + }); + + it('updates nested properties in LogMeta', () => { + const log = createLogRecord({ a: 'before a', b: { c: 'before b.c' }, d: [0, 1] }); + const policy = createPolicy('update', [ + { path: 'a', value: 'after a' }, + { path: 'b.c', value: 'after b.c' }, + { path: 'd[1]', value: 2 }, + ]); + expect(policy.rewrite(log).meta).toMatchInlineSnapshot(` + Object { + "a": "after a", + "b": Object { + "c": "after b.c", + }, + "d": Array [ + 0, + 2, + ], + } + `); + }); + + it('handles string, number, boolean, null', () => { + const policy = createPolicy('update', [ + { path: 'a', value: false }, + { path: 'b', value: null }, + { path: 'c', value: 123 }, + { path: 'd', value: 'hi' }, + ]); + const log = createLogRecord({ + a: 'a', + b: 'b', + c: 'c', + d: 'd', + }); + expect(policy.rewrite(log).meta).toMatchInlineSnapshot(` + Object { + "a": false, + "b": null, + "c": 123, + "d": "hi", + } + `); + }); + + it(`does not add properties which don't exist yet`, () => { + const policy = createPolicy('update', [ + { path: 'a.b', value: 'foo' }, + { path: 'a.c', value: 'bar' }, + ]); + const log = createLogRecord({ a: { b: 'existing meta' } }); + const { meta } = policy.rewrite(log); + expect(meta!.a.b).toBe('foo'); + expect(meta!.a.c).toBeUndefined(); + }); + + it('does not touch anything outside of LogMeta', () => { + const policy = createPolicy('update', [{ path: 'a', value: 'bar' }]); + const message = Symbol(); + expect( + policy.rewrite(({ message, meta: { a: 'foo' } } as unknown) as LogRecord).message + ).toBe(message); + expect(policy.rewrite(({ message, meta: { a: 'foo' } } as unknown) as LogRecord)) + .toMatchInlineSnapshot(` + Object { + "message": Symbol(), + "meta": Object { + "a": "bar", + }, + } + `); + }); + }); + + describe('mode: remove', () => { + it('removes existing properties in LogMeta', () => { + const log = createLogRecord({ a: 'goodbye' }); + const policy = createPolicy('remove', [{ path: 'a' }]); + expect(policy.rewrite(log).meta!.a).toBeUndefined(); + }); + + it('removes nested properties in LogMeta', () => { + const log = createLogRecord({ a: 'a', b: { c: 'b.c' }, d: [0, 1] }); + const policy = createPolicy('remove', [{ path: 'b.c' }, { path: 'd[1]' }]); + expect(policy.rewrite(log).meta).toMatchInlineSnapshot(` + Object { + "a": "a", + "b": Object {}, + "d": Array [ + 0, + undefined, + ], + } + `); + }); + + it('has no effect if property does not exist', () => { + const log = createLogRecord({ a: 'a' }); + const policy = createPolicy('remove', [{ path: 'b' }]); + expect(policy.rewrite(log).meta).toMatchInlineSnapshot(` + Object { + "a": "a", + } + `); + }); + + it('does not touch anything outside of LogMeta', () => { + const policy = createPolicy('remove', [{ path: 'message' }]); + const message = Symbol(); + expect( + policy.rewrite(({ message, meta: { message: 'foo' } } as unknown) as LogRecord).message + ).toBe(message); + expect(policy.rewrite(({ message, meta: { message: 'foo' } } as unknown) as LogRecord)) + .toMatchInlineSnapshot(` + Object { + "message": Symbol(), + "meta": Object {}, + } + `); + }); + }); +}); diff --git a/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.ts b/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.ts new file mode 100644 index 00000000000000..2215b3489539fa --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/policies/meta/meta_policy.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { LogRecord } from '@kbn/logging'; +import { set } from '@elastic/safer-lodash-set'; +import { has, unset } from 'lodash'; +import { assertNever } from '@kbn/std'; +import { RewritePolicy } from '../policy'; + +type MetaRewritePolicyConfigProperties = Array<{ + path: string; + value?: string | number | boolean | null; +}>; + +export interface MetaRewritePolicyConfig { + type: 'meta'; + + /** + * The 'mode' specifies what action to perform on the specified properties. + * - 'update' updates an existing property at the provided 'path'. + * - 'remove' removes an existing property at the provided 'path'. + */ + mode: 'remove' | 'update'; + + /** + * The properties to modify. + * + * @remarks + * Each provided 'path' is relative to the record's {@link LogMeta}. + * For the 'remove' mode, no 'value' is provided. + */ + properties: MetaRewritePolicyConfigProperties; +} + +export const metaRewritePolicyConfigSchema = schema.object({ + type: schema.literal('meta'), + mode: schema.oneOf([schema.literal('update'), schema.literal('remove')], { + defaultValue: 'update', + }), + properties: schema.arrayOf( + schema.object({ + path: schema.string(), + value: schema.maybe( + schema.nullable(schema.oneOf([schema.string(), schema.number(), schema.boolean()])) + ), + }) + ), +}); + +/** + * A rewrite policy which can add, remove, or update properties + * from a record's {@link LogMeta}. + */ +export class MetaRewritePolicy implements RewritePolicy { + constructor(private readonly config: MetaRewritePolicyConfig) {} + + rewrite(record: LogRecord): LogRecord { + switch (this.config.mode) { + case 'update': + return this.update(record); + case 'remove': + return this.remove(record); + default: + return assertNever(this.config.mode); + } + } + + private update(record: LogRecord) { + for (const { path, value } of this.config.properties) { + if (!has(record, `meta.${path}`)) { + continue; // don't add properties which don't already exist + } + set(record, `meta.${path}`, value); + } + return record; + } + + private remove(record: LogRecord) { + for (const { path } of this.config.properties) { + unset(record, `meta.${path}`); + } + return record; + } +} diff --git a/src/core/server/logging/appenders/rewrite/policies/policy.ts b/src/core/server/logging/appenders/rewrite/policies/policy.ts new file mode 100644 index 00000000000000..f8aef887965fd2 --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/policies/policy.ts @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { LogRecord } from '@kbn/logging'; + +/** + * Rewrites a {@link LogRecord} based on the policy's configuration. + **/ +export interface RewritePolicy { + rewrite(record: LogRecord): LogRecord; +} diff --git a/src/core/server/logging/appenders/rewrite/rewrite_appender.test.mocks.ts b/src/core/server/logging/appenders/rewrite/rewrite_appender.test.mocks.ts new file mode 100644 index 00000000000000..9d29a683057922 --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/rewrite_appender.test.mocks.ts @@ -0,0 +1,19 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; + +export const createRewritePolicyMock = jest.fn(); +jest.doMock('./policies', () => ({ + rewritePolicyConfigSchema: schema.any(), + createRewritePolicy: createRewritePolicyMock, +})); + +export const resetAllMocks = () => { + createRewritePolicyMock.mockReset(); +}; diff --git a/src/core/server/logging/appenders/rewrite/rewrite_appender.test.ts b/src/core/server/logging/appenders/rewrite/rewrite_appender.test.ts new file mode 100644 index 00000000000000..72a54b5012ce54 --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/rewrite_appender.test.ts @@ -0,0 +1,137 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { MockedKeys } from '@kbn/utility-types/jest'; +import { createRewritePolicyMock, resetAllMocks } from './rewrite_appender.test.mocks'; +import { rewriteAppenderMocks } from './mocks'; +import { LogLevel, LogRecord, LogMeta, DisposableAppender } from '@kbn/logging'; +import { RewriteAppender, RewriteAppenderConfig } from './rewrite_appender'; + +// Helper to ensure tuple is typed [A, B] instead of Array +const toTuple = (a: A, b: B): [A, B] => [a, b]; + +const createAppenderMock = (name: string) => { + const appenderMock: MockedKeys = { + append: jest.fn(), + dispose: jest.fn(), + }; + + return toTuple(name, appenderMock); +}; + +const createConfig = (appenderNames: string[]): RewriteAppenderConfig => ({ + type: 'rewrite', + appenders: appenderNames, + policy: { + type: 'meta', + mode: 'update', + properties: [{ path: 'foo', value: 'bar' }], + }, +}); + +const createLogRecord = (meta: LogMeta = {}): LogRecord => ({ + timestamp: new Date(), + level: LogLevel.Info, + context: 'context', + message: 'just a log', + pid: 42, + meta, +}); + +describe('RewriteAppender', () => { + let policy: ReturnType; + + beforeEach(() => { + policy = rewriteAppenderMocks.createPolicy(); + createRewritePolicyMock.mockReturnValue(policy); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + resetAllMocks(); + }); + + it('creates a rewrite policy with the provided config', () => { + const config = createConfig([]); + new RewriteAppender(config); + expect(createRewritePolicyMock).toHaveBeenCalledTimes(1); + expect(createRewritePolicyMock).toHaveBeenCalledWith(config.policy); + }); + + describe('#addAppender', () => { + it('updates the map of available appenders', () => { + const config = createConfig(['mock1']); + const appender = new RewriteAppender(config); + appender.addAppender(...createAppenderMock('mock1')); + expect(() => { + appender.append(createLogRecord()); + }).not.toThrowError(); + }); + }); + + describe('#append', () => { + it('calls the configured appenders with the provided LogRecord', () => { + const config = createConfig(['mock1', 'mock2']); + const appenderMocks = [createAppenderMock('mock1'), createAppenderMock('mock2')]; + + const appender = new RewriteAppender(config); + appenderMocks.forEach((mock) => appender.addAppender(...mock)); + + const log1 = createLogRecord({ a: 'b' }); + const log2 = createLogRecord({ c: 'd' }); + + appender.append(log1); + + expect(appenderMocks[0][1].append).toHaveBeenCalledTimes(1); + expect(appenderMocks[1][1].append).toHaveBeenCalledTimes(1); + expect(appenderMocks[0][1].append).toHaveBeenCalledWith(log1); + expect(appenderMocks[1][1].append).toHaveBeenCalledWith(log1); + + appender.append(log2); + + expect(appenderMocks[0][1].append).toHaveBeenCalledTimes(2); + expect(appenderMocks[1][1].append).toHaveBeenCalledTimes(2); + expect(appenderMocks[0][1].append).toHaveBeenCalledWith(log2); + expect(appenderMocks[1][1].append).toHaveBeenCalledWith(log2); + }); + + it('calls `rewrite` on the configured policy', () => { + const config = createConfig(['mock1']); + + const appender = new RewriteAppender(config); + appender.addAppender(...createAppenderMock('mock1')); + + const log1 = createLogRecord({ a: 'b' }); + const log2 = createLogRecord({ c: 'd' }); + + appender.append(log1); + + expect(policy.rewrite).toHaveBeenCalledTimes(1); + expect(policy.rewrite.mock.calls).toEqual([[log1]]); + + appender.append(log2); + + expect(policy.rewrite).toHaveBeenCalledTimes(2); + expect(policy.rewrite.mock.calls).toEqual([[log1], [log2]]); + }); + + it('throws if an appender key cannot be found', () => { + const config = createConfig(['oops']); + const appender = new RewriteAppender(config); + + expect(() => { + appender.append(createLogRecord()); + }).toThrowErrorMatchingInlineSnapshot( + `"Rewrite Appender could not find appender key \\"oops\\". Be sure \`appender.addAppender()\` was called before \`appender.append()\`."` + ); + }); + }); +}); diff --git a/src/core/server/logging/appenders/rewrite/rewrite_appender.ts b/src/core/server/logging/appenders/rewrite/rewrite_appender.ts new file mode 100644 index 00000000000000..e54d8ba40ebfcc --- /dev/null +++ b/src/core/server/logging/appenders/rewrite/rewrite_appender.ts @@ -0,0 +1,100 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { LogRecord, Appender, DisposableAppender } from '@kbn/logging'; +import { + createRewritePolicy, + rewritePolicyConfigSchema, + RewritePolicy, + RewritePolicyConfig, +} from './policies'; + +export interface RewriteAppenderConfig { + type: 'rewrite'; + /** + * The {@link Appender | appender(s)} to pass the log event to after + * implementing the specified rewrite policy. + */ + appenders: string[]; + /** + * The {@link RewritePolicy | policy} to use to manipulate the provided data. + */ + policy: RewritePolicyConfig; +} + +/** + * Appender that can modify the `LogRecord` instances it receives before passing + * them along to another {@link Appender}. + * @internal + */ +export class RewriteAppender implements DisposableAppender { + public static configSchema = schema.object({ + type: schema.literal('rewrite'), + appenders: schema.arrayOf(schema.string(), { defaultValue: [] }), + policy: rewritePolicyConfigSchema, + }); + + private appenders: Map = new Map(); + private readonly policy: RewritePolicy; + + constructor(private readonly config: RewriteAppenderConfig) { + this.policy = createRewritePolicy(config.policy); + } + + /** + * List of appenders that are dependencies of this appender. + * + * `addAppender` will throw an error when called with an appender + * reference that isn't in this list. + */ + public get appenderRefs() { + return this.config.appenders; + } + + /** + * Appenders can be "attached" to this one so that the RewriteAppender + * is able to act as a sort of middleware by calling `append` on other appenders. + * + * As appenders cannot be attached to each other until they are created, + * the `addAppender` method is used to pass in a configured appender. + */ + public addAppender(appenderRef: string, appender: Appender) { + if (!this.appenderRefs.includes(appenderRef)) { + throw new Error( + `addAppender was called with an appender key that is missing from the appenderRefs: "${appenderRef}".` + ); + } + + this.appenders.set(appenderRef, appender); + } + + /** + * Modifies the `record` and passes it to the specified appender. + */ + public append(record: LogRecord) { + const rewrittenRecord = this.policy.rewrite(record); + for (const appenderRef of this.appenderRefs) { + const appender = this.appenders.get(appenderRef); + if (!appender) { + throw new Error( + `Rewrite Appender could not find appender key "${appenderRef}". ` + + 'Be sure `appender.addAppender()` was called before `appender.append()`.' + ); + } + appender.append(rewrittenRecord); + } + } + + /** + * Disposes `RewriteAppender`. + */ + public dispose() { + this.appenders.clear(); + } +} diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index 2cb5831a8fb4ce..83f3c139e371af 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -78,7 +78,6 @@ test('correctly fills in custom `appenders` config.', () => { type: 'console', layout: { type: 'pattern', highlight: true }, }); - expect(configValue.appenders.get('console')).toEqual({ type: 'console', layout: { type: 'pattern' }, diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index f68d6c6a97fbc4..8a6fe71bc62220 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -134,6 +134,76 @@ test('uses `root` logger if context name is not specified.', async () => { expect(mockConsoleLog.mock.calls).toMatchSnapshot(); }); +test('attaches appenders to appenders that declare refs', async () => { + await system.upgrade( + config.schema.validate({ + appenders: { + console: { + type: 'console', + layout: { type: 'pattern', pattern: '[%logger] %message %meta' }, + }, + file: { + type: 'file', + layout: { type: 'pattern', pattern: '[%logger] %message %meta' }, + fileName: 'path', + }, + rewrite: { + type: 'rewrite', + appenders: ['console', 'file'], + policy: { type: 'meta', mode: 'remove', properties: [{ path: 'b' }] }, + }, + }, + loggers: [{ name: 'tests', level: 'warn', appenders: ['rewrite'] }], + }) + ); + + const testLogger = system.get('tests'); + testLogger.warn('This message goes to a test context.', { a: 'hi', b: 'remove me' }); + + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog.mock.calls[0][0]).toMatchInlineSnapshot( + `"[tests] This message goes to a test context. {\\"a\\":\\"hi\\"}"` + ); + + expect(mockStreamWrite).toHaveBeenCalledTimes(1); + expect(mockStreamWrite.mock.calls[0][0]).toMatchInlineSnapshot(` + "[tests] This message goes to a test context. {\\"a\\":\\"hi\\"} + " + `); +}); + +test('throws if a circular appender reference is detected', async () => { + expect(async () => { + await system.upgrade( + config.schema.validate({ + appenders: { + console: { type: 'console', layout: { type: 'pattern' } }, + a: { + type: 'rewrite', + appenders: ['b'], + policy: { type: 'meta', mode: 'remove', properties: [{ path: 'b' }] }, + }, + b: { + type: 'rewrite', + appenders: ['c'], + policy: { type: 'meta', mode: 'remove', properties: [{ path: 'b' }] }, + }, + c: { + type: 'rewrite', + appenders: ['console', 'a'], + policy: { type: 'meta', mode: 'remove', properties: [{ path: 'b' }] }, + }, + }, + loggers: [{ name: 'tests', level: 'warn', appenders: ['a'] }], + }) + ); + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"Circular appender reference detected: [b -> c -> a -> b]"` + ); + + expect(mockConsoleLog).toHaveBeenCalledTimes(0); +}); + test('`stop()` disposes all appenders.', async () => { await system.upgrade( config.schema.validate({ diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts index 9ae434aff41d3c..d7c34b48c41012 100644 --- a/src/core/server/logging/logging_system.ts +++ b/src/core/server/logging/logging_system.ts @@ -146,6 +146,26 @@ export class LoggingSystem implements LoggerFactory { return this.getLoggerConfigByContext(config, LoggingConfig.getParentLoggerContext(context)); } + /** + * Retrieves an appender by the provided key, after first checking that no circular + * dependencies exist between appender refs. + */ + private getAppenderByRef(appenderRef: string) { + const checkCircularRefs = (key: string, stack: string[]) => { + if (stack.includes(key)) { + throw new Error(`Circular appender reference detected: [${stack.join(' -> ')} -> ${key}]`); + } + stack.push(key); + const appender = this.appenders.get(key); + if (appender?.appenderRefs) { + appender.appenderRefs.forEach((ref) => checkCircularRefs(ref, [...stack])); + } + return appender; + }; + + return checkCircularRefs(appenderRef, []); + } + private async applyBaseConfig(newBaseConfig: LoggingConfig) { const computedConfig = [...this.contextConfigs.values()].reduce( (baseConfig, contextConfig) => baseConfig.extend(contextConfig), @@ -167,6 +187,23 @@ export class LoggingSystem implements LoggerFactory { this.appenders.set(appenderKey, Appenders.create(appenderConfig)); } + // Once all appenders have been created, check for any that have explicitly + // declared `appenderRefs` dependencies, and look up those dependencies to + // attach to the appender. This enables appenders to act as a sort of + // middleware and call `append` on each other if needed. + for (const [key, appender] of this.appenders) { + if (!appender.addAppender || !appender.appenderRefs) { + continue; + } + for (const ref of appender.appenderRefs) { + const foundAppender = this.getAppenderByRef(ref); + if (!foundAppender) { + throw new Error(`Appender "${key}" config contains unknown appender key "${ref}".`); + } + appender.addAppender(ref, foundAppender); + } + } + for (const [loggerKey, loggerAdapter] of this.loggers) { loggerAdapter.updateLogger(this.createLogger(loggerKey, computedConfig)); } diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index eae0e73e86c465..7ac629534ba089 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -47,6 +47,7 @@ const KNOWN_MANIFEST_FIELDS = (() => { server: true, extraPublicDirs: true, requiredBundles: true, + serviceFolders: true, }; return new Set(Object.keys(manifestFields)); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 45db98201b7587..a6086bd6f17e8e 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -169,6 +169,12 @@ export interface PluginManifest { * @deprecated */ readonly extraPublicDirs?: string[]; + + /** + * Only used for the automatically generated API documentation. Specifying service + * folders will cause your plugin API reference to be broken up into sub sections. + */ + readonly serviceFolders?: readonly string[]; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 2177da84b2b53d..72d66bc04f08e8 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -180,10 +180,11 @@ export interface AppCategory { // Warning: (ae-forgotten-export) The symbol "ConsoleAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FileAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "LegacyAppenderConfig" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RewriteAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RollingFileAppenderConfig" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RollingFileAppenderConfig; +export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; // @public @deprecated (undocumented) export interface AssistanceAPIResponse { @@ -1882,6 +1883,7 @@ export interface PluginManifest { readonly requiredBundles: readonly string[]; readonly requiredPlugins: readonly PluginName[]; readonly server: boolean; + readonly serviceFolders?: readonly string[]; readonly ui: boolean; readonly version: string; } @@ -3197,9 +3199,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:388:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:289:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:394:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index c4559029e5607a..f9c1e67c0540d9 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -21,6 +21,10 @@ cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" +if [[ "$BUILD_TS_REFS_CACHE_ENABLE" != "true" ]]; then + export BUILD_TS_REFS_CACHE_ENABLE=false +fi + ### ### install dependencies ### diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts index fc8911a2517733..a073e586232782 100644 --- a/src/dev/typescript/build_ts_refs_cli.ts +++ b/src/dev/typescript/build_ts_refs_cli.ts @@ -23,7 +23,7 @@ export async function runBuildRefsCli() { async ({ log, flags }) => { const outDirs = getOutputsDeep(REF_CONFIG_PATHS); - const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE === 'true' || !!flags.cache; + const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE !== 'false' && !!flags.cache; const doCapture = process.env.BUILD_TS_REFS_CACHE_CAPTURE === 'true'; const doClean = !!flags.clean || doCapture; const doInitCache = cacheEnabled && !doClean; @@ -62,6 +62,9 @@ export async function runBuildRefsCli() { description: 'Build TypeScript projects', flags: { boolean: ['clean', 'cache'], + default: { + cache: true, + }, }, log: { defaultLevel: 'debug', diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts index 342470ce0c6e36..6f51243e47555b 100644 --- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts +++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts @@ -132,7 +132,7 @@ export class RefOutputCache { this.log.debug(`[${relative}] clearing outDir and replacing with cache`); await del(outDir); await unzip(Path.resolve(tmpDir, cacheName), outDir); - await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), archive.sha); + await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), this.mergeBase); }); } diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index a09ab12f0c6f09..452b081d6387f7 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -10,6 +10,7 @@ "share", "inspector" ], + "serviceFolders": ["search", "index_patterns", "query", "autocomplete", "ui", "field_formats"], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], "requiredBundles": [ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 05e5c8577ebe3d..884873f4535ccb 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1531,6 +1531,7 @@ export type IndexPatternSelectProps = Required, 'isLo indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; + maxIndexPatterns?: number; }; // Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 96e7a6d83d2d27..aa36323d11bcc4 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -25,6 +25,7 @@ export type IndexPatternSelectProps = Required< indexPatternId: string; fieldTypes?: string[]; onNoIndexPatterns?: () => void; + maxIndexPatterns?: number; }; export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { @@ -41,6 +42,10 @@ interface IndexPatternSelectState { // Needed for React.lazy // eslint-disable-next-line import/no-default-export export default class IndexPatternSelect extends Component { + static defaultProps: { + maxIndexPatterns: 1000; + }; + private isMounted: boolean = false; state: IndexPatternSelectState; @@ -103,7 +108,10 @@ export default class IndexPatternSelect extends Component { const { fieldTypes, onNoIndexPatterns, indexPatternService } = this.props; - const indexPatterns = await indexPatternService.find(`${searchValue}*`, 100); + const indexPatterns = await indexPatternService.find( + `${searchValue}*`, + this.props.maxIndexPatterns + ); // We need this check to handle the case where search results come back in a different // order than they were sent out. Only load results for the most recent search. diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index a7a5b8626914af..349e024f31c315 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -9,7 +9,9 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectMetaData, OnSaveProps } from 'src/plugins/saved_objects/public'; import { first } from 'rxjs/operators'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { SavedObjectAttributes } from '../../../../core/public'; +import { extractSearchSourceReferences } from '../../../data/public'; import { EmbeddableFactoryDefinition, EmbeddableOutput, @@ -236,4 +238,42 @@ export class VisualizeEmbeddableFactory } ); } + + public extract(_state: EmbeddableStateWithType) { + const state = (_state as unknown) as VisualizeInput; + const references = []; + + if (state.savedVis?.data.searchSource) { + const [, searchSourceReferences] = extractSearchSourceReferences( + state.savedVis.data.searchSource + ); + + references.push(...searchSourceReferences); + } + + if (state.savedVis?.data.savedSearchId) { + references.push({ + name: 'search_0', + type: 'search', + id: String(state.savedVis.data.savedSearchId), + }); + } + + if (state.savedVis?.params.controls) { + const controls = state.savedVis.params.controls; + controls.forEach((control: Record, i: number) => { + if (!control.indexPattern) { + return; + } + control.indexPatternRefName = `control_${i}_index_pattern`; + references.push({ + name: control.indexPatternRefName, + type: 'index-pattern', + id: control.indexPattern, + }); + }); + } + + return { state: _state, references }; + } } diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts index 809c4ad1ea1bd7..490519187f49ea 100644 --- a/x-pack/plugins/case/server/client/cases/mock.ts +++ b/x-pack/plugins/case/server/client/cases/mock.ts @@ -11,6 +11,7 @@ import { ConnectorMappingsAttributes, CaseUserActionsResponse, AssociationType, + CommentResponseAlertsType, } from '../../../common/api'; import { BasicParams } from './types'; @@ -76,6 +77,20 @@ export const commentAlert: CommentResponse = { version: 'WzEsMV0=', }; +export const commentAlertMultipleIds: CommentResponseAlertsType = { + ...commentAlert, + id: 'mock-comment-2', + alertId: ['alert-id-1', 'alert-id-2'], + index: 'alert-index-1', + type: CommentType.alert as const, +}; + +export const commentGeneratedAlert: CommentResponseAlertsType = { + ...commentAlertMultipleIds, + id: 'mock-comment-3', + type: CommentType.generatedAlert as const, +}; + export const defaultPipes = ['informationCreated']; export const basicParams: BasicParams = { description: 'a description', diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts index f1d56e7132bd11..2dd2caf9fe73a0 100644 --- a/x-pack/plugins/case/server/client/cases/types.ts +++ b/x-pack/plugins/case/server/client/cases/types.ts @@ -72,7 +72,7 @@ export interface TransformFieldsArgs { export interface ExternalServiceComment { comment: string; - commentId: string; + commentId?: string; } export interface MapIncident { diff --git a/x-pack/plugins/case/server/client/cases/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts index 361d0fb561afd7..44e7a682aa7edf 100644 --- a/x-pack/plugins/case/server/client/cases/utils.test.ts +++ b/x-pack/plugins/case/server/client/cases/utils.test.ts @@ -17,6 +17,8 @@ import { basicParams, userActions, commentAlert, + commentAlertMultipleIds, + commentGeneratedAlert, } from './mock'; import { @@ -48,7 +50,7 @@ describe('utils', () => { { actionType: 'overwrite', key: 'short_description', - pipes: ['informationCreated'], + pipes: [], value: 'a title', }, { @@ -71,7 +73,7 @@ describe('utils', () => { { actionType: 'overwrite', key: 'short_description', - pipes: ['myTestPipe'], + pipes: [], value: 'a title', }, { @@ -98,7 +100,7 @@ describe('utils', () => { }); expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title', description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', }); }); @@ -122,13 +124,13 @@ describe('utils', () => { }, fields, currentIncident: { - short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'first title', description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, }); expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', + short_description: 'a title', description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', }); @@ -168,7 +170,7 @@ describe('utils', () => { }); expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', + short_description: 'a title', description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', }); }); @@ -190,7 +192,7 @@ describe('utils', () => { }); expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + short_description: 'a title', description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', }); }); @@ -448,8 +450,7 @@ describe('utils', () => { labels: ['defacement'], issueType: null, parent: null, - short_description: - 'Super Bad Security Issue (created at 2019-11-25T21:54:48.952Z by elastic)', + short_description: 'Super Bad Security Issue', description: 'This is a brand new case of a bad meanie defacing data (created at 2019-11-25T21:54:48.952Z by elastic)', externalId: null, @@ -504,7 +505,7 @@ describe('utils', () => { expect(res.comments).toEqual([]); }); - it('it creates comments of type alert correctly', async () => { + it('it adds the total alert comments correctly', async () => { const res = await createIncident({ actionsClient: actionsMock, theCase: { @@ -512,7 +513,9 @@ describe('utils', () => { comments: [ { ...commentObj, id: 'comment-user-1' }, { ...commentAlert, id: 'comment-alert-1' }, - { ...commentAlert, id: 'comment-alert-2' }, + { + ...commentAlertMultipleIds, + }, ], }, // Remove second push @@ -536,14 +539,36 @@ describe('utils', () => { commentId: 'comment-user-1', }, { - comment: - 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', - commentId: 'comment-alert-1', + comment: 'Elastic Security Alerts attached to the case: 3', }, + ]); + }); + + it('it removes alerts correctly', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [ + { ...commentObj, id: 'comment-user-1' }, + commentAlertMultipleIds, + commentGeneratedAlert, + ], + }, + userActions, + connector, + mappings, + alerts: [], + }); + + expect(res.comments).toEqual([ { comment: - 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', - commentId: 'comment-alert-2', + 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-user-1', + }, + { + comment: 'Elastic Security Alerts attached to the case: 4', }, ]); }); @@ -578,8 +603,7 @@ describe('utils', () => { description: 'fun description \r\nThis is a brand new case of a bad meanie defacing data (updated at 2019-11-25T21:54:48.952Z by elastic)', externalId: 'external-id', - short_description: - 'Super Bad Security Issue (updated at 2019-11-25T21:54:48.952Z by elastic)', + short_description: 'Super Bad Security Issue', }, comments: [], }); diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts index fda4142bf77c7b..a5013d9b93982f 100644 --- a/x-pack/plugins/case/server/client/cases/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -40,6 +40,15 @@ import { } from './types'; import { getAlertIds } from '../../routes/api/utils'; +interface CreateIncidentArgs { + actionsClient: ActionsClient; + theCase: CaseResponse; + userActions: CaseUserActionsResponse; + connector: ActionConnector; + mappings: ConnectorMappingsAttributes[]; + alerts: CaseClientGetAlertsResponse; +} + export const getLatestPushInfo = ( connectorId: string, userActions: CaseUserActionsResponse @@ -75,14 +84,13 @@ const getCommentContent = (comment: CommentResponse): string => { return ''; }; -interface CreateIncidentArgs { - actionsClient: ActionsClient; - theCase: CaseResponse; - userActions: CaseUserActionsResponse; - connector: ActionConnector; - mappings: ConnectorMappingsAttributes[]; - alerts: CaseClientGetAlertsResponse; -} +const countAlerts = (comments: CaseResponse['comments']): number => + comments?.reduce((total, comment) => { + if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) { + return total + (Array.isArray(comment.alertId) ? comment.alertId.length : 1); + } + return total; + }, 0) ?? 0; export const createIncident = async ({ actionsClient, @@ -152,22 +160,34 @@ export const createIncident = async ({ userActions .slice(latestPushInfo?.index ?? 0) .filter( - (action, index) => - Array.isArray(action.action_field) && action.action_field[0] === 'comment' + (action) => Array.isArray(action.action_field) && action.action_field[0] === 'comment' ) .map((action) => action.comment_id) ); - const commentsToBeUpdated = caseComments?.filter((comment) => - commentsIdsToBeUpdated.has(comment.id) + + const commentsToBeUpdated = caseComments?.filter( + (comment) => + // We push only user's comments + comment.type === CommentType.user && commentsIdsToBeUpdated.has(comment.id) ); + const totalAlerts = countAlerts(caseComments); + let comments: ExternalServiceComment[] = []; + if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) { const commentsMapping = mappings.find((m) => m.source === 'comments'); if (commentsMapping?.action_type !== 'nothing') { comments = transformComments(commentsToBeUpdated, ['informationAdded']); } } + + if (totalAlerts > 0) { + comments.push({ + comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`, + }); + } + return { incident, comments }; }; @@ -247,7 +267,13 @@ export const prepareFieldsForTransformation = ({ key: mapping.target, value: params[mapping.source] ?? '', actionType: mapping.action_type, - pipes: mapping.action_type === 'append' ? [...defaultPipes, 'append'] : defaultPipes, + pipes: + // Do not transform titles + mapping.source !== 'title' + ? mapping.action_type === 'append' + ? [...defaultPipes, 'append'] + : defaultPipes + : [], }, ] : acc, diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts index bf398d1ffcf407..c8501130493bac 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts @@ -170,7 +170,7 @@ describe('Push case', () => { parent: null, priority: 'High', labels: ['LOLBins'], - summary: 'Another bad one (created at 2019-11-25T22:32:17.947Z by elastic)', + summary: 'Another bad one', description: 'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)', externalId: null, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 59b64de3697452..1d75e873f9b183 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -187,272 +187,275 @@ export function LayerPanel( ]); return ( -

- - - - - - - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, + <> +
+ + + + - )} - - - - - {groups.map((group, groupIndex) => { - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - {group.groupLabel}} - labelType="legend" - key={group.groupId} - isInvalid={isMissing} - error={ - isMissing ? ( -
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - })} -
- ) : ( - [] - ) - } - > - <> - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; - return ( - -
- { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - }) - ); - removeButtonRef(id); - }} - > - - -
-
- ); - })} -
- {group.supportsMoreColumns ? ( - { - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: true, - }); - }} - onDrop={onDrop} - /> - ) : null} - -
- ); - })} - { - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); - } - } - setActiveDimension(initialActiveDimensionState); - }} - panel={ - <> - {activeGroup && activeId && ( + {layerDatasource && ( + { - if (shouldReplaceDimension || shouldRemoveDimension) { - props.updateAll( - datasourceId, - newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ - layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ - layerId, - groupId: activeGroup.groupId, - columnId: activeId, - prevState: props.visualizationState, - }) - ); - } else { - props.updateDatasource(datasourceId, newState); - } - setActiveDimension({ - ...activeDimension, - isNew: false, + layerId, + state: layerDatasourceState, + activeData: props.framePublicAPI.activeData, + setState: (updater: unknown) => { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); }); + + props.updateAll(datasourceId, newState, nextVisState); }, }} /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
- + )} + + + + + {groups.map((group, groupIndex) => { + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + {group.groupLabel}
} + labelType="legend" + key={group.groupId} + isInvalid={isMissing} + error={ + isMissing ? ( +
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', + })} +
+ ) : ( + [] + ) + } + > + <> + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + return ( + +
+ { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ + layerId, + columnId: id, + prevState: props.visualizationState, + }) + ); + removeButtonRef(id); + }} + > + + +
+
+ ); + })} +
+ {group.supportsMoreColumns ? ( + { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); }} + onDrop={onDrop} /> - - )} - - } - /> + ) : null} + + + ); + })} - + - - - - - -
-
+ + + + + +
+
+ + { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } + } + setActiveDimension(initialActiveDimensionState); + }} + panel={ + <> + {activeGroup && activeId && ( + { + if (shouldReplaceDimension || shouldRemoveDimension) { + props.updateAll( + datasourceId, + newState, + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } + setActiveDimension({ + ...activeDimension, + isNew: false, + }); + }, + }} + /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
+ +
+ )} + + } + /> + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 4c40282012d6db..a676b7283671ca 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { Capabilities, HttpSetup } from 'kibana/public'; +import { Capabilities, HttpSetup, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter/target/common'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { IndexPatternsContract, TimefilterContract, @@ -105,4 +106,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { parent ); } + + extract(state: EmbeddableStateWithType) { + let references: SavedObjectReference[] = []; + const typedState = (state as unknown) as LensEmbeddableInput; + + if ('attributes' in typedState && typedState.attributes !== undefined) { + references = typedState.attributes.references; + } + + return { state, references }; + } } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index b0390763054982..7e15bfa9a340e9 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { EmbeddableFactoryDefinition, IContainer, @@ -13,8 +14,10 @@ import { import '../index.scss'; import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; -import { MapByReferenceInput, MapEmbeddableInput } from './types'; +import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; +// @ts-expect-error +import { extractReferences } from '../../common/migrations/references'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; @@ -61,4 +64,16 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { parent ); }; + + extract(state: EmbeddableStateWithType) { + const maybeMapByValueInput = state as EmbeddableStateWithType | MapByValueInput; + + if ((maybeMapByValueInput as MapByValueInput).attributes !== undefined) { + const { references } = extractReferences(maybeMapByValueInput); + + return { state, references }; + } + + return { state, references: [] }; + } } diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index d97820f010a802..bfe450d240b08b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -444,13 +444,19 @@ export const threat_technique = t.intersection([ ]); export type ThreatTechnique = t.TypeOf; export const threat_techniques = t.array(threat_technique); -export const threat = t.exact( - t.type({ - framework: threat_framework, - tactic: threat_tactic, - technique: threat_techniques, - }) -); +export const threat = t.intersection([ + t.exact( + t.type({ + framework: threat_framework, + tactic: threat_tactic, + }) + ), + t.exact( + t.partial({ + technique: threat_techniques, + }) + ), +]); export type Threat = t.TypeOf; export const threats = t.array(threat); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts index 93094e34454889..f3bef5ad7445fb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -924,7 +924,7 @@ describe('add prepackaged rules schema', () => { expect(message.schema).toEqual({}); }); - test('You cannot send in an array of threat that are missing "technique"', () => { + test('You can send in an array of threat that are missing "technique"', () => { const payload: Omit & { threat: Array>>; } = { @@ -944,10 +944,21 @@ describe('add prepackaged rules schema', () => { const decoded = addPrepackagedRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + expect(message.schema).toEqual(expected); }); test('You can optionally send in an array of false positives', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts index a59c8736584117..2caedd2e01193b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -926,7 +926,7 @@ describe('import rules schema', () => { expect(message.schema).toEqual({}); }); - test('You cannot send in an array of threat that are missing "technique"', () => { + test('You can send in an array of threat that are missing "technique"', () => { const payload: Omit & { threat: Array>>; } = { @@ -946,10 +946,21 @@ describe('import rules schema', () => { const decoded = importRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + expect(message.schema).toEqual(expected); }); test('You can optionally send in an array of false positives', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts index 8cdb85a5554512..3dfa12acc29d59 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts @@ -973,7 +973,7 @@ describe('patch_rules_schema', () => { expect(message.schema).toEqual({}); }); - test('threat is invalid when updated with missing technique', () => { + test('threat is valid when updated with missing technique', () => { const threat: Omit = [ { framework: 'fake', @@ -993,10 +993,8 @@ describe('patch_rules_schema', () => { const decoded = patchRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); }); test('validates with timeline_id and timeline_title', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 6b8211b23088ca..70ff921d3b334f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -618,7 +618,7 @@ describe('create rules schema', () => { expect(message.schema).toEqual({}); }); - test('You cannot send in an array of threat that are missing "technique"', () => { + test('You can send in an array of threat that are missing "technique"', () => { const payload = { ...getCreateRulesSchemaMock(), threat: [ @@ -636,10 +636,8 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); }); test('You can optionally send in an array of false positives', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 966ce3098d6a7a..ef9c7f49cb371d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -60,12 +60,15 @@ import { } from '../../tasks/alerts'; import { changeRowsPerPageTo300, + duplicateFirstRule, + duplicateRuleFromMenu, filterByCustomRules, goToCreateNewRule, goToRuleDetails, waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; -import { cleanKibana } from '../../tasks/common'; +import { createCustomIndicatorRule } from '../../tasks/api_calls/rules'; +import { cleanKibana, reload } from '../../tasks/common'; import { createAndActivateRule, fillAboutRuleAndContinue, @@ -92,8 +95,10 @@ import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted, } from '../../tasks/create_new_rule'; +import { waitForKibana } from '../../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; @@ -465,5 +470,30 @@ describe('indicator match', () => { cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); }); }); + + describe('Duplicates the indicator rule', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + createCustomIndicatorRule(newThreatIndicatorRule); + reload(); + }); + + it('Allows the rule to be duplicated from the table', () => { + waitForKibana(); + duplicateFirstRule(); + cy.contains(RULE_NAME, `${newThreatIndicatorRule.name} [Duplicate]`); + }); + + it('Allows the rule to be duplicated from the edit screen', () => { + waitForKibana(); + goToRuleDetails(); + duplicateRuleFromMenu(); + goBackToAllRulesTable(); + reload(); + cy.contains(RULE_NAME, `${newThreatIndicatorRule.name} [Duplicate]`); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 68baad7d3d259a..30365c9bd4c708 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -17,6 +17,12 @@ export const DELETE_RULE_ACTION_BTN = '[data-test-subj="deleteRuleAction"]'; export const EDIT_RULE_ACTION_BTN = '[data-test-subj="editRuleAction"]'; +export const DUPLICATE_RULE_ACTION_BTN = '[data-test-subj="duplicateRuleAction"]'; + +export const DUPLICATE_RULE_MENU_PANEL_BTN = '[data-test-subj="rules-details-duplicate-rule"]'; + +export const REFRESH_BTN = '[data-test-subj="refreshRulesAction"] button'; + export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]'; export const ELASTIC_RULES_BTN = '[data-test-subj="showElasticRulesFilterButton"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 3553889449e6d9..529ef4afdfa637 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -31,6 +31,8 @@ import { RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE, rowsPerPageSelector, pageSelector, + DUPLICATE_RULE_ACTION_BTN, + DUPLICATE_RULE_MENU_PANEL_BTN, } from '../screens/alerts_detection_rules'; import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details'; @@ -45,6 +47,33 @@ export const editFirstRule = () => { cy.get(EDIT_RULE_ACTION_BTN).click(); }; +export const duplicateFirstRule = () => { + cy.get(COLLAPSED_ACTION_BTN).should('be.visible'); + cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); + cy.get(DUPLICATE_RULE_ACTION_BTN).should('be.visible'); + cy.get(DUPLICATE_RULE_ACTION_BTN).click(); +}; + +/** + * Duplicates the rule from the menu and does additional + * pipes and checking that the elements are present on the + * page as well as removed when doing the clicks to help reduce + * flake. + */ +export const duplicateRuleFromMenu = () => { + cy.get(ALL_ACTIONS).should('be.visible'); + cy.root() + .pipe(($el) => { + $el.find(ALL_ACTIONS).trigger('click'); + return $el.find(DUPLICATE_RULE_MENU_PANEL_BTN); + }) + .should(($el) => expect($el).to.be.visible); + // Because of a fade effect and fast clicking this can produce more than one click + cy.get(DUPLICATE_RULE_MENU_PANEL_BTN) + .pipe(($el) => $el.trigger('click')) + .should('not.be.visible'); +}; + export const deleteFirstRule = () => { cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); cy.get(DELETE_RULE_ACTION_BTN).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index ab6063f5809c4a..99f5bd9c20230c 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CustomRule } from '../../objects/rule'; +import { CustomRule, ThreatIndicatorRule } from '../../objects/rule'; export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing') => cy.request({ @@ -29,6 +29,44 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing') => failOnStatusCode: false, }); +export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: '10s', + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'threat_match', + threat_mapping: [ + { + entries: [ + { + field: rule.indicatorMapping, + type: 'mapping', + value: rule.indicatorMapping, + }, + ], + }, + ], + threat_query: '*:*', + threat_language: 'kuery', + threat_filters: [], + threat_index: ['mock*'], + threat_indicator_path: '', + from: 'now-17520h', + index: ['exceptions-*'], + query: rule.customQuery || '*:*', + language: 'kuery', + enabled: false, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + export const createCustomRuleActivated = (rule: CustomRule, ruleId = '1') => cy.request({ method: 'POST', diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts index 0867dc41eeb786..77c263385df0a9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts @@ -87,7 +87,7 @@ export const PRIORITY = i18n.translate( export const ALERT_FIELDS_LABEL = i18n.translate( 'xpack.securitySolution.components.connectors.serviceNow.alertFieldsTitle', { - defaultMessage: 'Fields associated with alerts', + defaultMessage: 'Select Observables to push', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 7e2da88a58f183..af3e4270568678 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -157,49 +157,54 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription : `${singleThreat.tactic.name} (${singleThreat.tactic.id})`} - {singleThreat.technique.map((technique, techniqueIndex) => { - const myTechnique = techniquesOptions.find((t) => t.id === technique.id); - return ( - - - {myTechnique != null - ? myTechnique.label - : `${technique.name} (${technique.id})`} - - - {technique.subtechnique != null && - technique.subtechnique.map((subtechnique, subtechniqueIndex) => { - const mySubtechnique = subtechniquesOptions.find( - (t) => t.id === subtechnique.id - ); - return ( - - { + const myTechnique = techniquesOptions.find((t) => t.id === technique.id); + return ( + + + {myTechnique != null + ? myTechnique.label + : `${technique.name} (${technique.id})`} + + + {technique.subtechnique != null && + technique.subtechnique.map((subtechnique, subtechniqueIndex) => { + const mySubtechnique = subtechniquesOptions.find( + (t) => t.id === subtechnique.id + ); + return ( + - {mySubtechnique != null - ? mySubtechnique.label - : `${subtechnique.name} (${subtechnique.id})`} - - - ); - })} - - - ); - })} + + {mySubtechnique != null + ? mySubtechnique.label + : `${subtechnique.name} (${subtechnique.id})`} + + + ); + })} + + + ); + })} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx index da18f282574525..2a083ef89ab19c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx @@ -8,7 +8,7 @@ import { getValidThreat } from '../../../mitre/valid_threat_mock'; import { hasSubtechniqueOptions } from './helpers'; -const mockTechniques = getValidThreat()[0].technique; +const mockTechniques = getValidThreat()[0].technique ?? []; describe('helpers', () => { describe('hasSubtechniqueOptions', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx index e3c771534bedab..d283c19bd13daa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx @@ -51,45 +51,46 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ const values = field.value as Threats; const technique = useMemo(() => { - return values[threatIndex].technique[techniqueIndex]; - }, [values, threatIndex, techniqueIndex]); + return [...(values[threatIndex].technique ?? [])]; + }, [values, threatIndex]); const removeSubtechnique = useCallback( (index: number) => { const threats = [...(field.value as Threats)]; - const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + const subtechniques = technique[techniqueIndex].subtechnique ?? []; if (subtechniques != null) { subtechniques.splice(index, 1); - threats[threatIndex].technique[techniqueIndex] = { - ...threats[threatIndex].technique[techniqueIndex], + technique[techniqueIndex] = { + ...technique[techniqueIndex], subtechnique: subtechniques, }; + threats[threatIndex].technique = technique; onFieldChange(threats); } }, - [field, threatIndex, onFieldChange, techniqueIndex] + [field, onFieldChange, techniqueIndex, technique, threatIndex] ); const addMitreAttackSubtechnique = useCallback(() => { const threats = [...(field.value as Threats)]; - const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + const subtechniques = technique[techniqueIndex].subtechnique; if (subtechniques != null) { - threats[threatIndex].technique[techniqueIndex] = { - ...threats[threatIndex].technique[techniqueIndex], + technique[techniqueIndex] = { + ...technique[techniqueIndex], subtechnique: [...subtechniques, { id: 'none', name: 'none', reference: 'none' }], }; } else { - threats[threatIndex].technique[techniqueIndex] = { - ...threats[threatIndex].technique[techniqueIndex], + technique[techniqueIndex] = { + ...technique[techniqueIndex], subtechnique: [{ id: 'none', name: 'none', reference: 'none' }], }; } - + threats[threatIndex].technique = technique; onFieldChange(threats); - }, [field, threatIndex, onFieldChange, techniqueIndex]); + }, [field, onFieldChange, techniqueIndex, technique, threatIndex]); const updateSubtechnique = useCallback( (index: number, value: string) => { @@ -99,7 +100,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ name: '', reference: '', }; - const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + const subtechniques = technique[techniqueIndex].subtechnique; if (subtechniques != null) { onFieldChange([ @@ -107,9 +108,9 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ { ...threats[threatIndex], technique: [ - ...threats[threatIndex].technique.slice(0, techniqueIndex), + ...technique.slice(0, techniqueIndex), { - ...threats[threatIndex].technique[techniqueIndex], + ...technique[techniqueIndex], subtechnique: [ ...subtechniques.slice(0, index), { @@ -120,19 +121,21 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ ...subtechniques.slice(index + 1), ], }, - ...threats[threatIndex].technique.slice(techniqueIndex + 1), + ...technique.slice(techniqueIndex + 1), ], }, ...threats.slice(threatIndex + 1), ]); } }, - [threatIndex, techniqueIndex, onFieldChange, field] + [threatIndex, techniqueIndex, onFieldChange, field, technique] ); const getSelectSubtechnique = useCallback( (index: number, disabled: boolean, subtechnique: ThreatSubtechnique) => { - const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id); + const options = subtechniquesOptions.filter( + (t) => t.techniqueId === technique[techniqueIndex].id + ); return ( <> @@ -166,13 +169,17 @@ export const MitreAttackSubtechniqueFields: React.FC = ({ ); }, - [field, updateSubtechnique, technique] + [field, updateSubtechnique, technique, techniqueIndex] ); + const subtechniques = useMemo(() => { + return technique[techniqueIndex].subtechnique; + }, [technique, techniqueIndex]); + return ( - {technique.subtechnique != null && - technique.subtechnique.map((subtechnique, index) => ( + {subtechniques != null && + subtechniques.map((subtechnique, index) => (
= ({ const removeTechnique = useCallback( (index: number) => { const threats = [...(field.value as Threats)]; - const techniques = threats[threatIndex].technique; + const techniques = threats[threatIndex].technique ?? []; techniques.splice(index, 1); threats[threatIndex] = { ...threats[threatIndex], @@ -73,7 +73,7 @@ export const MitreAttackTechniqueFields: React.FC = ({ threats[threatIndex] = { ...threats[threatIndex], technique: [ - ...threats[threatIndex].technique, + ...(threats[threatIndex].technique ?? []), { id: 'none', name: 'none', reference: 'none', subtechnique: [] }, ], }; @@ -88,19 +88,20 @@ export const MitreAttackTechniqueFields: React.FC = ({ name: '', reference: '', }; + const technique = threats[threatIndex].technique ?? []; onFieldChange([ ...threats.slice(0, threatIndex), { ...threats[threatIndex], technique: [ - ...threats[threatIndex].technique.slice(0, index), + ...technique.slice(0, index), { id, reference, name, subtechnique: [], }, - ...threats[threatIndex].technique.slice(index + 1), + ...technique.slice(index + 1), ], }, ...threats.slice(threatIndex + 1), @@ -147,9 +148,11 @@ export const MitreAttackTechniqueFields: React.FC = ({ [field, updateTechnique] ); + const techniques = values[threatIndex].technique ?? []; + return ( - {values[threatIndex].technique.map((technique, index) => ( + {techniques.map((technique, index) => (
{ history.push(getEditRuleUrl(rule.id)); @@ -41,7 +43,11 @@ export const duplicateRulesAction = async ( ) => { try { dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' }); - const response = await duplicateRules({ rules }); + const response = await duplicateRules({ + // We cast this back and forth here as the front end types are not really the right io-ts ones + // and the two types conflict with each other. + rules: rules.map((rule) => transformOutput(rule as CreateRulesSchema) as Rule), + }); const { errors } = bucketRulesResponse(response); if (errors.length > 0) { displayErrorToast( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index d2488bd3d043cd..d2eadef48d9c73 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -67,6 +67,7 @@ export const getActions = ( enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), }, { + 'data-test-subj': 'duplicateRuleAction', description: i18n.DUPLICATE_RULE, icon: 'copy', name: !actionsPrivileges ? ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 12e6d276c18d86..b8824d2b8798e1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -182,7 +182,7 @@ export const filterEmptyThreats = (threats: Threats): Threats => { .map((threat) => { return { ...threat, - technique: trimThreatsWithNoName(threat.technique).map((technique) => { + technique: trimThreatsWithNoName(threat.technique ?? []).map((technique) => { return { ...technique, subtechnique: diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx index d82f0769c8b74c..fb846d041bd17c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx @@ -123,6 +123,7 @@ export const DeleteActionModal: FC = ({ return ( = ({ closeModal, items, startAndC return ( 8.0.0) + * The release branch should match the release version (e.g., 7.x --> 7.0.0) + */ +export const mockKibanaVersion = '8.0.0'; +export const mockKibanaSemverVersion = new SemVer(mockKibanaVersion); diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index 91a19bfec3e81b..6d83bdc5f36e9e 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -94,7 +94,7 @@ export type ReindexSavedObject = SavedObject; export enum ReindexWarning { // 7.0 -> 8.0 warnings - apmReindex, + customTypeName, // 8.0 -> 9.0 warnings } diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx index ee722a39372169..b732f6806a388b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import SemVer from 'semver/classes/semver'; import { mountWithIntl } from '@kbn/test/jest'; import { httpServiceMock } from 'src/core/public/mocks'; +import { mockKibanaSemverVersion } from '../../../common/constants'; import { UpgradeAssistantTabs } from './tabs'; import { LoadingState } from './types'; @@ -18,7 +18,6 @@ import { OverviewTab } from './tabs/overview'; const promisesToResolve = () => new Promise((resolve) => setTimeout(resolve, 0)); const mockHttp = httpServiceMock.createSetupContract(); -const mockKibanaVersion = new SemVer('8.0.0'); jest.mock('../app_context', () => { return { @@ -29,9 +28,9 @@ jest.mock('../app_context', () => { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', }, kibanaVersionInfo: { - currentMajor: mockKibanaVersion.major, - prevMajor: mockKibanaVersion.major - 1, - nextMajor: mockKibanaVersion.major + 1, + currentMajor: mockKibanaSemverVersion.major, + prevMajor: mockKibanaSemverVersion.major - 1, + nextMajor: mockKibanaSemverVersion.major + 1, }, }; }, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx index 1ed1e0b01f65b4..bf890c856239e8 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import SemVer from 'semver/classes/semver'; +import { mockKibanaSemverVersion } from '../../../../../common/constants'; import { LoadingState } from '../../types'; import AssistanceData from '../__fixtures__/checkup_api_response.json'; @@ -22,8 +22,6 @@ const defaultProps = { setSelectedTabIndex: jest.fn(), }; -const mockKibanaVersion = new SemVer('8.0.0'); - jest.mock('../../../app_context', () => { return { useAppContext: () => { @@ -33,9 +31,9 @@ jest.mock('../../../app_context', () => { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', }, kibanaVersionInfo: { - currentMajor: mockKibanaVersion.major, - prevMajor: mockKibanaVersion.major - 1, - nextMajor: mockKibanaVersion.major + 1, + currentMajor: mockKibanaSemverVersion.major, + prevMajor: mockKibanaSemverVersion.major - 1, + nextMajor: mockKibanaSemverVersion.major + 1, }, }; }, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx index 67aa5d8b9d7dec..292887853e4b3b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx @@ -143,9 +143,11 @@ export class IndexDeprecationTable extends React.Component< private generateActionsColumn() { // NOTE: this naive implementation assumes all indices in the table are - // should show the reindex button. This should work for known usecases. + // should show the reindex button. This should work for known use cases. const { indices } = this.props; - if (!indices.find((i) => i.reindex === true)) { + const hasActionsColumn = Boolean(indices.find((i) => i.reindex === true)); + + if (hasActionsColumn === false) { return null; } diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap index d92db98ae40cb9..dba019550f2a1b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap @@ -23,30 +23,6 @@ exports[`WarningsFlyoutStep renders 1`] = `

- - } - documentationUrl="https://www.elastic.co/guide/en/observability/master/whats-new.html" - label={ - - } - onChange={[Function]} - warning={0} - /> { status: undefined, reindexTaskPercComplete: null, errorMessage: null, - reindexWarnings: [ReindexWarning.apmReindex], + reindexWarnings: [ReindexWarning.customTypeName], hasRequiredPrivileges: true, } as ReindexState, }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx index 9f76ef0aa78ba6..d365cd82ba86c5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx @@ -8,6 +8,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { mount, shallow } from 'enzyme'; import React from 'react'; +import { mockKibanaSemverVersion } from '../../../../../../../../common/constants'; import { ReindexWarning } from '../../../../../../../../common/types'; import { idForWarning, WarningsFlyoutStep } from './warnings_step'; @@ -20,6 +21,11 @@ jest.mock('../../../../../../app_context', () => { DOC_LINK_VERSION: 'current', ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', }, + kibanaVersionInfo: { + currentMajor: mockKibanaSemverVersion.major, + prevMajor: mockKibanaSemverVersion.major - 1, + nextMajor: mockKibanaSemverVersion.major + 1, + }, }; }, }; @@ -28,7 +34,7 @@ jest.mock('../../../../../../app_context', () => { describe('WarningsFlyoutStep', () => { const defaultProps = { advanceNextStep: jest.fn(), - warnings: [ReindexWarning.apmReindex], + warnings: [ReindexWarning.customTypeName], closeFlyout: jest.fn(), renderGlobalCallouts: jest.fn(), }; @@ -37,19 +43,21 @@ describe('WarningsFlyoutStep', () => { expect(shallow()).toMatchSnapshot(); }); - it('does not allow proceeding until all are checked', () => { - const wrapper = mount( - - - - ); - const button = wrapper.find('EuiButton'); - - button.simulate('click'); - expect(defaultProps.advanceNextStep).not.toHaveBeenCalled(); - - wrapper.find(`input#${idForWarning(ReindexWarning.apmReindex)}`).simulate('change'); - button.simulate('click'); - expect(defaultProps.advanceNextStep).toHaveBeenCalled(); - }); + if (mockKibanaSemverVersion.major === 7) { + it('does not allow proceeding until all are checked', () => { + const wrapper = mount( + + + + ); + const button = wrapper.find('EuiButton'); + + button.simulate('click'); + expect(defaultProps.advanceNextStep).not.toHaveBeenCalled(); + + wrapper.find(`input#${idForWarning(ReindexWarning.customTypeName)}`).simulate('change'); + button.simulate('click'); + expect(defaultProps.advanceNextStep).toHaveBeenCalled(); + }); + } }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx index 2e6b039a2fe764..f6620e4125c9a4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx @@ -10,6 +10,7 @@ import React, { useState } from 'react'; import { EuiButton, EuiButtonEmpty, + EuiCode, EuiCallOut, EuiCheckbox, EuiFlexGroup, @@ -100,9 +101,9 @@ export const WarningsFlyoutStep: React.FunctionComponent @@ -128,25 +129,31 @@ export const WarningsFlyoutStep: React.FunctionComponent - {warnings.includes(ReindexWarning.apmReindex) && ( + {kibanaVersionInfo.currentMajor === 7 && warnings.includes(ReindexWarning.customTypeName) && ( _doc, + }} /> } description={ _doc, + }} /> } - documentationUrl={`${observabilityDocBasePath}/master/whats-new.html`} + documentationUrl={`${esDocBasePath}/${DOC_LINK_VERSION}/removal-of-types.html`} /> )} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts index 6caad4f5050fc4..d93fe7920f1d79 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts @@ -5,16 +5,13 @@ * 2.0. */ -import { SemVer } from 'semver'; +import { mockKibanaSemverVersion } from '../../../common/constants'; -export const MOCK_VERSION_STRING = '8.0.0'; - -export const getMockVersionInfo = (versionString = MOCK_VERSION_STRING) => { - const currentVersion = new SemVer(versionString); - const currentMajor = currentVersion.major; +export const getMockVersionInfo = () => { + const currentMajor = mockKibanaSemverVersion.major; return { - currentVersion, + currentVersion: mockKibanaSemverVersion, currentMajor, prevMajor: currentMajor - 1, nextMajor: currentMajor + 1, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index 479a7475efd688..9ab8d0aa7cffb6 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -24,6 +24,7 @@ describe('getUpgradeAssistantStatus', () => { const resolvedIndices = { indices: fakeIndexNames.map((f) => ({ name: f, attributes: ['open'] })), }; + // @ts-expect-error mock data is too loosely typed const deprecationsResponse: DeprecationAPIResponse = _.cloneDeep(fakeDeprecations); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts index 25dcd2521525df..f4631f3ba459d3 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts @@ -9,7 +9,8 @@ import { SemVer } from 'semver'; import { IScopedClusterClient, kibanaResponseFactory } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; -import { MOCK_VERSION_STRING, getMockVersionInfo } from './__fixtures__/version'; +import { mockKibanaVersion } from '../../common/constants'; +import { getMockVersionInfo } from './__fixtures__/version'; import { esVersionCheck, @@ -97,7 +98,7 @@ describe('verifyAllMatchKibanaVersion', () => { describe('EsVersionPrecheck', () => { beforeEach(() => { - versionService.setup(MOCK_VERSION_STRING); + versionService.setup(mockKibanaVersion); }); it('returns a 403 when callCluster fails with a 403', async () => { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts index 609f36c25619e4..f778981b95054e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts @@ -5,8 +5,10 @@ * 2.0. */ +import { mockKibanaSemverVersion, mockKibanaVersion } from '../../../common/constants'; +import { ReindexWarning } from '../../../common/types'; import { versionService } from '../version'; -import { MOCK_VERSION_STRING, getMockVersionInfo } from '../__fixtures__/version'; +import { getMockVersionInfo } from '../__fixtures__/version'; import { generateNewIndexName, @@ -123,7 +125,7 @@ describe('transformFlatSettings', () => { describe('sourceNameForIndex', () => { beforeEach(() => { - versionService.setup(MOCK_VERSION_STRING); + versionService.setup(mockKibanaVersion); }); it('parses internal indices', () => { @@ -144,7 +146,7 @@ describe('sourceNameForIndex', () => { describe('generateNewIndexName', () => { beforeEach(() => { - versionService.setup(MOCK_VERSION_STRING); + versionService.setup(mockKibanaVersion); }); it('parses internal indices', () => { @@ -177,4 +179,26 @@ describe('getReindexWarnings', () => { }) ).toEqual([]); }); + + if (mockKibanaSemverVersion.major === 7) { + describe('customTypeName warning', () => { + it('returns customTypeName for non-_doc mapping types', () => { + expect( + getReindexWarnings({ + settings: {}, + mappings: { doc: {} }, + }) + ).toEqual([ReindexWarning.customTypeName]); + }); + + it('does not return customTypeName for _doc mapping types', () => { + expect( + getReindexWarnings({ + settings: {}, + mappings: { _doc: {} }, + }) + ).toEqual([]); + }); + }); + } }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts index 11cc01b69d3a5b..70e1992d5b3e95 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts @@ -8,8 +8,7 @@ import { flow, omit } from 'lodash'; import { ReindexWarning } from '../../../common/types'; import { versionService } from '../version'; -import { FlatSettings } from './types'; - +import { FlatSettings, FlatSettingsWithTypeName } from './types'; export interface ParsedIndexName { cleanIndexName: string; baseName: string; @@ -69,11 +68,24 @@ export const generateNewIndexName = (indexName: string): string => { * Returns an array of warnings that should be displayed to user before reindexing begins. * @param flatSettings */ -export const getReindexWarnings = (flatSettings: FlatSettings): ReindexWarning[] => { +export const getReindexWarnings = ( + flatSettings: FlatSettingsWithTypeName | FlatSettings +): ReindexWarning[] => { const warnings = [ // No warnings yet for 8.0 -> 9.0 ] as Array<[ReindexWarning, boolean]>; + if (versionService.getMajorVersion() === 7) { + const DEFAULT_TYPE_NAME = '_doc'; + // In 7+ it's not possible to have more than one type anyways, so always grab the first + // (and only) key. + const typeName = Object.getOwnPropertyNames(flatSettings.mappings)[0]; + + const typeNameWarning = Boolean(typeName && typeName !== DEFAULT_TYPE_NAME); + + warnings.push([ReindexWarning.customTypeName, typeNameWarning]); + } + return warnings.filter(([_, applies]) => applies).map(([warning, _]) => warning); }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 59c83a05aa5516..592c2d15b9c0c5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -19,9 +19,10 @@ import { ReindexStatus, ReindexStep, } from '../../../common/types'; +import { mockKibanaVersion } from '../../../common/constants'; import { versionService } from '../version'; import { LOCK_WINDOW, ReindexActions, reindexActionsFactory } from './reindex_actions'; -import { MOCK_VERSION_STRING, getMockVersionInfo } from '../__fixtures__/version'; +import { getMockVersionInfo } from '../__fixtures__/version'; const { currentMajor, prevMajor } = getMockVersionInfo(); @@ -53,7 +54,7 @@ describe('ReindexActions', () => { describe('createReindexOp', () => { beforeEach(() => { - versionService.setup(MOCK_VERSION_STRING); + versionService.setup(mockKibanaVersion); client.create.mockResolvedValue(); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index 738d54c6f6d4f3..fe8844b28e37a6 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -21,8 +21,9 @@ import { ReindexStatus, ReindexStep, } from '../../../common/types'; +import { versionService } from '../version'; import { generateNewIndexName } from './index_settings'; -import { FlatSettings } from './types'; +import { FlatSettings, FlatSettingsWithTypeName } from './types'; // TODO: base on elasticsearch.requestTimeout? export const LOCK_WINDOW = moment.duration(90, 'seconds'); @@ -85,7 +86,7 @@ export interface ReindexActions { * Retrieve index settings (in flat, dot-notation style) and mappings. * @param indexName */ - getFlatSettings(indexName: string): Promise; + getFlatSettings(indexName: string): Promise; // ----- Functions below are for enforcing locks around groups of indices like ML or Watcher @@ -237,18 +238,33 @@ export const reindexActionsFactory = ( }, async getFlatSettings(indexName: string) { - const { body: flatSettings } = await esClient.indices.get<{ - [indexName: string]: FlatSettings; - }>({ - index: indexName, - flat_settings: true, - }); + let flatSettings; + + if (versionService.getMajorVersion() === 7) { + // On 7.x, we need to get index settings with mapping type + flatSettings = await esClient.indices.get<{ + [indexName: string]: FlatSettingsWithTypeName; + }>({ + index: indexName, + flat_settings: true, + // This @ts-ignore is needed on master since the flag is deprecated on >7.x + // @ts-ignore + include_type_name: true, + }); + } else { + flatSettings = await esClient.indices.get<{ + [indexName: string]: FlatSettings; + }>({ + index: indexName, + flat_settings: true, + }); + } - if (!flatSettings[indexName]) { + if (!flatSettings.body[indexName]) { return null; } - return flatSettings[indexName]; + return flatSettings.body[indexName]; }, async _fetchAndLockIndexGroupDoc(indexGroup) { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index 69105465a04f0d..a91cf8ddeada9d 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -20,10 +20,11 @@ import { ReindexStatus, ReindexStep, } from '../../../common/types'; +import { mockKibanaVersion } from '../../../common/constants'; import { licensingMock } from '../../../../licensing/server/mocks'; import { LicensingPluginSetup } from '../../../../licensing/server'; -import { MOCK_VERSION_STRING, getMockVersionInfo } from '../__fixtures__/version'; +import { getMockVersionInfo } from '../__fixtures__/version'; import { esIndicesStateCheck } from '../es_indices_state_check'; import { versionService } from '../version'; @@ -88,7 +89,7 @@ describe('reindexService', () => { licensingPluginSetup ); - versionService.setup(MOCK_VERSION_STRING); + versionService.setup(mockKibanaVersion); }); describe('hasRequiredPrivileges', () => { @@ -215,7 +216,7 @@ describe('reindexService', () => { 'index.provided_name': indexName, }, mappings: { - properties: { https: { type: 'boolean' } }, + _doc: { properties: { https: { type: 'boolean' } } }, }, }); @@ -571,7 +572,10 @@ describe('reindexService', () => { const mlReindexedOp = { id: '2', - attributes: { ...reindexOp.attributes, indexName: '.reindexed-v7-ml-anomalies' }, + attributes: { + ...reindexOp.attributes, + indexName: `.reindexed-v${prevMajor}-ml-anomalies`, + }, } as ReindexSavedObject; const updatedOp = await service.processNextStep(mlReindexedOp); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 72bcb5330f8193..1b5f91e0c53b84 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -219,7 +219,7 @@ export const reindexServiceFactory = ( .cancel({ task_id: reindexOp.attributes.reindexTaskId ?? undefined, }) - .catch((e) => undefined); // Ignore any exceptions trying to cancel (it may have already completed). + .catch(() => undefined); // Ignore any exceptions trying to cancel (it may have already completed). } // Set index back to writable if we ever got past this point. @@ -347,6 +347,11 @@ export const reindexServiceFactory = ( await esClient.indices.open({ index: indexName }); } + const flatSettings = await actions.getFlatSettings(indexName); + if (!flatSettings) { + throw error.indexNotFound(`Index ${indexName} does not exist.`); + } + const { body: startReindexResponse } = await esClient.reindex({ refresh: true, wait_for_completion: false, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts index b24625a8c2a9dc..569316e276e430 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts @@ -27,3 +27,16 @@ export interface FlatSettings { _meta?: MetaProperties; }; } + +// Specific to 7.x-8 upgrade +export interface FlatSettingsWithTypeName { + settings: { + [key: string]: string; + }; + mappings: { + [typeName: string]: { + properties?: MappingProperties; + _meta?: MetaProperties; + }; + }; +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts index 82d039ab9413ab..21dded346bbd33 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts @@ -89,7 +89,9 @@ describe('reindex API', () => { mockReindexService.findReindexOperation.mockResolvedValueOnce({ attributes: { indexName: 'wowIndex', status: ReindexStatus.inProgress }, }); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce([ReindexWarning.apmReindex]); + mockReindexService.detectReindexWarnings.mockResolvedValueOnce([ + ReindexWarning.customTypeName, + ]); const resp = await routeDependencies.router.getHandler({ method: 'get', diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx index 9fcd946df2f847..befe53219a4494 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx @@ -68,16 +68,16 @@ export const StepDetail: React.FC = ({ }) => { return ( <> - + - +

{stepName}

- + = ({ - + = (item) => { - return {item.name}; + return ( + + {item.name} + + ); }; interface Props { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx index c746a5cc63a9b2..9a66b586d1d561 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx @@ -9,18 +9,25 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartProps } from './waterfall_chart'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; interface LegendProps { items: Required['legendItems']; render: Required['renderLegendItem']; } +const StyledFlexItem = euiStyled(EuiFlexItem)` + margin-right: ${(props) => props.theme.eui.paddingSizes.m}; + max-width: 7%; + min-width: 160px; +`; + export const Legend: React.FC = ({ items, render }) => { return ( - - {items.map((item, index) => { - return {render(item, index)}; - })} + + {items.map((item, index) => ( + {render(item, index)} + ))} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index 59990b29db5dbe..119c907f76ca10 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -120,8 +120,12 @@ export const WaterfallChart = ({ - - + + {shouldRenderSidebar && } ) { @@ -188,6 +191,40 @@ export function defineRoutes(core: CoreSetup) { } ); + router.put( + { + path: '/api/alerts_fixture/{id}/reset_task_status', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + status: schema.string(), + }), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + const { id } = req.params; + const { status } = req.body; + + const [{ savedObjects }] = await core.getStartServices(); + const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, { + includedHiddenTypes: ['task', 'alert'], + }); + const alert = await savedObjectsWithTasksAndAlerts.get('alert', id); + const result = await savedObjectsWithTasksAndAlerts.update( + 'task', + alert.attributes.scheduledTaskId!, + { status } + ); + return res.ok({ body: result }); + } + ); + router.get( { path: '/api/alerts_fixture/api_keys_pending_invalidation', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index c1f65fab3669e8..e8cc8ea699e17f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -11,8 +11,7 @@ import { setupSpacesAndUsers, tearDown } from '..'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { describe('Alerts', () => { - // FLAKY: https://github.com/elastic/kibana/issues/86952 - describe.skip('legacy alerts', () => { + describe('legacy alerts', () => { before(async () => { await setupSpacesAndUsers(getService); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index ef5914965ddce8..3db35653747400 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -77,6 +77,7 @@ export default function alertTests({ getService }: FtrProviderContext) { case 'space_1_all at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': + await resetTaskStatus(migratedAlertId); await ensureLegacyAlertHasBeenMigrated(migratedAlertId); await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); @@ -92,6 +93,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await ensureAlertIsRunning(); break; case 'global_read at space1': + await resetTaskStatus(migratedAlertId); await ensureLegacyAlertHasBeenMigrated(migratedAlertId); await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); @@ -115,6 +117,7 @@ export default function alertTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all_alerts_none_actions at space1': + await resetTaskStatus(migratedAlertId); await ensureLegacyAlertHasBeenMigrated(migratedAlertId); await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); @@ -140,6 +143,21 @@ export default function alertTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } + async function resetTaskStatus(alertId: string) { + // occasionally when the task manager starts running while the alert saved objects + // are mid-migration, the task will fail and set its status to "failed". this prevents + // the alert from running ever again and downstream tasks that depend on successful alert + // execution will fail. this ensures the task status is set to "idle" so the + // task manager will continue claiming and executing it. + await supertest + .put(`${getUrlPrefix(space.id)}/api/alerts_fixture/${alertId}/reset_task_status`) + .set('kbn-xsrf', 'foo') + .send({ + status: 'idle', + }) + .expect(200); + } + async function ensureLegacyAlertHasBeenMigrated(alertId: string) { const getResponse = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`) diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index d804f0ef14cf8b..665c126e00a017 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -159,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.filterWithSearchString(testData.originalConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); - await transform.table.assertTransformRowActions(false); + await transform.table.assertTransformRowActions(testData.originalConfig.id, false); await transform.testExecution.logTestStep('should display the define pivot step'); await transform.table.clickTransformRowAction('Clone'); diff --git a/x-pack/test/functional/apps/transform/deleting.ts b/x-pack/test/functional/apps/transform/deleting.ts new file mode 100644 index 00000000000000..bdba06454c5c25 --- /dev/null +++ b/x-pack/test/functional/apps/transform/deleting.ts @@ -0,0 +1,136 @@ +/* + * 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 { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { getLatestTransformConfig, getPivotTransformConfig } from './index'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('deleting', function () { + const PREFIX = 'deleting'; + + const testDataList = [ + { + suiteTitle: 'batch transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, false), + expected: { + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'batch', + progress: 100, + }, + }, + }, + { + suiteTitle: 'continuous transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, true), + expected: { + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'continuous', + progress: undefined, + }, + }, + }, + { + suiteTitle: 'batch transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX), + transformDescription: 'updated description', + transformDocsPerSecond: '1000', + transformFrequency: '10m', + expected: { + messageText: 'updated transform.', + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'batch', + progress: 100, + }, + }, + }, + ]; + + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + for (const testData of testDataList) { + await transform.api.createAndRunTransform( + testData.originalConfig.id, + testData.originalConfig + ); + } + + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + for (const testData of testDataList) { + await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index); + await transform.api.deleteIndices(testData.originalConfig.dest.index); + } + await transform.api.cleanTransformIndices(); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + it('delete transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + if (testData.expected.row.mode === 'continuous') { + await transform.testExecution.logTestStep('should have the delete action disabled'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Delete', + false + ); + + await transform.testExecution.logTestStep('should stop the transform'); + await transform.table.clickTransformRowActionWithRetry( + testData.originalConfig.id, + 'Stop' + ); + } + + await transform.testExecution.logTestStep('should display the stopped transform'); + await transform.table.assertTransformRowFields(testData.originalConfig.id, { + id: testData.originalConfig.id, + description: testData.originalConfig.description, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + + await transform.testExecution.logTestStep('should show the delete modal'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Delete', + true + ); + await transform.table.clickTransformRowActionWithRetry( + testData.originalConfig.id, + 'Delete' + ); + await transform.table.assertTransformDeleteModalExists(); + + await transform.testExecution.logTestStep('should delete the transform'); + await transform.table.confirmDeleteTransform(); + await transform.table.assertTransformRowNotExists(testData.originalConfig.id); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 71a7cf02df1fdc..1f0bb058bdc381 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.filterWithSearchString(testData.originalConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); - await transform.table.assertTransformRowActions(false); + await transform.table.assertTransformRowActions(testData.originalConfig.id, false); await transform.testExecution.logTestStep('should show the edit flyout'); await transform.table.clickTransformRowAction('Edit'); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 63d8d0b51bc8cb..1440f0a3f9a091 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -6,7 +6,10 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformLatestConfig } from '../../../../plugins/transform/common/types/transform'; +import { + TransformLatestConfig, + TransformPivotConfig, +} from '../../../../plugins/transform/common/types/transform'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -41,6 +44,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./cloning')); loadTestFile(require.resolve('./editing')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./deleting')); + loadTestFile(require.resolve('./starting')); }); } export interface ComboboxOption { @@ -80,20 +85,46 @@ export function isLatestTransformTestData(arg: any): arg is LatestTransformTestD return arg.type === 'latest'; } -export function getLatestTransformConfig(): TransformLatestConfig { +export function getPivotTransformConfig( + prefix: string, + continuous?: boolean +): TransformPivotConfig { const timestamp = Date.now(); return { - id: `ec_cloning_2_${timestamp}`, + id: `ec_${prefix}_pivot_${timestamp}_${continuous ? 'cont' : 'batch'}`, + source: { index: ['ft_ecommerce'] }, + pivot: { + group_by: { category: { terms: { field: 'category.keyword' } } }, + aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, + }, + description: `ecommerce ${ + continuous ? 'continuous' : 'batch' + } transform with avg(products.base_price) grouped by terms(category.keyword)`, + dest: { index: `user-ec_2_${timestamp}` }, + ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}), + }; +} + +export function getLatestTransformConfig( + prefix: string, + continuous?: boolean +): TransformLatestConfig { + const timestamp = Date.now(); + return { + id: `ec_${prefix}_latest_${timestamp}_${continuous ? 'cont' : 'batch'}`, source: { index: ['ft_ecommerce'] }, latest: { unique_key: ['category.keyword'], sort: 'order_date', }, - description: 'ecommerce batch transform with category unique key and sorted by order date', + description: `ecommerce ${ + continuous ? 'continuous' : 'batch' + } transform with category unique key and sorted by order date`, frequency: '3s', settings: { max_page_search_size: 250, }, dest: { index: `user-ec_3_${timestamp}` }, + ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}), }; } diff --git a/x-pack/test/functional/apps/transform/starting.ts b/x-pack/test/functional/apps/transform/starting.ts new file mode 100644 index 00000000000000..4b0b6f8dade666 --- /dev/null +++ b/x-pack/test/functional/apps/transform/starting.ts @@ -0,0 +1,106 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; +import { getLatestTransformConfig, getPivotTransformConfig } from './index'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('starting', function () { + const PREFIX = 'starting'; + const testDataList = [ + { + suiteTitle: 'batch transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, false), + mode: 'batch', + }, + { + suiteTitle: 'continuous transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, true), + mode: 'continuous', + }, + { + suiteTitle: 'batch transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX, false), + mode: 'batch', + }, + { + suiteTitle: 'continuous transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX, true), + mode: 'continuous', + }, + ]; + + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + for (const testData of testDataList) { + await transform.api.createTransform(testData.originalConfig.id, testData.originalConfig); + } + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + for (const testData of testDataList) { + await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index); + await transform.api.deleteIndices(testData.originalConfig.dest.index); + } + + await transform.api.cleanTransformIndices(); + }); + + for (const testData of testDataList) { + const transformId = testData.originalConfig.id; + + describe(`${testData.suiteTitle}`, function () { + it('start transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + await transform.testExecution.logTestStep( + 'should display the original transform in the transform list' + ); + await transform.table.filterWithSearchString(transformId, 1); + + await transform.testExecution.logTestStep('should start the transform'); + await transform.table.assertTransformRowActionEnabled(transformId, 'Start', true); + await transform.table.clickTransformRowActionWithRetry(transformId, 'Start'); + await transform.table.confirmStartTransform(); + await transform.table.clearSearchString(testDataList.length); + + if (testData.mode === 'continuous') { + await transform.testExecution.logTestStep('should display the started transform'); + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.STOPPED + ); + } else { + await transform.table.assertTransformRowProgressGreaterThan(transformId, 0); + } + + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.FAILED + ); + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.ABORTING + ); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/services/transform/management.ts b/x-pack/test/functional/services/transform/management.ts index fdfd1d1d9b40f0..807c3d49e344cc 100644 --- a/x-pack/test/functional/services/transform/management.ts +++ b/x-pack/test/functional/services/transform/management.ts @@ -5,8 +5,11 @@ * 2.0. */ +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; +export type TransformManagement = ProvidedType; + export function TransformManagementProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 72626580e9461a..ce2625677e4795 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -12,6 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformTableProvider({ getService }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const browser = getService('browser'); return new (class TransformTable { public async parseTransformTable() { @@ -129,21 +130,63 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { const filteredRows = rows.filter((row) => row.id === filter); expect(filteredRows).to.have.length( expectedRowCount, - `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + `Filtered Transform table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` ); } - public async assertTransformRowFields(transformId: string, expectedRow: object) { + public async clearSearchString(expectedRowCount: number = 1) { + await this.waitForTransformsToLoad(); + const tableListContainer = await testSubjects.find('transformListTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); const rows = await this.parseTransformTable(); - const transformRow = rows.filter((row) => row.id === transformId)[0]; - expect(transformRow).to.eql( - expectedRow, - `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( - transformRow - )}')` + expect(rows).to.have.length( + expectedRowCount, + `Transform table should have ${expectedRowCount} row(s) after clearing search' (got '${rows.length}')` ); } + public async assertTransformRowFields(transformId: string, expectedRow: object) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow).to.eql( + expectedRow, + `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( + transformRow + )}')` + ); + }); + } + + public async assertTransformRowProgressGreaterThan( + transformId: string, + expectedProgress: number + ) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow.progress).to.greaterThan( + 0, + `Expected transform row progress to be greater than '${expectedProgress}' (got '${transformRow.progress}')` + ); + }); + } + + public async assertTransformRowStatusNotEql(transformId: string, status: string) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow.status).to.not.eql( + status, + `Expected transform row status to not be '${status}' (got '${transformRow.status}')` + ); + }); + } + public async assertTransformExpandedRow() { await testSubjects.click('transformListRowDetailsToggle'); @@ -185,8 +228,13 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } - public async assertTransformRowActions(isTransformRunning = false) { - await testSubjects.click('euiCollapsedItemActionsButton'); + public rowSelector(transformId: string, subSelector?: string) { + const row = `~transformListTable > ~row-${transformId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async assertTransformRowActions(transformId: string, isTransformRunning = false) { + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); await testSubjects.existOrFail('transformActionClone'); await testSubjects.existOrFail('transformActionDelete'); @@ -201,6 +249,42 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { } } + public async assertTransformRowActionEnabled( + transformId: string, + action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit', + expectedValue: boolean + ) { + const selector = `transformAction${action}`; + await retry.tryForTime(60 * 1000, async () => { + await this.refreshTransformList(); + + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); + + await testSubjects.existOrFail(selector); + const isEnabled = await testSubjects.isEnabled(selector); + expect(isEnabled).to.eql( + expectedValue, + `Expected '${action}' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }); + } + + public async clickTransformRowActionWithRetry( + transformId: string, + action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit' + ) { + await retry.tryForTime(30 * 1000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); + await testSubjects.existOrFail(`transformAction${action}`); + await testSubjects.click(`transformAction${action}`); + await testSubjects.missingOrFail(`transformAction${action}`); + }); + } + public async clickTransformRowAction(action: string) { await testSubjects.click(`transformAction${action}`); } @@ -214,5 +298,53 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await this.waitForTransformsExpandedRowPreviewTabToLoad(); await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values); } + + public async assertTransformDeleteModalExists() { + await testSubjects.existOrFail('transformDeleteModal', { timeout: 60 * 1000 }); + } + + public async assertTransformDeleteModalNotExists() { + await testSubjects.missingOrFail('transformDeleteModal', { timeout: 60 * 1000 }); + } + + public async assertTransformStartModalExists() { + await testSubjects.existOrFail('transformStartModal', { timeout: 60 * 1000 }); + } + + public async assertTransformStartModalNotExists() { + await testSubjects.missingOrFail('transformStartModal', { timeout: 60 * 1000 }); + } + + public async confirmDeleteTransform() { + await retry.tryForTime(30 * 1000, async () => { + await this.assertTransformDeleteModalExists(); + await testSubjects.click('transformDeleteModal > confirmModalConfirmButton'); + await this.assertTransformDeleteModalNotExists(); + }); + } + + public async assertTransformRowNotExists(transformId: string) { + await retry.tryForTime(30 * 1000, async () => { + // If after deletion, and there's no transform left + const noTransformsFoundMessageExists = await testSubjects.exists( + 'transformNoTransformsFound' + ); + + if (noTransformsFoundMessageExists) { + return true; + } else { + // Checks that the tranform was deleted + await this.filterWithSearchString(transformId, 0); + } + }); + } + + public async confirmStartTransform() { + await retry.tryForTime(30 * 1000, async () => { + await this.assertTransformStartModalExists(); + await testSubjects.click('transformStartModal > confirmModalConfirmButton'); + await this.assertTransformStartModalNotExists(); + }); + } })(); } diff --git a/yarn.lock b/yarn.lock index 4a3399ece1fd0c..92c67cda974c3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2107,6 +2107,14 @@ enabled "2.0.x" kuler "^2.0.0" +"@dsherret/to-absolute-glob@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c" + integrity sha1-H2R13IvZdM6gei2vOGSzF7HdMyw= + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + "@elastic/apm-rum-core@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.7.0.tgz#2213987285324781e2ebeca607f3a71245da5a84" @@ -3428,6 +3436,10 @@ version "0.0.0" uid "" +"@kbn/docs-utils@link:packages/kbn-docs-utils": + version "0.0.0" + uid "" + "@kbn/es-archiver@link:packages/kbn-es-archiver": version "0.0.0" uid "" @@ -3484,10 +3496,6 @@ version "0.0.0" uid "" -"@kbn/release-notes@link:packages/kbn-release-notes": - version "0.0.0" - uid "" - "@kbn/std@link:packages/kbn-std": version "0.0.0" uid "" @@ -5134,6 +5142,18 @@ dependencies: "@babel/runtime" "^7.10.2" +"@ts-morph/common@~0.7.0": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.7.3.tgz#380020c278e4aa6cecedf362a1157591d1003267" + integrity sha512-M6Tcu0EZDLL8Ht7WAYz7yJfDZ9eArhqR8XZ9Mk3q8jwU6MKFAttrw3JtW4JhneqTz7pZMv4XaimEdXI0E4K4rg== + dependencies: + "@dsherret/to-absolute-glob" "^2.0.2" + fast-glob "^3.2.4" + is-negated-glob "^1.0.0" + mkdirp "^1.0.4" + multimatch "^5.0.0" + typescript "~4.1.2" + "@turf/along@6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/along/-/along-6.0.1.tgz#595cecdc48fc7fcfa83c940a8e3eb24d4c2e04d4" @@ -10561,6 +10581,11 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +code-block-writer@^10.1.1: + version "10.1.1" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f" + integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw== + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -19991,10 +20016,10 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-ok@^0.1.1: version "0.1.1" @@ -21176,6 +21201,17 @@ multimatch@^4.0.0: arrify "^2.0.1" minimatch "^3.0.4" +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + multiparty@^4.1.2: version "4.2.1" resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.1.tgz#d9b6c46d8b8deab1ee70c734b0af771dd46e0b13" @@ -28444,6 +28480,15 @@ ts-log@2.1.4: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.1.4.tgz#063c5ad1cbab5d49d258d18015963489fb6fb59a" integrity sha512-P1EJSoyV+N3bR/IWFeAqXzKPZwHpnLY6j7j58mAvewHRipo+BQM2Y1f9Y9BjEQznKwgqqZm7H8iuixmssU7tYQ== +ts-morph@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-9.1.0.tgz#10d2088387c71f3c674f82492a3cec1e3538f0dd" + integrity sha512-sei4u651MBenr27sD6qLDXN3gZ4thiX71E3qV7SuVtDas0uvK2LtgZkIYUf9DKm/fLJ6AB/+yhRJ1vpEBJgy7Q== + dependencies: + "@dsherret/to-absolute-glob" "^2.0.2" + "@ts-morph/common" "~0.7.0" + code-block-writer "^10.1.1" + ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -28648,7 +28693,7 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@4.1.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.5.3, typescript@~3.7.2: +typescript@4.1.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.5.3, typescript@~3.7.2, typescript@~4.1.2: version "4.1.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==