From aa67c800ce74539caceb48c907dc34a592cd8668 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 23 Jul 2024 11:44:32 -0400 Subject: [PATCH] chore(investigate): Add investigate-app plugin from poc (#188122) --- .github/CODEOWNERS | 1 + docs/developer/plugin-list.asciidoc | 4 + package.json | 2 + packages/deeplinks/observability/constants.ts | 2 + .../src/strict_keys_rt/index.test.ts | 28 ++ .../src/to_boolean_rt/index.ts | 15 +- packages/kbn-optimizer/limits.yml | 1 + src/dev/storybook/aliases.ts | 1 + .../collectors/application_usage/schema.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 131 ++++++ tsconfig.base.json | 2 + x-pack/.i18nrc.json | 1 + .../.storybook/extend_props.ts | 34 ++ .../get_mock_investigate_app_services.tsx | 111 +++++ .../investigate_app/.storybook/jest_setup.js | 11 + .../investigate_app/.storybook/main.js | 8 + .../.storybook/mock_kibana_services.ts | 16 + .../investigate_app/.storybook/preview.js | 13 + .../.storybook/storybook_decorator.tsx | 18 + .../investigate_app/README.md | 0 .../investigate_app/jest.config.js | 24 ++ .../investigate_app/kibana.jsonc | 32 ++ .../investigate_app/public/api/index.ts | 63 +++ .../investigate_app/public/application.tsx | 63 +++ .../add_widget_ui/index.stories.tsx | 66 +++ .../public/components/add_widget_ui/index.tsx | 111 +++++ .../public/components/error_message/index.tsx | 21 + .../esql_widget_preview.tsx | 245 +++++++++++ .../esql_widget_control/index.stories.tsx | 36 ++ .../components/esql_widget_control/index.tsx | 107 +++++ .../components/grid_item/index.stories.tsx | 79 ++++ .../public/components/grid_item/index.tsx | 219 ++++++++++ .../index.tsx | 19 + .../components/investigate_page_template.tsx | 39 ++ .../investigate_text_button/index.tsx | 74 ++++ .../investigate_view/index.stories.tsx | 29 ++ .../components/investigate_view/index.tsx | 246 +++++++++++ .../investigate_widget_grid/index.stories.tsx | 144 +++++++ .../investigate_widget_grid/index.tsx | 390 ++++++++++++++++++ .../investigate_widget_grid/styles.scss | 1 + .../investigation_history/index.stories.tsx | 96 +++++ .../investigation_history/index.tsx | 181 ++++++++ .../components/note_widget/index.stories.tsx | 56 +++ .../public/components/note_widget/index.tsx | 32 ++ .../components/note_widget_control/index.tsx | 83 ++++ .../preview_lens_suggestion/index.stories.tsx | 30 ++ .../preview_lens_suggestion/index.tsx | 63 +++ .../components/resizable_text_input/index.tsx | 79 ++++ .../index.stories.tsx | 78 ++++ .../suggest_visualization_list/index.tsx | 138 +++++++ .../suggestions.mock.tsx | 384 +++++++++++++++++ .../components/timeline_message/index.tsx | 133 ++++++ .../workflow_blocks_control/index.stories.tsx | 77 ++++ .../workflow_blocks_control/index.tsx | 160 +++++++ .../public/constants/add_widget_mode.ts | 11 + .../investigate_app/public/constants/index.ts | 10 + .../public/hooks/use_abort_signal.ts | 21 + .../public/hooks/use_breakpoints.ts | 47 +++ .../public/hooks/use_date_range.ts | 59 +++ .../public/hooks/use_investigate_params.ts | 14 + .../public/hooks/use_investigate_router.ts | 55 +++ .../public/hooks/use_kibana.ts | 23 ++ .../public/hooks/use_local_storage.ts | 50 +++ .../hooks/use_memo_with_abort_signal.ts | 25 ++ .../investigate_app/public/hooks/use_theme.ts | 12 + .../workflow_blocks/use_workflow_blocks.tsx | 32 ++ .../investigate_app/public/index.ts | 26 ++ .../investigate_app/public/plugin.tsx | 148 +++++++ .../investigate_app/public/routes/config.tsx | 58 +++ .../investigate_app/public/services/esql.ts | 131 ++++++ .../investigate_app/public/services/types.ts | 12 + .../investigate_app/public/types.ts | 65 +++ .../public/utils/find_scrollable_parent.ts | 20 + .../get_data_table_from_esql_response.ts | 42 ++ .../utils/get_es_filter_from_overrides.ts | 36 ++ .../public/utils/get_kibana_columns.ts | 21 + .../utils/get_lens_attrs_for_suggestion.ts | 55 +++ .../get_overrides_from_global_parameters.tsx | 82 ++++ .../create_embeddable_widget.ts | 13 + .../register_embeddable_widget.tsx | 213 ++++++++++ .../public/widgets/embeddable_widget/types.ts | 18 + .../esql_widget/get_date_histogram_results.ts | 54 +++ .../esql_widget/register_esql_widget.tsx | 307 ++++++++++++++ .../widgets/note_widget/create_note_widget.ts | 12 + .../public/widgets/note_widget/index.tsx | 48 +++ .../public/widgets/note_widget/types.ts | 16 + .../public/widgets/register_widgets.ts | 28 ++ .../investigate_app/server/config.ts | 14 + .../investigate_app/server/index.ts | 28 ++ .../investigate_app/server/plugin.ts | 64 +++ .../create_investigate_app_server_route.ts | 16 + ...investigate_app_server_route_repository.ts | 14 + .../server/routes/register_routes.ts | 31 ++ .../investigate_app/server/routes/types.ts | 60 +++ .../investigate_app/server/types.ts | 18 + .../investigate_app/tsconfig.json | 56 +++ yarn.lock | 38 ++ 97 files changed, 6100 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/observability_solution/investigate_app/.storybook/extend_props.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/.storybook/jest_setup.js create mode 100644 x-pack/plugins/observability_solution/investigate_app/.storybook/main.js create mode 100644 x-pack/plugins/observability_solution/investigate_app/.storybook/mock_kibana_services.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/.storybook/preview.js create mode 100644 x-pack/plugins/observability_solution/investigate_app/.storybook/storybook_decorator.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/README.md create mode 100644 x-pack/plugins/observability_solution/investigate_app/jest.config.js create mode 100644 x-pack/plugins/observability_solution/investigate_app/kibana.jsonc create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/api/index.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/application.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/error_message/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/esql_widget_preview.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_app_context_provider/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_page_template.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_text_button/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/styles.scss create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/note_widget_control/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/resizable_text_input/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/suggestions.mock.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/timeline_message/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.stories.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/constants/add_widget_mode.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/constants/index.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_abort_signal.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_breakpoints.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_date_range.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_params.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_router.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_kibana.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_local_storage.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_memo_with_abort_signal.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/use_theme.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/hooks/workflow_blocks/use_workflow_blocks.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/index.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/routes/config.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/services/esql.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/services/types.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/types.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/utils/find_scrollable_parent.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/utils/get_data_table_from_esql_response.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/utils/get_es_filter_from_overrides.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/utils/get_kibana_columns.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/utils/get_overrides_from_global_parameters.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/create_embeddable_widget.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/register_embeddable_widget.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/types.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/get_date_histogram_results.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/register_esql_widget.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/create_note_widget.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/index.tsx create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/types.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/public/widgets/register_widgets.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/config.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/index.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/plugin.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/routes/create_investigate_app_server_route.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/routes/register_routes.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/server/types.ts create mode 100644 x-pack/plugins/observability_solution/investigate_app/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e08bcbeff8ef61..70f86e2f52680a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -507,6 +507,7 @@ x-pack/plugins/integration_assistant @elastic/security-solution src/plugins/interactive_setup @elastic/kibana-security test/interactive_setup_api_integration/plugins/test_endpoints @elastic/kibana-security packages/kbn-interpreter @elastic/kibana-visualizations +x-pack/plugins/observability_solution/investigate_app @elastic/obs-ai-assistant x-pack/plugins/observability_solution/investigate @elastic/obs-ux-management-team packages/kbn-io-ts-utils @elastic/obs-knowledge-team packages/kbn-ipynb @elastic/search-kibana diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 18102c1224431e..aff3a490c8d9b0 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -647,6 +647,10 @@ the infrastructure monitoring use-case within Kibana. |undefined +|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/investigate_app/README.md[investigateApp] +|undefined + + |{kib-repo}blob/{branch}/x-pack/plugins/kubernetes_security/README.md[kubernetesSecurity] |This plugin provides interactive visualizations of your Kubernetes workload and session data. diff --git a/package.json b/package.json index 3d204cf5bae76c..704732dffe64e4 100644 --- a/package.json +++ b/package.json @@ -545,6 +545,7 @@ "@kbn/interactive-setup-plugin": "link:src/plugins/interactive_setup", "@kbn/interactive-setup-test-endpoints-plugin": "link:test/interactive_setup_api_integration/plugins/test_endpoints", "@kbn/interpreter": "link:packages/kbn-interpreter", + "@kbn/investigate-app-plugin": "link:x-pack/plugins/observability_solution/investigate_app", "@kbn/investigate-plugin": "link:x-pack/plugins/observability_solution/investigate", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/ipynb": "link:packages/kbn-ipynb", @@ -1058,6 +1059,7 @@ "he": "^1.2.0", "history": "^4.9.0", "hjson": "3.2.1", + "html2canvas": "^1.4.1", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "i18n-iso-countries": "^4.3.1", diff --git a/packages/deeplinks/observability/constants.ts b/packages/deeplinks/observability/constants.ts index dd440dc10b6387..339bc7b85514be 100644 --- a/packages/deeplinks/observability/constants.ts +++ b/packages/deeplinks/observability/constants.ts @@ -24,6 +24,8 @@ export const SLO_APP_ID = 'slo'; export const AI_ASSISTANT_APP_ID = 'observabilityAIAssistant'; +export const INVESTIGATE_APP_ID = 'investigate'; + export const OBLT_UX_APP_ID = 'ux'; export const OBLT_PROFILING_APP_ID = 'profiling'; diff --git a/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts index 6bca714e81a8ab..3751e3b2674af2 100644 --- a/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts +++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts @@ -12,6 +12,7 @@ import { strictKeysRt } from '.'; import { jsonRt } from '../json_rt'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { isoToEpochRt } from '../iso_to_epoch_rt'; +import { toBooleanRt } from '../to_boolean_rt'; describe('strictKeysRt', () => { it('correctly and deeply validates object keys', () => { @@ -238,6 +239,33 @@ describe('strictKeysRt', () => { }); }); + it('deals with union types', () => { + const type = t.intersection([ + t.type({ + required: t.string, + }), + t.partial({ + disable: t.union([ + toBooleanRt, + t.type({ + except: t.array(t.string), + }), + ]), + }), + ]); + + const value = { + required: 'required', + disable: { + except: ['foo'], + }, + }; + + const asStrictType = strictKeysRt(type); + + expect(isRight(asStrictType.decode(value))).toBe(true); + }); + it('does not support piped types', () => { const typeA = t.type({ query: t.type({ filterNames: jsonRt.pipe(t.array(t.string)) }), diff --git a/packages/kbn-io-ts-utils/src/to_boolean_rt/index.ts b/packages/kbn-io-ts-utils/src/to_boolean_rt/index.ts index bf2ca188e24d6e..29e30623defac0 100644 --- a/packages/kbn-io-ts-utils/src/to_boolean_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/to_boolean_rt/index.ts @@ -8,10 +8,23 @@ import * as t from 'io-ts'; +export function isPrimitive(value: unknown): value is string | number | boolean | null | undefined { + return ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' || + value === null || + value === undefined + ); +} + export const toBooleanRt = new t.Type( 'ToBoolean', t.boolean.is, - (input) => { + (input, context) => { + if (!isPrimitive(input)) { + return t.failure(input, context); + } let value: boolean; if (typeof input === 'string') { value = input === 'true'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 4ea1c79eaad3a9..b044ba8e093f41 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -86,6 +86,7 @@ pageLoadAssetSize: integrationAssistant: 19524 interactiveSetup: 80000 investigate: 17970 + investigateApp: 91898 kibanaOverview: 56279 kibanaReact: 74422 kibanaUsageCollection: 16463 diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 3e7efefc1b8591..5b5c7d2606cdf5 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -44,6 +44,7 @@ export const storybookAliases = { grouping: 'packages/kbn-grouping/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/observability_solution/infra/.storybook', + investigate: 'x-pack/plugins/observability_solution/investigate_app/.storybook', kibana_react: 'src/plugins/kibana_react/.storybook', lists: 'x-pack/plugins/lists/.storybook', logs_explorer: 'x-pack/plugins/observability_solution/logs_explorer/.storybook', diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 9d14c3f1f73172..0b856b639a5617 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -152,6 +152,7 @@ export const applicationUsageSchema = { fleet: commonSchema, integrations: commonSchema, ingestManager: commonSchema, + investigate: commonSchema, lens: commonSchema, maps: commonSchema, ml: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c0689afef492d0..805b290de5f762 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -4325,6 +4325,137 @@ } } }, + "investigate": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "lens": { "properties": { "appId": { diff --git a/tsconfig.base.json b/tsconfig.base.json index ab36ec64b49628..8ab42d494bb8b1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1008,6 +1008,8 @@ "@kbn/interactive-setup-test-endpoints-plugin/*": ["test/interactive_setup_api_integration/plugins/test_endpoints/*"], "@kbn/interpreter": ["packages/kbn-interpreter"], "@kbn/interpreter/*": ["packages/kbn-interpreter/*"], + "@kbn/investigate-app-plugin": ["x-pack/plugins/observability_solution/investigate_app"], + "@kbn/investigate-app-plugin/*": ["x-pack/plugins/observability_solution/investigate_app/*"], "@kbn/investigate-plugin": ["x-pack/plugins/observability_solution/investigate"], "@kbn/investigate-plugin/*": ["x-pack/plugins/observability_solution/investigate/*"], "@kbn/io-ts-utils": ["packages/kbn-io-ts-utils"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index fbee9ec63f29f1..d0a336369ed29c 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -55,6 +55,7 @@ "xpack.ingestPipelines": "plugins/ingest_pipelines", "xpack.integrationAssistant": "plugins/integration_assistant", "xpack.investigate": "plugins/observability_solution/investigate", + "xpack.investigateApp": "plugins/observability_solution/investigate_app", "xpack.kubernetesSecurity": "plugins/kubernetes_security", "xpack.lens": "plugins/lens", "xpack.licenseApiGuard": "plugins/license_api_guard", diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/extend_props.ts b/x-pack/plugins/observability_solution/investigate_app/.storybook/extend_props.ts new file mode 100644 index 00000000000000..b3dc16667cff5e --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/extend_props.ts @@ -0,0 +1,34 @@ +/* + * 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 { isPlainObject, mergeWith } from 'lodash'; + +type DeepOverwrite = T extends Record + ? Omit & { + [TKey in keyof U]: T extends Record ? DeepOverwrite : U[TKey]; + } + : U; + +type DeepPartialPlainObjects = T extends Record + ? Partial<{ + [TKey in keyof T]: DeepPartialPlainObjects; + }> + : T; + +function mergePlainObjectsOnly(val: T, src: U): DeepOverwrite { + if (isPlainObject(src)) { + return mergeWith({}, val, src, mergePlainObjectsOnly) as DeepOverwrite; + } + return src as DeepOverwrite; +} + +export function extendProps< + T extends Record | undefined, + U extends DeepPartialPlainObjects +>(props: T, extension: U): DeepOverwrite { + return mergePlainObjectsOnly(props, extension); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx b/x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx new file mode 100644 index 00000000000000..13e2c008648e5e --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo } from 'react'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; +import type { ESQLSearchResponse } from '@kbn/es-types'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { SearchBar, IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { merge } from 'lodash'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { EsqlQueryMeta } from '../public/services/esql'; +import type { InvestigateAppServices } from '../public/services/types'; +import { InvestigateAppKibanaContext } from '../public/hooks/use_kibana'; + +export function getMockInvestigateAppContext(): DeeplyMockedKeys { + const services: DeeplyMockedKeys = { + esql: { + meta: jest.fn().mockImplementation((): Promise => { + return Promise.resolve({ + suggestions: [], + columns: [], + dataView: {} as DataView, + }); + }), + query: jest.fn().mockImplementation((): Promise => { + return Promise.resolve({ + values: [], + columns: [], + }); + }), + queryWithMeta: jest + .fn() + .mockImplementation((): Promise<{ meta: EsqlQueryMeta; query: ESQLSearchResponse }> => { + return Promise.resolve({ + meta: { + suggestions: [], + columns: [], + dataView: {} as DataView, + }, + query: { + values: [], + columns: [], + }, + }); + }), + }, + }; + + const core = coreMock.createStart(); + + const dataMock = merge({}, dataPluginMock.createStartContract(), { + query: { + savedQueries: {}, + timefilter: { + timefilter: { + getTime: () => ({ from: 'now-15m', to: 'now', mode: 'relative' }), + }, + }, + }, + }); + + return { + core: core as any, + dependencies: { + start: { + data: dataMock, + unifiedSearch: merge({}, unifiedSearchPluginMock.createStartContract(), { + ui: { + SearchBar: function SearchBarWithContext(props: {}) { + const unifiedSearchServices = useMemo(() => { + return { + data: dataMock, + storage: new Storage(window.localStorage), + uiSettings: core.uiSettings, + } as unknown as IUnifiedSearchPluginServices; + }, []); + return ( + + + + ); + }, + }, + }), + embeddable: merge({}, embeddablePluginMock.createStartContract(), { + getEmbeddableFactories: () => [ + { + canCreateNew: () => true, + getDisplayName: () => 'Alerts', + type: 'alerts', + }, + ], + }), + investigate: {}, + lens: {}, + observabilityShared: {}, + dataViews: dataViewPluginMocks.createStartContract(), + }, + } as any, + services, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/jest_setup.js b/x-pack/plugins/observability_solution/investigate_app/.storybook/jest_setup.js new file mode 100644 index 00000000000000..32071b8aa3f628 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/jest_setup.js @@ -0,0 +1,11 @@ +/* + * 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 { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/main.js b/x-pack/plugins/observability_solution/investigate_app/.storybook/main.js new file mode 100644 index 00000000000000..86b48c32f103e3 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/main.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/mock_kibana_services.ts b/x-pack/plugins/observability_solution/investigate_app/.storybook/mock_kibana_services.ts new file mode 100644 index 00000000000000..e9f1b2b40ef407 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/mock_kibana_services.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { setKibanaServices } from '@kbn/esql/public/kibana_services'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks'; + +setKibanaServices( + coreMock.createStart(), + dataViewPluginMocks.createStartContract(), + expressionsPluginMock.createStartContract() +); diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/preview.js b/x-pack/plugins/observability_solution/investigate_app/.storybook/preview.js new file mode 100644 index 00000000000000..c8155e9c3d92ce --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/preview.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common'; +import * as jest from 'jest-mock'; + +window.jest = jest; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/storybook_decorator.tsx b/x-pack/plugins/observability_solution/investigate_app/.storybook/storybook_decorator.tsx new file mode 100644 index 00000000000000..e43c9b146b5145 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/storybook_decorator.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ComponentType, useMemo } from 'react'; +import { InvestigateAppContextProvider } from '../public/components/investigate_app_context_provider'; +import { getMockInvestigateAppContext } from './get_mock_investigate_app_services'; + +export function KibanaReactStorybookDecorator(Story: ComponentType) { + const context = useMemo(() => getMockInvestigateAppContext(), []); + return ( + + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/README.md b/x-pack/plugins/observability_solution/investigate_app/README.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/x-pack/plugins/observability_solution/investigate_app/jest.config.js b/x-pack/plugins/observability_solution/investigate_app/jest.config.js new file mode 100644 index 00000000000000..b37e5c69d46359 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: [ + '/x-pack/plugins/observability_solution/investigate_app/public', + '/x-pack/plugins/observability_solution/investigate_app/server', + ], + setupFiles: [ + '/x-pack/plugins/observability_solution/investigate_app/.storybook/jest_setup.js', + ], + collectCoverage: true, + collectCoverageFrom: [ + '/x-pack/plugins/observability_solution/investigate_app/{public,server}/**/*.{js,ts,tsx}', + ], + + coverageReporters: ['html'], +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc new file mode 100644 index 00000000000000..56e76e9fa73aa3 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc @@ -0,0 +1,32 @@ +{ + "type": "plugin", + "id": "@kbn/investigate-app-plugin", + "owner": "@elastic/obs-ai-assistant", + "plugin": { + "id": "investigateApp", + "server": true, + "browser": true, + "configPath": ["xpack", "investigateApp"], + "requiredPlugins": [ + "investigate", + "observabilityAIAssistant", + "observabilityShared", + "lens", + "dataViews", + "data", + "embeddable", + "contentManagement", + "datasetQuality", + "unifiedSearch", + "security", + ], + "requiredBundles": [ + "kibanaReact", + "kibanaUtils", + "esql", + "esqlDataGrid", + ], + "optionalPlugins": [], + "extraPublicDirs": [] + } +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/api/index.ts b/x-pack/plugins/observability_solution/investigate_app/public/api/index.ts new file mode 100644 index 00000000000000..c79fe1e0cdd033 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/api/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, CoreStart, HttpFetchOptions } from '@kbn/core/public'; +import type { + ClientRequestParamsOf, + ReturnOf, + RouteRepositoryClient, +} from '@kbn/server-route-repository'; +import { formatRequest } from '@kbn/server-route-repository/src/format_request'; +import type { InvestigateAppServerRouteRepository } from '../../server'; + +type FetchOptions = Omit & { + body?: any; +}; + +export type InvestigateAppAPIClientOptions = Omit< + FetchOptions, + 'query' | 'body' | 'pathname' | 'signal' +> & { + signal: AbortSignal | null; +}; + +export type InvestigateAppAPIClient = RouteRepositoryClient< + InvestigateAppServerRouteRepository, + InvestigateAppAPIClientOptions +>; + +export type AutoAbortedInvestigateAppAPIClient = RouteRepositoryClient< + InvestigateAppServerRouteRepository, + Omit +>; + +export type InvestigateAppAPIEndpoint = keyof InvestigateAppServerRouteRepository; + +export type APIReturnType = ReturnOf< + InvestigateAppServerRouteRepository, + TEndpoint +>; + +export type InvestigateAppAPIClientRequestParamsOf = + ClientRequestParamsOf; + +export function createCallInvestigateAppAPI(core: CoreStart | CoreSetup) { + return ((endpoint, options) => { + const { params } = options as unknown as { + params?: Partial>; + }; + + const { method, pathname, version } = formatRequest(endpoint, params?.path); + + return core.http[method](pathname, { + ...options, + body: params && params.body ? JSON.stringify(params.body) : undefined, + query: params?.query, + version, + }); + }) as InvestigateAppAPIClient; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/application.tsx b/x-pack/plugins/observability_solution/investigate_app/public/application.tsx new file mode 100644 index 00000000000000..9b40b1523fefec --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/application.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { CoreStart, CoreTheme } from '@kbn/core/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import type { History } from 'history'; +import React, { useMemo } from 'react'; +import type { Observable } from 'rxjs'; +import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; +import type { InvestigateAppStartDependencies } from './types'; +import { investigateRouter } from './routes/config'; +import { InvestigateAppKibanaContext } from './hooks/use_kibana'; +import { InvestigateAppServices } from './services/types'; +import { InvestigateAppContextProvider } from './components/investigate_app_context_provider'; + +function Application({ + coreStart, + history, + pluginsStart, + theme$, + services, +}: { + coreStart: CoreStart; + history: History; + pluginsStart: InvestigateAppStartDependencies; + theme$: Observable; + services: InvestigateAppServices; +}) { + const theme = useMemo(() => { + return { theme$ }; + }, [theme$]); + + const context: InvestigateAppKibanaContext = useMemo( + () => ({ + core: coreStart, + dependencies: { + start: pluginsStart, + }, + services, + }), + [coreStart, pluginsStart, services] + ); + + return ( + + + + + + + + + + + + ); +} + +export { Application }; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.stories.tsx new file mode 100644 index 00000000000000..b5e0da6ce1aa6a --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.stories.tsx @@ -0,0 +1,66 @@ +/* + * 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 { Meta, StoryObj } from '@storybook/react'; +import moment from 'moment'; +import React from 'react'; +import { InvestigationRevision } from '@kbn/investigate-plugin/common'; +import { AddWidgetUI as Component } from '.'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +interface Args { + props: React.ComponentProps; +} + +type StoryMeta = Meta; +type Story = StoryObj; + +const meta: StoryMeta = { + component: Component, + title: 'app/Molecules/AddWidgetUI', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +const defaultStory: Story = { + args: { + props: { + start: moment().subtract(15, 'minutes'), + end: moment(), + onWidgetAdd: async () => {}, + revision: { + items: [], + } as unknown as InvestigationRevision, + user: { + username: 'johndoe', + full_name: 'John Doe', + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + timeRange: { + from: moment().subtract(15, 'minutes').toISOString(), + to: moment().toISOString(), + }, + workflowBlocks: [], + }, + }, + render: function Render(args) { + return ; + }, +}; + +export const InvestigateSearchBarStory: Story = { + ...defaultStory, + args: { + ...defaultStory.args, + }, + name: 'default', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.tsx new file mode 100644 index 00000000000000..04f2c5f6a316cc --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/add_widget_ui/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { AuthenticatedUser } from '@kbn/core/public'; +import type { + GlobalWidgetParameters, + InvestigateWidgetCreate, + InvestigationRevision, + OnWidgetAdd, + WorkflowBlock, +} from '@kbn/investigate-plugin/public'; +import { assertNever } from '@kbn/std'; +import { Moment } from 'moment'; +import React, { useState } from 'react'; +import { AddWidgetMode } from '../../constants/add_widget_mode'; +import { useWorkflowBlocks } from '../../hooks/workflow_blocks/use_workflow_blocks'; +import { EsqlWidgetControl } from '../esql_widget_control'; +import { NoteWidgetControl } from '../note_widget_control'; + +type AddWidgetUIProps = { + user: Pick; + onWidgetAdd: OnWidgetAdd; + revision: InvestigationRevision; + start: Moment; + end: Moment; + workflowBlocks: WorkflowBlock[]; +} & GlobalWidgetParameters; + +function getControlsForMode({ + user, + mode, + onWidgetAdd, + revision, + start, + end, + query, + timeRange, + filters, +}: { + user: Pick; + mode: AddWidgetMode; + onWidgetAdd: (widget: InvestigateWidgetCreate) => Promise; + revision: InvestigationRevision; + start: Moment; + end: Moment; +} & GlobalWidgetParameters) { + switch (mode) { + case AddWidgetMode.Esql: + return ( + + ); + + case AddWidgetMode.Note: + return ; + + default: + assertNever(mode); + } +} + +export function AddWidgetUI({ + user, + onWidgetAdd, + revision, + start, + end, + query, + filters, + timeRange, + workflowBlocks, +}: AddWidgetUIProps) { + const [mode] = useState(AddWidgetMode.Note); + + const workflowBlocksControl = useWorkflowBlocks({ + start: start.toISOString(), + end: end.toISOString(), + dynamicBlocks: workflowBlocks, + isTimelineEmpty: revision.items.length === 0, + onWidgetAdd, + }); + + return ( + + {workflowBlocksControl ? ( + {workflowBlocksControl} + ) : null} + + {getControlsForMode({ + mode, + onWidgetAdd, + revision, + start, + end, + query, + filters, + timeRange, + user, + })} + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/error_message/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/error_message/index.tsx new file mode 100644 index 00000000000000..01d3d23ee4eb39 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/error_message/index.tsx @@ -0,0 +1,21 @@ +/* + * 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 { EuiErrorBoundary } from '@elastic/eui'; +import React from 'react'; + +function ThrowError({ error }: { error: Error }) { + throw error; + return <>; +} + +export function ErrorMessage({ error }: { error: Error }) { + return ( + + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/esql_widget_preview.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/esql_widget_preview.tsx new file mode 100644 index 00000000000000..078e61fef45cc5 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/esql_widget_preview.tsx @@ -0,0 +1,245 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { + InvestigateWidgetColumnSpan, + InvestigateWidgetCreate, + WorkflowBlock, +} from '@kbn/investigate-plugin/public'; +import { + createEsqlWidget, + ESQL_WIDGET_NAME, + GlobalWidgetParameters, + OnWidgetAdd, +} from '@kbn/investigate-plugin/public'; +import type { Suggestion } from '@kbn/lens-plugin/public'; +import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; +import { noop } from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; +import type { ESQLColumn, ESQLRow } from '@kbn/es-types'; +import { css } from '@emotion/css'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { useKibana } from '../../hooks/use_kibana'; +import { getEsFilterFromOverrides } from '../../utils/get_es_filter_from_overrides'; +import { EsqlWidget } from '../../widgets/esql_widget/register_esql_widget'; +import { SuggestVisualizationList } from '../suggest_visualization_list'; +import { ErrorMessage } from '../error_message'; +import { getDateHistogramResults } from '../../widgets/esql_widget/get_date_histogram_results'; + +function getWidgetFromSuggestion({ + query, + suggestion, +}: { + query: string; + suggestion: Suggestion; +}): InvestigateWidgetCreate { + const makeItWide = suggestion.visualizationId !== 'lnsMetric'; + + const makeItTall = suggestion.visualizationId !== 'lnsMetric'; + + let rows = makeItTall ? 12 : 4; + + if (suggestion.visualizationId === 'lnsDatatable') { + rows = 18; + } + + return createEsqlWidget({ + title: suggestion.title, + type: ESQL_WIDGET_NAME, + parameters: { + esql: query, + suggestion, + }, + columns: makeItWide ? InvestigateWidgetColumnSpan.Four : InvestigateWidgetColumnSpan.One, + rows, + locked: false, + }); +} + +function PreviewContainer({ children }: { children: React.ReactNode }) { + return ( + div { + width: 100%; + } + `} + > + {children} + + ); +} + +export function EsqlWidgetPreview({ + esqlQuery, + onWidgetAdd, + filters, + timeRange, + query, +}: { + esqlQuery: string; + onWidgetAdd: OnWidgetAdd; +} & GlobalWidgetParameters) { + const { + services: { esql }, + } = useKibana(); + + const filter = useMemo(() => { + return getEsFilterFromOverrides({ + filters, + timeRange, + query, + }); + }, [filters, timeRange, query]); + + const [selectedSuggestion, setSelectedSuggestion] = useState(undefined); + + const queryResult = useAbortableAsync( + async ({ signal }) => { + return await esql.queryWithMeta({ signal, query: esqlQuery, filter }).then((result) => { + setSelectedSuggestion((prevSuggestion) => { + const mostSimilarSuggestion = + result.meta.suggestions.find( + (suggestion) => suggestion.visualizationId === prevSuggestion?.visualizationId + ) || result.meta.suggestions[0]; + return mostSimilarSuggestion; + }); + return result; + }); + }, + [esqlQuery, filter, esql] + ); + + const dateHistoResponse = useAbortableAsync( + ({ signal }) => { + if (!queryResult.value || queryResult.loading || !selectedSuggestion) { + return undefined; + } + return getDateHistogramResults({ + columns: queryResult.value.query.columns, + esql, + filter, + query: esqlQuery, + signal, + suggestion: selectedSuggestion, + timeRange, + }); + }, + [queryResult, esql, filter, esqlQuery, selectedSuggestion, timeRange] + ); + + const fakeRenderApi = useMemo(() => { + return { + blocks: { + publish: (_blocks: WorkflowBlock[]) => { + return noop; + }, + }, + }; + }, []); + + const [displayedProps, setDisplayedProps] = useState< + { + error: Error | undefined; + loading: boolean; + } & ( + | { + value: { + columns: ESQLColumn[]; + values: ESQLRow[]; + allColumns?: ESQLColumn[]; + dataView: DataView; + suggestions: Array; + }; + } + | { value: undefined } + ) + >({ + loading: true, + value: undefined, + error: undefined, + }); + + useEffect(() => { + setDisplayedProps((prevDisplayedProps) => { + if (queryResult.loading) { + return { + ...prevDisplayedProps, + loading: true, + error: undefined, + }; + } + + return { + error: queryResult.error, + loading: queryResult.loading, + value: queryResult.value + ? { + columns: queryResult.value.query.columns, + values: queryResult.value.query.values, + allColumns: queryResult.value.query.all_columns, + dataView: queryResult.value.meta.dataView, + suggestions: queryResult.value.meta.suggestions, + } + : undefined, + }; + }); + }, [queryResult]); + + if (displayedProps.error) { + return ( + + + + ); + } + + if (!displayedProps.value || !selectedSuggestion) { + return ( + + + + ); + } + + return ( + + + + + + + + { + onWidgetAdd(getWidgetFromSuggestion({ query: esqlQuery, suggestion })); + }} + loading={queryResult.loading} + onMouseLeave={() => {}} + onSuggestionRollOver={(suggestion) => { + setSelectedSuggestion(suggestion); + }} + /> + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.stories.tsx new file mode 100644 index 00000000000000..e36c8b6867114f --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.stories.tsx @@ -0,0 +1,36 @@ +/* + * 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 { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import React from 'react'; +import { EsqlWidgetControl as Component } from '.'; +import '../../../.storybook/mock_kibana_services'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +const meta: ComponentMeta = { + component: Component, + title: 'app/Organisms/EsqlControlWidget', + decorators: [KibanaReactStorybookDecorator], +}; + +function WithContainer(props: React.ComponentProps) { + return ( +
+ +
+ ); +} + +export default meta; + +const defaultProps: ComponentStoryObj = { + render: WithContainer, +}; + +export const EsqlControlStory: ComponentStoryObj = { + ...defaultProps, + name: 'default', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.tsx new file mode 100644 index 00000000000000..789acadc01d141 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/esql_widget_control/index.tsx @@ -0,0 +1,107 @@ +/* + * 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 { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { GlobalWidgetParameters, OnWidgetAdd } from '@kbn/investigate-plugin/public'; +import { TextBasedLangEditor } from '@kbn/esql/public'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EsqlWidgetPreview } from './esql_widget_preview'; + +const editorContainerClassName = css` + .kibanaCodeEditor { + width: 100%; + } + + .monaco-editor { + position: absolute !important; + } + > div { + margin: 0; + } +`; + +type EsqlWidgetControlProps = { + onWidgetAdd: OnWidgetAdd; +} & GlobalWidgetParameters; + +export function EsqlWidgetControl({ + onWidgetAdd, + filters, + timeRange, + query, +}: EsqlWidgetControlProps) { + const [isExpanded, setIsExpanded] = useState(false); + + const [esqlQuery, setEsqlQuery] = useState('FROM *'); + + const [submittedEsqlQuery, setSubmittedEsqlQuery] = useState(esqlQuery); + + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + + return ( + + + + { + setIsPreviewOpen(nextIsOpen); + }} + buttonContent={ + +

+ {i18n.translate('xpack.investigateApp.esqlWidgetControl.previewResultsLabel', { + defaultMessage: 'Preview results', + })} +

+
+ } + > + { + setIsPreviewOpen(false); + return onWidgetAdd(widget); + }} + /> +
+
+
+ + { + setIsPreviewOpen(true); + setEsqlQuery(nextQuery.esql); + }} + onTextLangQuerySubmit={async (nextSubmittedQuery) => { + setSubmittedEsqlQuery(nextSubmittedQuery?.esql ?? ''); + }} + errors={undefined} + warning={undefined} + expandCodeEditor={(expanded: boolean) => { + setIsExpanded(() => expanded); + }} + isCodeEditorExpanded={isExpanded} + hideMinimizeButton={false} + editorIsInline + hideRunQueryText + isLoading={false} + disableSubmitAction + isDisabled={false} + hideQueryHistory + hideTimeFilterInfo + /> + +
+ ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.stories.tsx new file mode 100644 index 00000000000000..aff1eab5143512 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.stories.tsx @@ -0,0 +1,79 @@ +/* + * 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 { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { v4 } from 'uuid'; +import { GridItem as Component } from '.'; +import { extendProps } from '../../../.storybook/extend_props'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +type Props = React.ComponentProps; + +interface Args { + props: Partial & { id: string; children: React.ReactNode }; +} + +type StoryMeta = Meta; +type Story = StoryObj; + +const meta: StoryMeta = { + component: Component, + title: 'app/Molecules/GridItem', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +const defaultProps: Story = { + args: { + props: { + id: v4(), + children: <>TODO, + }, + }, + render: ({ props }) => { + return ( +
+ {}} + onDelete={() => {}} + onLockToggle={() => {}} + onOverrideRemove={async () => {}} + onTitleChange={() => {}} + overrides={[]} + title="My visualization" + description="A long description" + onEditClick={() => {}} + {...props} + /> +
+ ); + }, +}; + +export const GridItemStory: Story = { + ...defaultProps, + args: { + props: extendProps(defaultProps.args!.props!, { + title: 'A widget title', + children: <>TODO, + description: + 'An even longer description that should flow off screen especially if there are overrides defined', + overrides: [ + { + id: 'query', + label: `service.name:opbeans-java AND service.environment:(production OR development)`, + }, + ], + }), + }, + name: 'default', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.tsx new file mode 100644 index 00000000000000..df418271fac224 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.tsx @@ -0,0 +1,219 @@ +/* + * 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useTheme } from '../../hooks/use_theme'; +import { InvestigateTextButton } from '../investigate_text_button'; +import { InvestigateWidgetGridItemOverride } from '../investigate_widget_grid'; + +export const GRID_ITEM_HEADER_HEIGHT = 40; + +interface GridItemProps { + id: string; + title: string; + description: string; + children: React.ReactNode; + locked: boolean; + onCopy: () => void; + onTitleChange: (title: string) => void; + onDelete: () => void; + onLockToggle: () => void; + loading: boolean; + faded: boolean; + onOverrideRemove: (override: InvestigateWidgetGridItemOverride) => Promise; + onEditClick: () => void; + overrides: InvestigateWidgetGridItemOverride[]; +} + +const editTitleButtonClassName = `investigateGridItemTitleEditButton`; + +const titleContainerClassName = css` + overflow: hidden; +`; +const titleItemClassName = css` + max-width: 100%; + .euiText { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +`; + +const fadedClassName = css` + opacity: 0.5 !important; +`; + +const lockedControlClassName = css` + opacity: 0.9 !important; + &:hover { + opacity: 1 !important; + } +`; + +const panelContainerClassName = css` + overflow: clip; + overflow-clip-margin: 20px; +`; + +const panelClassName = css` + overflow-y: auto; +`; + +const panelContentClassName = css` + overflow-y: auto; + height: 100%; + > [data-shared-item] { + height: 100%; + } +`; + +const headerClassName = css` + height: ${GRID_ITEM_HEADER_HEIGHT}px; +`; + +const changeBadgeClassName = css` + max-width: 96px; + .euiText { + text-overflow: ellipsis; + overflow: hidden; + } +`; + +export function GridItem({ + id, + title, + description, + children, + locked, + onLockToggle, + onDelete, + onCopy, + loading, + faded, + overrides, + onOverrideRemove, + onEditClick, +}: GridItemProps) { + const theme = useTheme(); + + const containerClassName = css` + height: 100%; + max-width: 100%; + transition: opacity ${theme.animation.normal} ${theme.animation.resistance}; + overflow: auto; + + &:not(:hover) .${editTitleButtonClassName} { + opacity: 0; + } + `; + + return ( + + + + + + {title} + + + + {overrides.length ? ( + + {overrides.map((override) => ( + + { + onOverrideRemove(override); + }} + iconOnClickAriaLabel={i18n.translate( + 'xpack.investigateApp.gridItem.removeOverrideButtonAriaLabel', + { + defaultMessage: 'Remove filter', + } + )} + > + {override.label} + + + ))} + + ) : null} + + + + + { + onCopy(); + }} + disabled={loading} + /> + + + { + onDelete(); + }} + disabled={loading} + /> + + + { + onEditClick(); + }} + disabled={loading} + /> + + + { + onLockToggle(); + }} + disabled={loading} + /> + + + + + + + +
{children}
+
+
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_app_context_provider/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_app_context_provider/index.tsx new file mode 100644 index 00000000000000..45110331f9b4f9 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_app_context_provider/index.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { InvestigateAppKibanaContext } from '../../hooks/use_kibana'; + +export function InvestigateAppContextProvider({ + context, + children, +}: { + context: InvestigateAppKibanaContext; + children: React.ReactNode; +}) { + return {children}; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_page_template.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_page_template.tsx new file mode 100644 index 00000000000000..8e9d731366e437 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_page_template.tsx @@ -0,0 +1,39 @@ +/* + * 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 { css } from '@emotion/css'; +import React from 'react'; +import { useKibana } from '../hooks/use_kibana'; + +const pageSectionContentClassName = css` + width: 100%; + display: flex; + flex-grow: 1; + max-block-size: calc(100vh - 96px); +`; + +export function InvestigatePageTemplate({ children }: { children: React.ReactNode }) { + const { + dependencies: { + start: { observabilityShared }, + }, + } = useKibana(); + + const { PageTemplate } = observabilityShared.navigation; + + return ( + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_text_button/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_text_button/index.tsx new file mode 100644 index 00000000000000..56cee6eccca494 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_text_button/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiButtonEmpty, EuiText } from '@elastic/eui'; +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import React from 'react'; + +const buttonClassName = css` + opacity: 0.5; + &:disabled, + &:hover { + opacity: 1; + } + &:disabled { + color: inherit; + } +`; + +const buttonOnlyClassName = css` + .euiButtonEmpty__content { + gap: 0; + } +`; + +interface InvestigateTextButtonProps { + iconType: string; + disabled?: boolean; + onClick: () => void; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + children?: string; + className?: string; + type?: 'submit' | 'reset' | 'button'; + color?: React.ComponentProps['color']; + size?: 'xs' | 's' | 'm'; + iconSize?: 's' | 'm'; +} + +export function InvestigateTextButton({ + iconType, + disabled, + onClick, + children, + onMouseEnter, + onMouseLeave, + className, + type, + color = 'text', + size = 's', + iconSize = 's', +}: InvestigateTextButtonProps) { + const props = { + size, + iconSize, + iconType, + color, + disabled, + className: classNames(buttonClassName, className, { + [buttonOnlyClassName]: !children, + }), + onClick, + onMouseEnter, + onMouseLeave, + type, + children: children ? {children} : undefined, + }; + + return ; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.stories.tsx new file mode 100644 index 00000000000000..665622e063be81 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.stories.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import React from 'react'; +import { InvestigateView as Component } from '.'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +const meta: ComponentMeta = { + component: Component, + title: 'app/Organisms/InvestigateView', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +const defaultProps: ComponentStoryObj = { + args: {}, + render: (props) => , +}; + +export const InvestigateViewStory: ComponentStoryObj = { + ...defaultProps, + name: 'default', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.tsx new file mode 100644 index 00000000000000..38d1e8b16e08b3 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_view/index.tsx @@ -0,0 +1,246 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import type { InvestigateWidget, InvestigateWidgetCreate } from '@kbn/investigate-plugin/public'; +import { DATE_FORMAT_ID } from '@kbn/management-settings-ids'; +import { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { keyBy, omit, pick } from 'lodash'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { AddWidgetMode } from '../../constants/add_widget_mode'; +import { useDateRange } from '../../hooks/use_date_range'; +import { useKibana } from '../../hooks/use_kibana'; +import { getOverridesFromGlobalParameters } from '../../utils/get_overrides_from_global_parameters'; +import { AddWidgetUI } from '../add_widget_ui'; +import { InvestigateWidgetGrid } from '../investigate_widget_grid'; + +const containerClassName = css` + overflow: auto; + padding: 24px 24px 0px 24px; +`; + +const scrollContainerClassName = css` + min-width: 1px; +`; + +const gridContainerClassName = css` + position: relative; +`; + +const sideBarClassName = css` + width: 240px; + position: sticky; + top: 0; + padding: 0px 12px 32px 12px; +`; + +function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) { + const { + core: { uiSettings }, + dependencies: { + start: { investigate }, + }, + } = useKibana(); + + const [_displayedKuery, setDisplayedKuery] = useState(''); + + const widgetDefinitions = useMemo(() => investigate.getWidgetDefinitions(), [investigate]); + + const [range, setRange] = useDateRange(); + + const { + addItem, + setItemPositions, + setItemTitle, + blocks, + copyItem, + deleteItem, + investigation, + lockItem, + setItemParameters, + unlockItem, + revision, + } = investigate.useInvestigation({ + user, + from: range.start.toISOString(), + to: range.end.toISOString(), + }); + + const [_editingItem, setEditingItem] = useState(undefined); + + const createWidget = (widgetCreate: InvestigateWidgetCreate) => { + return addItem(widgetCreate); + }; + + const createWidgetRef = useRef(createWidget); + + createWidgetRef.current = createWidget; + + useEffect(() => { + const itemIds = revision?.items.map((item) => item.id) ?? []; + setEditingItem((prevEditingItem) => { + if (prevEditingItem && !itemIds.includes(prevEditingItem.id)) { + return undefined; + } + return prevEditingItem; + }); + }, [revision]); + + useEffect(() => { + setDisplayedKuery(revision?.parameters.query.query ?? ''); + }, [revision?.parameters.query.query]); + + useEffect(() => { + if ( + revision?.parameters.timeRange.from && + revision?.parameters.timeRange.to && + range.start.toISOString() !== revision.parameters.timeRange.from && + range.end.toISOString() !== revision.parameters.timeRange.to + ) { + setRange({ + from: revision.parameters.timeRange.from, + to: revision.parameters.timeRange.to, + }); + } + }, [ + revision?.parameters.timeRange.from, + revision?.parameters.timeRange.to, + range.start, + range.end, + setRange, + ]); + + const gridItems = useMemo(() => { + const widgetDefinitionsByType = keyBy(widgetDefinitions, 'type'); + + return revision?.items.map((item) => { + const definitionForType = widgetDefinitionsByType[item.type]; + + return ( + { + title: item.title, + description: item.description ?? '', + id: item.id, + element: item.element, + columns: item.columns, + rows: item.rows, + chrome: definitionForType.chrome, + locked: item.locked, + loading: item.loading, + overrides: item.locked + ? getOverridesFromGlobalParameters( + pick(item.parameters, 'filters', 'query', 'timeRange'), + revision.parameters, + uiSettings.get(DATE_FORMAT_ID) ?? 'Browser' + ) + : [], + } ?? [] + ); + }); + }, [revision, widgetDefinitions, uiSettings]); + + const [searchBarFocused] = useState(false); + + if (!investigation || !revision || !gridItems) { + return ; + } + + return ( + + + + + + { + return setItemPositions( + nextGridItems.map((gridItem) => ({ + columns: gridItem.columns, + rows: gridItem.rows, + id: gridItem.id, + })) + ); + }} + onItemTitleChange={async (item, title) => { + return setItemTitle(item.id, title); + }} + onItemCopy={async (copiedItem) => { + return copyItem(copiedItem.id); + }} + onItemDelete={async (deletedItem) => { + return deleteItem(deletedItem.id); + }} + onItemLockToggle={async (toggledItem) => { + return toggledItem.locked ? unlockItem(toggledItem.id) : lockItem(toggledItem.id); + }} + fadeLockedItems={searchBarFocused} + onItemOverrideRemove={async (updatedItem, override) => { + // TODO: remove filters + const itemToUpdate = revision.items.find((item) => item.id === updatedItem.id); + if (itemToUpdate) { + return setItemParameters(updatedItem.id, { + ...revision.parameters, + ...omit(itemToUpdate.parameters, override.id), + }); + } + }} + onItemEditClick={(itemToEdit) => { + setEditingItem(revision.items.find((item) => item.id === itemToEdit.id)); + }} + /> + + + { + return createWidgetRef.current(widget); + }} + /> + + + + + {i18n.translate( + 'xpack.investigateApp.investigateViewWithUser.addAnObservationChartButtonLabel', + { defaultMessage: 'Add an observation chart' } + )} + + + + + + + {i18n.translate( + 'xpack.investigateApp.investigateViewWithUser.placeholderForRightSidebarFlexItemLabel', + { defaultMessage: 'placeholder for right sidebar' } + )} + + + ); +} + +export function InvestigateView({}: {}) { + const { + core: { security }, + } = useKibana(); + + const user = useAsync(() => { + return security.authc.getCurrentUser(); + }, [security]); + + return user.value ? : null; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.stories.tsx new file mode 100644 index 00000000000000..f4436051d76c26 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.stories.tsx @@ -0,0 +1,144 @@ +/* + * 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 { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import React, { useState } from 'react'; +import { v4 } from 'uuid'; +import { ChromeOption } from '@kbn/investigate-plugin/public'; +import { InvestigateWidgetGrid as Component, InvestigateWidgetGridItem } from '.'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; +import { TimelineUserPrompt, TimelineAssistantResponse } from '../timeline_message'; + +const meta: ComponentMeta = { + component: Component, + title: 'app/Organisms/InvestigateWidgetGrid', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +function WithPersistedChanges(props: React.ComponentProps) { + const [items, setItems] = useState(props.items); + + return ( + { + setItems(() => nextItems); + }} + onItemCopy={async (item) => { + setItems((prevItems) => + prevItems.concat({ + ...item, + id: v4(), + }) + ); + }} + onItemDelete={async (item) => { + setItems((prevItems) => prevItems.filter((currentItem) => currentItem.id !== item.id)); + }} + items={items} + /> + ); +} + +const defaultProps: ComponentStoryObj = { + args: {}, + render: (props) => ( +
+ +
+ ), +}; + +function createItem>(overrides: T) { + return { + ...overrides, + id: v4(), + columns: 4, + rows: 2, + description: '', + locked: false, + loading: false, + overrides: [], + }; +} + +export const InvestigateWidgetGridStory: ComponentStoryObj = { + ...defaultProps, + args: { + ...defaultProps.args, + items: [ + createItem({ + title: '5', + description: '', + element: ( + {}} + /> + ), + columns: 4, + rows: 2, + chrome: ChromeOption.disabled, + }), + createItem({ + title: '1', + element: ( +
+ This should not overflow +
+ ), + columns: 4, + rows: 12, + locked: true, + }), + createItem({ + title: '5', + element: ( + {}} + /> + ), + columns: 4, + rows: 2, + chrome: ChromeOption.disabled, + }), + createItem({ + title: '2', + element: <>TODO, + columns: 2, + rows: 3, + overrides: [ + { + id: v4(), + label: '4 hours earlier', + }, + { + id: v4(), + label: 'service.name:opbeans-java AND service.enviroment:(production OR development)', + }, + ], + }), + createItem({ + title: '3', + element: <>TODO, + columns: 2, + rows: 3, + }), + createItem({ + title: '4', + element: <>TODO, + columns: 4, + rows: 3, + }), + ], + }, + name: 'default', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.tsx new file mode 100644 index 00000000000000..eea40cdca391d5 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.tsx @@ -0,0 +1,390 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { ChromeOption, InvestigateWidgetColumnSpan } from '@kbn/investigate-plugin/public'; +import { keyBy, mapValues, orderBy } from 'lodash'; +import React, { useCallback, useMemo, useRef } from 'react'; +import { ItemCallback, Layout, Responsive, WidthProvider } from 'react-grid-layout'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; +import { EuiBreakpoint, EUI_BREAKPOINTS, useBreakpoints } from '../../hooks/use_breakpoints'; +import { useTheme } from '../../hooks/use_theme'; +import { GridItem, GRID_ITEM_HEADER_HEIGHT } from '../grid_item'; +import './styles.scss'; + +const gridContainerClassName = css` + position: relative; + + .react-resizable-handle-ne, + .react-resizable-handle-nw { + top: calc(${GRID_ITEM_HEADER_HEIGHT}px) !important; + } +`; + +interface SingleComponentSection { + item: InvestigateWidgetGridItem; +} + +interface GridSection { + items: InvestigateWidgetGridItem[]; +} + +type Section = SingleComponentSection | GridSection; + +export interface InvestigateWidgetGridItemOverride { + id: string; + label: React.ReactNode; +} + +export interface InvestigateWidgetGridItem { + title: string; + description: string; + element: React.ReactNode; + id: string; + columns: number; + rows: number; + locked: boolean; + chrome?: ChromeOption; + loading: boolean; + overrides: InvestigateWidgetGridItemOverride[]; +} + +interface InvestigateWidgetGridProps { + items: InvestigateWidgetGridItem[]; + onItemsChange: (items: InvestigateWidgetGridItem[]) => Promise; + onItemCopy: (item: InvestigateWidgetGridItem) => Promise; + onItemDelete: (item: InvestigateWidgetGridItem) => Promise; + onItemLockToggle: (item: InvestigateWidgetGridItem) => Promise; + onItemOverrideRemove: ( + item: InvestigateWidgetGridItem, + override: InvestigateWidgetGridItemOverride + ) => Promise; + onItemTitleChange: (item: InvestigateWidgetGridItem, title: string) => Promise; + onItemEditClick: (item: InvestigateWidgetGridItem) => void; + fadeLockedItems: boolean; +} + +const ROW_HEIGHT = 32; + +const BREAKPOINT_COLUMNS: Record = { + [EUI_BREAKPOINTS.xs]: 1, + [EUI_BREAKPOINTS.s]: 1, + [EUI_BREAKPOINTS.m]: 4, + [EUI_BREAKPOINTS.l]: 4, + [EUI_BREAKPOINTS.xl]: 4, +}; + +const panelContainerClassName = css` + display: flex; +`; + +function getResponsiveLayouts( + items: InvestigateWidgetGridItem[], + currentBreakpoint: EuiBreakpoint +) { + const nextLayouts: Layout[] = []; + + let atColumn = 0; + let atRow = 0; + + let rowHeight = 0; + + const maxColumns = BREAKPOINT_COLUMNS[currentBreakpoint]; + + items.forEach((item) => { + const itemColumns = item.columns; + const itemRows = item.rows; + + if (atColumn + itemColumns > maxColumns) { + atColumn = 0; + atRow += rowHeight; + rowHeight = 0; + } + + nextLayouts.push({ + i: item.id, + w: itemColumns, + h: itemRows, + x: atColumn, + y: atRow, + resizeHandles: ['ne', 'se'], + }); + + atColumn += itemColumns; + + rowHeight = Math.max(itemRows, rowHeight); + }); + + return mapValues(EUI_BREAKPOINTS, () => nextLayouts); +} + +const CONTAINER_PADDING: [number, number] = [0, 0]; + +function GridSectionRenderer({ + items, + onItemsChange, + onItemDelete, + onItemCopy, + onItemLockToggle, + onItemOverrideRemove, + onItemTitleChange, + onItemEditClick, + fadeLockedItems, +}: InvestigateWidgetGridProps) { + const WithFixedWidth = useMemo(() => WidthProvider(Responsive), []); + + const theme = useTheme(); + + const callbacks = { + onItemsChange, + onItemCopy, + onItemDelete, + onItemLockToggle, + onItemOverrideRemove, + onItemTitleChange, + onItemEditClick, + }; + + const itemCallbacksRef = useRef(callbacks); + + itemCallbacksRef.current = callbacks; + + const { currentBreakpoint } = useBreakpoints(); + + const layouts = useMemo(() => { + return getResponsiveLayouts(items, currentBreakpoint); + }, [items, currentBreakpoint]); + + const gridElements = useMemo(() => { + return items.map((item) => ( +
+ { + return itemCallbacksRef.current.onItemTitleChange(item, title); + }} + onCopy={() => { + return itemCallbacksRef.current.onItemCopy(item); + }} + onDelete={() => { + return itemCallbacksRef.current.onItemDelete(item); + }} + locked={item.locked} + onLockToggle={() => { + itemCallbacksRef.current.onItemLockToggle(item); + }} + onOverrideRemove={(override) => { + return itemCallbacksRef.current.onItemOverrideRemove(item, override); + }} + onEditClick={() => { + return itemCallbacksRef.current.onItemEditClick(item); + }} + overrides={item.overrides} + loading={item.loading} + faded={fadeLockedItems && item.locked} + > + {item.element} + +
+ )); + }, [items, fadeLockedItems]); + + // react-grid calls `onLayoutChange` every time + // `layouts` changes, except when on mount. So... + // we do some gymnastics to skip the first call + // after a layout change + + const prevLayouts = useRef(layouts); + + const expectLayoutChangeCall = prevLayouts.current !== layouts; + + prevLayouts.current = layouts; + + const onLayoutChange = useMemo(() => { + let skipCall = expectLayoutChangeCall; + return (nextLayouts: Layout[]) => { + if (skipCall) { + skipCall = false; + return; + } + const itemsById = keyBy(items, (item) => item.id); + + const sortedLayouts = orderBy(nextLayouts, ['y', 'x']); + + const itemsInOrder = sortedLayouts.map((layout) => { + return itemsById[layout.i]; + }); + + itemCallbacksRef.current.onItemsChange(itemsInOrder); + }; + }, [items, expectLayoutChangeCall]); + + const onResize: ItemCallback = useCallback( + (layout) => { + const itemsById = keyBy(items, (item) => item.id); + + const itemsAfterResize = layout.map((layoutItem) => { + const gridItem = itemsById[layoutItem.i]; + + return { + ...gridItem, + columns: Math.max(1, layoutItem.w), + rows: Math.max(1, layoutItem.h), + }; + }); + + itemCallbacksRef.current.onItemsChange(itemsAfterResize); + }, + + [items] + ); + + return ( + + {gridElements} + + ); +} + +export function InvestigateWidgetGrid({ + items, + onItemsChange, + onItemDelete, + onItemCopy, + onItemLockToggle, + fadeLockedItems, + onItemOverrideRemove, + onItemTitleChange, + onItemEditClick, +}: InvestigateWidgetGridProps) { + const sections = useMemo(() => { + let currentGrid: GridSection = { items: [] }; + const allSections: Section[] = [currentGrid]; + + for (const item of items) { + if (item.chrome === ChromeOption.disabled || item.chrome === ChromeOption.static) { + const elementSection: SingleComponentSection = { + item, + }; + allSections.push(elementSection); + currentGrid = { items: [] }; + allSections.push(currentGrid); + } else { + currentGrid.items.push(item); + } + } + + return allSections.filter((grid) => 'item' in grid || grid.items.length > 0); + }, [items]); + + if (!sections.length) { + return null; + } + + return ( + + {sections.map((section, index) => { + if ('items' in section) { + return ( + + { + return onItemCopy(copiedItem); + }} + onItemDelete={(deletedItem) => { + return onItemDelete(deletedItem); + }} + onItemLockToggle={(toggledItem) => { + return onItemLockToggle(toggledItem); + }} + onItemsChange={(itemsInSection) => { + const nextItems = sections.flatMap((sectionAtIndex) => { + if ('item' in sectionAtIndex) { + return sectionAtIndex.item; + } + if (sectionAtIndex !== section) { + return sectionAtIndex.items; + } + return itemsInSection; + }); + + return onItemsChange(nextItems); + }} + onItemOverrideRemove={(item, override) => { + return onItemOverrideRemove(item, override); + }} + onItemTitleChange={(item, title) => { + return onItemTitleChange(item, title); + }} + onItemEditClick={(item) => { + return onItemEditClick(item); + }} + fadeLockedItems={fadeLockedItems} + /> + + ); + } + return ( + + {section.item.chrome === ChromeOption.disabled ? ( + section.item.element + ) : ( + { + return onItemCopy(section.item); + }} + onDelete={() => { + return onItemDelete(section.item); + }} + onOverrideRemove={(override) => { + return onItemOverrideRemove(section.item, override); + }} + onTitleChange={(nextTitle) => { + return onItemTitleChange(section.item, nextTitle); + }} + onLockToggle={() => { + return onItemLockToggle(section.item); + }} + onEditClick={() => { + return onItemEditClick(section.item); + }} + > + {section.item.element} + + )} + + ); + })} + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/styles.scss b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/styles.scss new file mode 100644 index 00000000000000..5528e053f84aac --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/styles.scss @@ -0,0 +1 @@ +@import '../../../../../../../src/plugins/dashboard/public/dashboard_container/dashboard_container'; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.stories.tsx new file mode 100644 index 00000000000000..3e9941db710f5b --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.stories.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Meta, StoryObj } from '@storybook/react'; +import { merge } from 'lodash'; +import React from 'react'; +import { InvestigationHistory as Component } from '.'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +interface Args { + props: React.ComponentProps; +} + +type StoryMeta = Meta; +type Story = StoryObj; + +const meta: StoryMeta = { + component: Component, + title: 'app/Molecules/InvestigationHistory', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +const defaultStory: Story = { + args: { + props: { + investigations: [], + error: undefined, + loading: false, + onDeleteInvestigationClick: () => {}, + onInvestigationClick: () => {}, + onStartNewInvestigationClick: () => {}, + }, + }, + render: function Render(args) { + return ( +
+ +
+ ); + }, +}; + +export const WithInvestigationsStory: Story = { + ...defaultStory, + args: merge({}, defaultStory.args, { + props: { + loading: false, + investigations: [ + { + id: 'one', + title: 'My previous investigation', + }, + { + id: 'two', + title: 'Another investigation', + }, + { + id: 'three', + title: 'Blabla', + }, + { + id: 'four', + title: 'A really really long title that shows how this component deals with overflow', + }, + ], + }, + }), + name: 'default', +}; + +export const LoadingEmptyStory: Story = { + ...defaultStory, + args: merge({}, defaultStory.args, { + props: { + loading: true, + }, + }), + name: 'loading empty', +}; + +export const ErrorStory: Story = { + ...defaultStory, + args: merge({}, defaultStory.args, { + props: { + loading: false, + error: new Error('Failed to load investigations'), + }, + }), + name: 'error', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.tsx new file mode 100644 index 00000000000000..963464cbc46e9c --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_history/index.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiLink, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import { Investigation } from '@kbn/investigate-plugin/common'; +import { useTheme } from '../../hooks/use_theme'; +import { InvestigateTextButton } from '../investigate_text_button'; + +const headerClassName = css` + text-transform: uppercase; + font-weight: 600; +`; + +const investigationItemClassName = css` + max-width: 100%; + white-space: normal; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +`; + +const newInvestigationItemClassName = css` + .euiText { + font-weight: 500 !important; + } +`; + +function WrapWithHeader({ children, loading }: { children: React.ReactElement; loading: boolean }) { + return ( + + + + + + {i18n.translate('xpack.investigateApp.investigationHistory.previously', { + defaultMessage: 'Previously', + })} + + + {loading ? ( + + + + ) : null} + + + {children} + + ); +} + +export function InvestigationHistory({ + investigations, + loading, + error, + onInvestigationClick, + onStartNewInvestigationClick, + onDeleteInvestigationClick, +}: { + investigations?: Array>; + loading: boolean; + error?: Error; + onInvestigationClick: (id: string) => void; + onStartNewInvestigationClick: () => void; + onDeleteInvestigationClick: (id: string) => void; +}) { + const theme = useTheme(); + + const investigationsList = ( + + + {} + { + onStartNewInvestigationClick(); + }} + > + + + + + + + {i18n.translate('xpack.investigateApp.investigationHistory.new', { + defaultMessage: 'New investigation', + })} + + + + + + {investigations?.length ? ( + + + + ) : null} + {investigations?.map((investigation) => ( + + + + { + onInvestigationClick(investigation.id); + }} + > + + {investigation.title} + + + + + { + onDeleteInvestigationClick(investigation.id); + }} + size="xs" + /> + + + + ))} + + ); + + if (error) { + return ( + + + + + + + + + + {error.message} + + + + + {investigationsList} + + + ); + } + + return {investigationsList}; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.stories.tsx new file mode 100644 index 00000000000000..32c9868eeaa635 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.stories.tsx @@ -0,0 +1,56 @@ +/* + * 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 { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { NoteWidget as Component } from '.'; +import { extendProps } from '../../../.storybook/extend_props'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +interface Args { + props: Omit, 'onChange' | 'onDelete'>; +} + +type StoryMeta = Meta; +type Story = StoryObj; + +const meta: StoryMeta = { + component: Component, + title: 'app/Molecules/NoteWidget', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +const defaultStory: Story = { + args: { + props: { + user: { + username: 'johndoe', + full_name: 'John Doe', + }, + note: 'A short note', + }, + }, + render: function Render(args) { + return ( +
+ {}} onDelete={() => {}} /> +
+ ); + }, +}; + +export const ShortNoteStory: Story = { + ...defaultStory, + args: { + props: extendProps(defaultStory.args!.props!, { + note: 'A short note', + }), + }, + name: 'default', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.tsx new file mode 100644 index 00000000000000..056ec7ccfe5523 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget/index.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiAvatar } from '@elastic/eui'; +import { AuthenticatedUser } from '@kbn/core/public'; +import React from 'react'; +import { useTheme } from '../../hooks/use_theme'; +import { TimelineMessage } from '../timeline_message'; + +export function NoteWidget({ + user, + note, + onDelete, +}: { + user: Pick; + note: string; + onChange: (note: string) => void; + onDelete: () => void; +}) { + const theme = useTheme(); + return ( + } + color={theme.colors.emptyShade} + content={note} + onDelete={onDelete} + /> + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget_control/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget_control/index.tsx new file mode 100644 index 00000000000000..7739058ce31598 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/note_widget_control/index.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { InvestigateWidgetCreate } from '@kbn/investigate-plugin/common'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { AuthenticatedUser } from '@kbn/core/public'; +import { ResizableTextInput } from '../resizable_text_input'; +import { createNoteWidget } from '../../widgets/note_widget/create_note_widget'; + +interface NoteWidgetControlProps { + user: Pick; + onWidgetAdd: (widget: InvestigateWidgetCreate) => Promise; +} + +export function NoteWidgetControl({ user, onWidgetAdd }: NoteWidgetControlProps) { + const [note, setNote] = useState(''); + + const [loading, setLoading] = useState(false); + + function submit() { + setLoading(false); + onWidgetAdd( + createNoteWidget({ + title: note, + parameters: { + note, + user: { + username: user.username, + full_name: user.full_name, + }, + }, + }) + ) + .then(() => { + setNote(''); + }) + .finally(() => { + setLoading(false); + }); + } + + return ( + + + { + setNote(value); + }} + onSubmit={() => { + submit(); + }} + /> + + + { + submit(); + }} + /> + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.stories.tsx new file mode 100644 index 00000000000000..7e506082bbbe65 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.stories.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import React from 'react'; +import { PreviewLensSuggestion as Component } from '.'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +const meta: ComponentMeta = { + component: Component, + title: 'app/Molecules/PreviewLensSuggestion', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +const defaultProps: ComponentStoryObj = { + args: {}, + render: (props) => , +}; + +export const PreviewLensSuggestionStory: ComponentStoryObj = { + ...defaultProps, + args: {}, + name: 'default', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.tsx new file mode 100644 index 00000000000000..5a224e14455404 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/preview_lens_suggestion/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { useKibana } from '../../hooks/use_kibana'; + +function Container({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +const panelContainerClassName = css` + overflow: clip auto; + height: 100%; +`; + +const panelContentClassName = css` + height: 100%; + overflow: clip auto; + > div { + height: 100%; + } +`; + +export function PreviewLensSuggestion({ + input, + loading, + error, +}: { + input: TypedLensByValueInput; + loading: boolean; + error?: Error; +}) { + const { + dependencies: { + start: { lens }, + }, + } = useKibana(); + if (loading) { + return ( + + + + ); + } + + return ( + +
+ +
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/resizable_text_input/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/resizable_text_input/index.tsx new file mode 100644 index 00000000000000..c0c4e7e3d1cfeb --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/resizable_text_input/index.tsx @@ -0,0 +1,79 @@ +/* + * 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 { EuiTextArea, keys } from '@elastic/eui'; +import { css } from '@emotion/css'; +import React, { useCallback, useEffect, useRef } from 'react'; + +interface Props { + placeholder: string; + disabled: boolean; + value: string; + onChange: (value: string) => void; + onSubmit: () => void; +} + +export function ResizableTextInput({ disabled, value, onChange, onSubmit, placeholder }: Props) { + const textAreaRef = useRef(null); + + const handleChange = (event: React.ChangeEvent) => { + handleResizeTextArea(); + + onChange(event.target.value); + }; + + const handleResizeTextArea = useCallback(() => { + if (textAreaRef.current) { + textAreaRef.current.style.minHeight = 'auto'; + + const cappedHeight = Math.min(textAreaRef.current?.scrollHeight, 350); + + textAreaRef.current.style.minHeight = cappedHeight + 'px'; + } + }, []); + + useEffect(() => { + const textarea = textAreaRef.current; + + if (textarea) { + textarea.focus(); + } + }, []); + + useEffect(() => { + handleResizeTextArea(); + }, [handleResizeTextArea]); + + useEffect(() => { + if (value === undefined) { + handleResizeTextArea(); + } + }, [handleResizeTextArea, value]); + + return ( + { + if (!event.shiftKey && event.key === keys.ENTER) { + event.preventDefault(); + onSubmit(); + } + }} + /> + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.stories.tsx new file mode 100644 index 00000000000000..445548dfc46c5b --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.stories.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Suggestion } from '@kbn/lens-plugin/public'; +import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import React from 'react'; +import { v4 } from 'uuid'; +import { SuggestVisualizationList as Component } from '.'; +import '../../../.storybook/mock_kibana_services'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; +import { metricSuggestion, tableSuggestion, treemapSuggestion } from './suggestions.mock'; + +const meta: ComponentMeta = { + component: Component, + title: 'app/Molecules/SuggestVisualizationList', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +function mapWithIds(suggestions: Suggestion[]) { + return suggestions.map((suggestion) => ({ id: v4(), ...suggestion })); +} + +const defaultProps: ComponentStoryObj = { + render: (props) => { + return ; + }, +}; + +export const WithSuggestions: ComponentStoryObj = { + ...defaultProps, + args: { + loading: false, + suggestions: mapWithIds([tableSuggestion, treemapSuggestion]), + }, + name: 'With suggestions', +}; + +export const WithoutSuggestions: ComponentStoryObj = { + ...defaultProps, + args: { + loading: false, + suggestions: [], + }, + name: 'Without suggestions', +}; + +export const LoadingStory: ComponentStoryObj = { + ...defaultProps, + args: { + loading: true, + suggestions: [], + }, + name: 'Loading without suggestions', +}; + +export const LoadingWithSuggestionsStory: ComponentStoryObj = { + ...defaultProps, + args: { + loading: true, + suggestions: mapWithIds([metricSuggestion, treemapSuggestion]), + }, + name: 'Loading with suggestions', +}; + +export const ErrorStory: ComponentStoryObj = { + ...defaultProps, + args: { + error: new Error('Network error'), + suggestions: [], + }, + name: 'Error', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.tsx new file mode 100644 index 00000000000000..a1702cc064f14c --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/index.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import type { Suggestion } from '@kbn/lens-plugin/public'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; + +const containerClassName = css` + min-height: 32px; +`; + +const suggestionClassName = css` + .euiText { + text-overflow: ellipsis; + white-space: nowrap; + max-width: 140px; + overflow: hidden; + text-align: left; + } + span { + justify-content: flex-start; + } +`; + +const iconContainerClassName = css` + display: flex; + align-items: center; + width: 16px; +`; + +export function SuggestVisualizationList({ + suggestions, + loading, + error, + onSuggestionClick, + onSuggestionRollOver, + onMouseLeave, +}: { + suggestions?: Array; + loading: boolean; + error?: Error; + onSuggestionClick: (suggestion: Suggestion) => void; + onSuggestionRollOver: (suggestion: Suggestion) => void; + onMouseLeave: () => void; +}) { + if (error) { + return ( + + + + + + + {i18n.translate( + 'xpack.investigateApp.suggestVisualizationList.errorLoadingSuggestionsLabel', + { + defaultMessage: 'Error loading suggestions: {message}', + values: { message: error.message }, + } + )} + + + + ); + } + + const icon = loading ? : ; + + let message: string = ''; + + if (loading && !suggestions?.length) { + message = i18n.translate( + 'xpack.investigateApp.suggestVisualizationList.loadingSuggestionsLabel', + { + defaultMessage: 'Loading suggestions', + } + ); + } else if (!loading && !suggestions?.length) { + message = i18n.translate('xpack.investigateApp.suggestVisualizationList.noSuggestionsLabel', { + defaultMessage: 'No suitable suggestions', + }); + } + + return ( + + + {icon} + + + {message ? ( + {message} + ) : ( + + {suggestions?.map((suggestion) => ( + + { + onSuggestionClick(suggestion); + }} + onMouseEnter={() => { + onSuggestionRollOver(suggestion); + }} + onMouseLeave={() => { + onMouseLeave(); + }} + > + {suggestion.title} + + + ))} + + )} + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/suggestions.mock.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/suggestions.mock.tsx new file mode 100644 index 00000000000000..22f26adef0a560 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/suggest_visualization_list/suggestions.mock.tsx @@ -0,0 +1,384 @@ +/* + * 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 { Suggestion } from '@kbn/lens-plugin/public'; + +export const tableSuggestion: Suggestion = { + title: + 'Table @timestamp & agent.activation_method & agent.ephemeral_id & agent.name & agent.version', + score: 0.2, + hide: true, + visualizationId: 'lnsDatatable', + previewIcon: 'visTable', + visualizationState: { + layerId: '5594a808-654b-4170-825d-26c58069bb27', + layerType: 'data', + columns: [ + { + columnId: '@timestamp', + }, + { + columnId: 'agent.activation_method', + }, + { + columnId: 'agent.ephemeral_id', + }, + { + columnId: 'agent.name', + }, + { + columnId: 'agent.version', + }, + ], + }, + keptLayerIds: ['5594a808-654b-4170-825d-26c58069bb27'], + datasourceState: { + layers: { + '5594a808-654b-4170-825d-26c58069bb27': { + index: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + query: { + esql: 'FROM logs-apm.error-default', + }, + columns: [ + { + columnId: '@timestamp', + fieldName: '@timestamp', + meta: { + type: 'date', + }, + inMetricDimension: true, + }, + { + columnId: 'agent.activation_method', + fieldName: 'agent.activation_method', + meta: { + type: 'string', + }, + inMetricDimension: true, + }, + { + columnId: 'agent.ephemeral_id', + fieldName: 'agent.ephemeral_id', + meta: { + type: 'string', + }, + inMetricDimension: true, + }, + { + columnId: 'agent.name', + fieldName: 'agent.name', + meta: { + type: 'string', + }, + inMetricDimension: true, + }, + { + columnId: 'agent.version', + fieldName: 'agent.version', + meta: { + type: 'string', + }, + inMetricDimension: true, + }, + ], + }, + }, + indexPatternRefs: [ + { + id: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + title: 'logs-apm.error-default', + }, + ], + }, + datasourceId: 'textBased', + columns: 5, + changeType: 'initial', +}; + +export const metricSuggestion: Suggestion = { + title: 'Metric', + score: 0.51, + hide: true, + visualizationId: 'lnsMetric', + previewIcon: 'visMetric', + visualizationState: { + layerId: 'ecd36789-1acb-4278-b087-2e46cf459f89', + layerType: 'data', + metricAccessor: 'COUNT(*)', + }, + keptLayerIds: ['ecd36789-1acb-4278-b087-2e46cf459f89'], + datasourceState: { + layers: { + 'ecd36789-1acb-4278-b087-2e46cf459f89': { + index: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + query: { + esql: 'FROM logs-apm.error-default | STATS COUNT(*)', + }, + columns: [ + { + columnId: 'COUNT(*)', + fieldName: 'COUNT(*)', + meta: { + type: 'number', + }, + inMetricDimension: true, + }, + ], + }, + }, + indexPatternRefs: [ + { + id: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + title: 'logs-apm.error-default', + }, + ], + }, + datasourceId: 'textBased', + columns: 1, + changeType: 'initial', +}; + +export const barSuggestion: Suggestion = { + title: 'Bar vertical stacked', + score: 0.16666666666666666, + hide: false, + incomplete: false, + visualizationId: 'lnsXY', + previewIcon: 'visBarVerticalStacked', + visualizationState: { + legend: { + isVisible: true, + position: 'right', + }, + valueLabels: 'hide', + fittingFunction: 'None', + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + labelsOrientation: { + x: 0, + yLeft: 0, + yRight: 0, + }, + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '6aeee1c5-c080-4c22-8548-c887a213a433', + seriesType: 'bar_stacked', + xAccessor: 'BUCKET(@timestamp, 1 minute)', + accessors: ['COUNT(*)'], + layerType: 'data', + colorMapping: { + assignments: [], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'loop', + }, + touched: false, + }, + ], + paletteId: 'eui_amsterdam_color_blind', + colorMode: { + type: 'categorical', + }, + }, + }, + ], + }, + keptLayerIds: ['6aeee1c5-c080-4c22-8548-c887a213a433'], + datasourceState: { + layers: { + '6aeee1c5-c080-4c22-8548-c887a213a433': { + index: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + query: { + esql: 'FROM logs-apm.error-default | WHERE @timestamp >= NOW() - 15 minutes | STATS COUNT(*) BY BUCKET(@timestamp, 1 minute)', + }, + columns: [ + { + columnId: 'COUNT(*)', + fieldName: 'COUNT(*)', + meta: { + type: 'number', + }, + inMetricDimension: true, + }, + { + columnId: 'BUCKET(@timestamp, 1 minute)', + fieldName: 'BUCKET(@timestamp, 1 minute)', + meta: { + type: 'date', + }, + }, + ], + }, + }, + indexPatternRefs: [ + { + id: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + title: 'logs-apm.error-default', + }, + ], + }, + datasourceId: 'textBased', + columns: 2, + changeType: 'unchanged', +}; + +export const treemapSuggestion: Suggestion = { + title: 'Treemap', + score: 0.56, + hide: false, + incomplete: false, + visualizationId: 'lnsPie', + previewIcon: 'namespace', + visualizationState: { + shape: 'treemap', + layers: [ + { + layerId: '6aeee1c5-c080-4c22-8548-c887a213a433', + primaryGroups: ['BUCKET(@timestamp, 1 minute)'], + metrics: ['COUNT(*)'], + numberDisplay: 'percent', + categoryDisplay: 'default', + legendDisplay: 'default', + nestedLegend: false, + layerType: 'data', + }, + ], + }, + keptLayerIds: ['6aeee1c5-c080-4c22-8548-c887a213a433'], + datasourceState: { + layers: { + '6aeee1c5-c080-4c22-8548-c887a213a433': { + index: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + query: { + esql: 'FROM logs-apm.error-default | WHERE @timestamp >= NOW() - 15 minutes | STATS COUNT(*) BY BUCKET(@timestamp, 1 minute)', + }, + columns: [ + { + columnId: 'COUNT(*)', + fieldName: 'COUNT(*)', + meta: { + type: 'number', + }, + inMetricDimension: true, + }, + { + columnId: 'BUCKET(@timestamp, 1 minute)', + fieldName: 'BUCKET(@timestamp, 1 minute)', + meta: { + type: 'date', + }, + }, + ], + }, + }, + indexPatternRefs: [ + { + id: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + title: 'logs-apm.error-default', + }, + ], + }, + datasourceId: 'textBased', + columns: 2, + changeType: 'initial', +}; + +export const donutSuggestion: Suggestion = { + title: 'Donut', + score: 0.46, + hide: false, + incomplete: false, + visualizationId: 'lnsPie', + previewIcon: 'help', + visualizationState: { + shape: 'donut', + layers: [ + { + layerId: '6aeee1c5-c080-4c22-8548-c887a213a433', + primaryGroups: ['BUCKET(@timestamp, 1 minute)'], + metrics: ['COUNT(*)'], + numberDisplay: 'percent', + categoryDisplay: 'default', + legendDisplay: 'default', + nestedLegend: false, + layerType: 'data', + colorMapping: { + assignments: [], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'loop', + }, + touched: false, + }, + ], + paletteId: 'eui_amsterdam_color_blind', + colorMode: { + type: 'categorical', + }, + }, + }, + ], + }, + keptLayerIds: ['6aeee1c5-c080-4c22-8548-c887a213a433'], + datasourceState: { + layers: { + '6aeee1c5-c080-4c22-8548-c887a213a433': { + index: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + query: { + esql: 'FROM logs-apm.error-default | WHERE @timestamp >= NOW() - 15 minutes | STATS COUNT(*) BY BUCKET(@timestamp, 1 minute)', + }, + columns: [ + { + columnId: 'COUNT(*)', + fieldName: 'COUNT(*)', + meta: { + type: 'number', + }, + inMetricDimension: true, + }, + { + columnId: 'BUCKET(@timestamp, 1 minute)', + fieldName: 'BUCKET(@timestamp, 1 minute)', + meta: { + type: 'date', + }, + }, + ], + }, + }, + indexPatternRefs: [ + { + id: '6176e654c875b9d3d9a3a69414fa44c561964cbb174a1f64f69e34eced7debac', + title: 'logs-apm.error-default', + }, + ], + }, + datasourceId: 'textBased', + columns: 2, + changeType: 'unchanged', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/timeline_message/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/timeline_message/index.tsx new file mode 100644 index 00000000000000..aa28df9a84902b --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/timeline_message/index.tsx @@ -0,0 +1,133 @@ +/* + * 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 { + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, + EuiMarkdownFormat, + EuiPanel, + EuiText, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import React from 'react'; +import { AssistantAvatar } from '@kbn/observability-ai-assistant-plugin/public'; +import { AuthenticatedUser } from '@kbn/core/public'; +import { shade } from 'polished'; +import { useTheme } from '../../hooks/use_theme'; +import { InvestigateTextButton } from '../investigate_text_button'; + +const textContainerClassName = css` + padding-top: 2px; +`; + +const borderColor = shade(0.15); + +export function TimelineMessage({ + icon, + content, + color, + onDelete, +}: { + icon: React.ReactNode; + content: string; + color: string; + onDelete: () => void; +}) { + const theme = useTheme(); + + const panelClassName = css` + background-color: ${color}; + border-radius: 16px; + padding: 12px; + border-width: 1px; + border-color: ${borderColor(color)}; + `; + + const containerClassName = css` + height: 100%; + .euiButtonIcon { + opacity: 0; + transition: opacity ${theme.animation.fast} ${theme.animation.resistance}; + } + `; + return ( + + + {icon} + + + {content} + + + + + + + + ); +} + +export function TimelineUserPrompt({ + user, + prompt, + onDelete, +}: { + user: Pick; + prompt: string; + onDelete: () => void; +}) { + const theme = useTheme(); + return ( + } + onDelete={onDelete} + /> + ); +} + +export function TimelineAssistantResponse({ + content, + onDelete, +}: { + content: string; + onDelete: () => void; +}) { + const theme = useTheme(); + + const assistantAvatarContainer = css` + border-radius: 32px; + width: 32px; + height: 32px; + background: ${theme.colors.emptyShade}; + padding: 7px; + border: 1px solid ${borderColor(theme.colors.highlight)}; + `; + + return ( + + + + } + onDelete={onDelete} + /> + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.stories.tsx new file mode 100644 index 00000000000000..2d08222a28452d --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.stories.tsx @@ -0,0 +1,77 @@ +/* + * 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 { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import React from 'react'; +import { WorkflowBlock } from '@kbn/investigate-plugin/common'; +import { WorkflowBlocksControl as Component } from '.'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +const meta: ComponentMeta = { + component: Component, + title: 'app/Molecules/WorkflowsBlock', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +function createWorkflowBlocks(): WorkflowBlock[] { + return [ + { + id: '0', + content: 'Investigate alerts', + description: '12 open alerts', + loading: false, + color: 'warning', + }, + { + id: '1', + content: '', + description: '', + loading: true, + onClick: () => {}, + }, + { + id: '2', + content: 'Really really really long content to see how the component deals with wrapping', + description: + 'I need a really long description too, because that one needs to deal with overflow as well, and should stay on a single line', + loading: false, + onClick: () => {}, + }, + ]; +} + +const defaultProps: ComponentStoryObj = { + render: (props) => { + return ( +
+ +
+ ); + }, +}; + +export const DefaultStory: ComponentStoryObj = { + ...defaultProps, + args: { + ...defaultProps.args, + blocks: createWorkflowBlocks(), + compressed: false, + }, + name: 'default', +}; + +export const CompressedStory: ComponentStoryObj = { + ...defaultProps, + args: { + ...defaultProps.args, + blocks: createWorkflowBlocks(), + compressed: true, + }, + name: 'compressed', +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.tsx new file mode 100644 index 00000000000000..8182ebd4a34eb7 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/workflow_blocks_control/index.tsx @@ -0,0 +1,160 @@ +/* + * 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 { + EuiErrorBoundary, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPanel, + EuiText, +} from '@elastic/eui'; +// @ts-expect-error +import { getTextColor } from '@elastic/eui/lib/components/badge/color_utils'; +import { css } from '@emotion/css'; +import { WorkflowBlock } from '@kbn/investigate-plugin/common'; +import classNames from 'classnames'; +import { rgba } from 'polished'; +import React from 'react'; +import { useTheme } from '../../hooks/use_theme'; + +const groupClassName = css` + height: 100%; +`; + +const textItemClassName = css` + max-width: 100%; + text-align: left; +`; + +const descriptionClassName = css` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const itemClassName = css` + max-width: 320px; +`; + +const loadingContainerClassName = css` + height: 100%; +`; + +function WorkflowBlockControl({ + content, + description, + loading, + onClick, + color = 'primary', + children, + compressed, +}: Omit & { compressed: boolean }) { + const theme = useTheme(); + + const actualColor = theme.colors[loading ? 'lightestShade' : color]; + + const panelClassName = css` + background-color: ${rgba(actualColor, 0.75)}; + height: ${compressed ? 32 : 128}px; + transition: all ${theme.animation.fast} ${theme.animation.resistance} !important; + `; + + const contentClassName = css` + overflow: hidden; + text-overflow: ellipsis; + font-weight: 500; + white-space: normal; + display: -webkit-box; + -webkit-line-clamp: ${compressed ? 1 : 2}; + -webkit-box-orient: vertical; + `; + + const panelClickableClassName = onClick + ? classNames( + panelClassName, + css` + cursor: pointer; + &:hover, + &:focus { + box-shadow: none; + background-color: ${rgba(actualColor, 1)}; + transform: none; + border: 1px solid ${theme.colors.darkestShade}; + } + ` + ) + : panelClassName; + + const textColor = getTextColor({ euiTheme: theme }, actualColor); + + if (loading) { + return ( + <> + + + + + + + + {children} + + ); + } + + return ( + <> + + + {description && !compressed && ( + + + {description} + + + )} + + + {content} + + + + + {children} + + ); +} + +export function WorkflowBlocksControl({ + blocks, + compressed, +}: { + blocks: WorkflowBlock[]; + compressed: boolean; +}) { + return ( + + {blocks.map((block) => ( + + + + + + ))} + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/constants/add_widget_mode.ts b/x-pack/plugins/observability_solution/investigate_app/public/constants/add_widget_mode.ts new file mode 100644 index 00000000000000..0996fea2472b96 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/constants/add_widget_mode.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum AddWidgetMode { + Esql = 'esql', + Note = 'note', +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/constants/index.ts b/x-pack/plugins/observability_solution/investigate_app/public/constants/index.ts new file mode 100644 index 00000000000000..32d78245dc51b3 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/constants/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ESQL_WIDGET_NAME = 'esql'; +export const EMBEDDABLE_WIDGET_NAME = 'embeddable'; +export const NOTE_WIDGET_NAME = 'note'; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_abort_signal.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_abort_signal.ts new file mode 100644 index 00000000000000..cf8f4ce7870420 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_abort_signal.ts @@ -0,0 +1,21 @@ +/* + * 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 { useEffect, useRef } from 'react'; + +export function useAbortSignal() { + const controllerRef = useRef(new AbortController()); + + useEffect(() => { + const controller = controllerRef.current; + return () => { + controller.abort(); + }; + }, []); + + return controllerRef.current.signal; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_breakpoints.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_breakpoints.ts new file mode 100644 index 00000000000000..526cc24287e336 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_breakpoints.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiThemeBreakpoints } from '@elastic/eui'; +import { + useCurrentEuiBreakpoint, + useIsWithinMaxBreakpoint, + useIsWithinMinBreakpoint, +} from '@elastic/eui'; +import { useMemo } from 'react'; +import { Values } from '@kbn/utility-types'; + +export type Breakpoints = Record; + +export const EUI_BREAKPOINTS = { + xs: EuiThemeBreakpoints[0], + s: EuiThemeBreakpoints[1], + m: EuiThemeBreakpoints[2], + l: EuiThemeBreakpoints[3], + xl: EuiThemeBreakpoints[4], +}; + +export type EuiBreakpoint = Values; + +export function useBreakpoints() { + const isXSmall = useIsWithinMaxBreakpoint('xs'); + const isSmall = useIsWithinMaxBreakpoint('s'); + const isMedium = useIsWithinMaxBreakpoint('m'); + const isLarge = useIsWithinMaxBreakpoint('l'); + const isXl = useIsWithinMinBreakpoint('xl'); + + const currentBreakpoint = useCurrentEuiBreakpoint(); + + return useMemo(() => { + return { + isXSmall, + isSmall, + isMedium, + isLarge, + isXl, + currentBreakpoint: (currentBreakpoint ?? EUI_BREAKPOINTS.xl) as EuiBreakpoint, + }; + }, [isXSmall, isSmall, isMedium, isLarge, isXl, currentBreakpoint]); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_date_range.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_date_range.ts new file mode 100644 index 00000000000000..606fc3b5298c79 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_date_range.ts @@ -0,0 +1,59 @@ +/* + * 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 datemath from '@elastic/datemath'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import moment from 'moment'; +import { useCallback, useEffect, useState } from 'react'; +import type { InputTimeRange } from '@kbn/data-plugin/public/query'; +import { useKibana } from './use_kibana'; + +function getDatesFromDataPluginStart(data: DataPublicPluginStart) { + const { from, to } = data.query.timefilter.timefilter.getTime(); + + return { + from, + to, + start: datemath.parse(from) ?? moment().subtract(15, 'minutes'), + end: datemath.parse(to, { roundUp: true }) ?? moment(), + }; +} + +export function useDateRange() { + const { + dependencies: { + start: { data }, + }, + } = useKibana(); + + const [time, setTime] = useState(() => { + return getDatesFromDataPluginStart(data); + }); + + useEffect(() => { + const subscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ + next: () => { + setTime(() => { + return getDatesFromDataPluginStart(data); + }); + }, + }); + + return () => { + subscription.unsubscribe(); + }; + }, [data]); + + const setRange = useCallback( + (inputRange: InputTimeRange) => { + return data.query.timefilter.timefilter.setTime(inputRange); + }, + [data] + ); + + return [time, setRange] as const; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_params.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_params.ts new file mode 100644 index 00000000000000..d29e06bea8b9cf --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_params.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { type PathsOf, type TypeOf, useParams } from '@kbn/typed-react-router-config'; +import type { InvestigateRoutes } from '../routes/config'; + +export function useInvestigateParams>( + path: TPath +): TypeOf { + return useParams(path)! as TypeOf; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_router.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_router.ts new file mode 100644 index 00000000000000..9eea4fe152b667 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_investigate_router.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config'; +import { useMemo } from 'react'; +import { InvestigateRouter, InvestigateRoutes } from '../routes/config'; +import { investigateRouter } from '../routes/config'; +import { useKibana } from './use_kibana'; + +interface StatefulInvestigateRouter extends InvestigateRouter { + push>( + path: T, + ...params: TypeAsArgs> + ): void; + replace>( + path: T, + ...params: TypeAsArgs> + ): void; +} + +export function useInvestigateRouter(): StatefulInvestigateRouter { + const { + core: { + http, + application: { navigateToApp }, + }, + } = useKibana(); + + const link = (...args: any[]) => { + // @ts-expect-error + return investigateRouter.link(...args); + }; + + return useMemo( + () => ({ + ...investigateRouter, + push: (...args) => { + const next = link(...args); + navigateToApp('investigate', { path: next, replace: false }); + }, + replace: (path, ...args) => { + const next = link(path, ...args); + navigateToApp('investigate', { path: next, replace: true }); + }, + link: (path, ...args) => { + return http.basePath.prepend('/app/investigate' + link(path, ...args)); + }, + }), + [navigateToApp, http.basePath] + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_kibana.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_kibana.ts new file mode 100644 index 00000000000000..a95da04eef446c --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_kibana.ts @@ -0,0 +1,23 @@ +/* + * 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 { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { InvestigateAppStartDependencies } from '../types'; +import { InvestigateAppServices } from '../services/types'; + +export interface InvestigateAppKibanaContext { + core: CoreStart; + dependencies: { start: InvestigateAppStartDependencies }; + services: InvestigateAppServices; +} + +const useTypedKibana = () => { + return useKibana().services; +}; + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_local_storage.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_local_storage.ts new file mode 100644 index 00000000000000..8c67101337ef45 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_local_storage.ts @@ -0,0 +1,50 @@ +/* + * 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 { useEffect, useMemo, useState } from 'react'; + +function getFromStorage(keyName: string, defaultValue: T) { + const storedItem = window.localStorage.getItem(keyName); + + if (storedItem !== null) { + try { + return JSON.parse(storedItem) as T; + } catch (err) { + window.localStorage.removeItem(keyName); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${keyName}`); + } + } + return defaultValue; +} + +export function useLocalStorage(key: string, defaultValue: T | undefined) { + const [storedItem, setStoredItem] = useState(() => getFromStorage(key, defaultValue)); + + useEffect(() => { + function onStorageUpdate(e: StorageEvent) { + if (e.key === key) { + setStoredItem((prev) => getFromStorage(key, prev)); + } + } + window.addEventListener('storage', onStorageUpdate); + + return () => { + window.removeEventListener('storage', onStorageUpdate); + }; + }, [key]); + + return useMemo(() => { + return { + storedItem, + setStoredItem: (next: T) => { + window.localStorage.setItem(key, JSON.stringify(next)); + setStoredItem(() => next); + }, + }; + }, [key, storedItem]); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_memo_with_abort_signal.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_memo_with_abort_signal.ts new file mode 100644 index 00000000000000..ec1a87246f15ca --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_memo_with_abort_signal.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useEffect, useMemo, useRef } from 'react'; + +export function useMemoWithAbortSignal(cb: (signal: AbortSignal) => T, deps: any[]): T { + const controllerRef = useRef(new AbortController()); + + useEffect(() => { + const controller = controllerRef.current; + return () => { + controller.abort(); + }; + }, []); + + return useMemo(() => { + controllerRef.current.abort(); + controllerRef.current = new AbortController(); + return cb(controllerRef.current.signal); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_theme.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_theme.ts new file mode 100644 index 00000000000000..d0b4ce61edef4f --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_theme.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEuiTheme } from '@elastic/eui'; + +export function useTheme() { + return useEuiTheme().euiTheme; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/workflow_blocks/use_workflow_blocks.tsx b/x-pack/plugins/observability_solution/investigate_app/public/hooks/workflow_blocks/use_workflow_blocks.tsx new file mode 100644 index 00000000000000..c38051f523ad80 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/workflow_blocks/use_workflow_blocks.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { InvestigateWidgetCreate, WorkflowBlock } from '@kbn/investigate-plugin/common'; +import { compact } from 'lodash'; +import React from 'react'; +import { WorkflowBlocksControl } from '../../components/workflow_blocks_control'; + +export function useWorkflowBlocks({ + isTimelineEmpty, + dynamicBlocks, + start, + end, + onWidgetAdd, +}: { + isTimelineEmpty: boolean; + dynamicBlocks: WorkflowBlock[]; + start: string; + end: string; + onWidgetAdd: (create: InvestigateWidgetCreate) => Promise; +}) { + const blocks = isTimelineEmpty ? compact([]) : dynamicBlocks; + + if (!blocks.length) { + return null; + } + + return ; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/index.ts b/x-pack/plugins/observability_solution/investigate_app/public/index.ts new file mode 100644 index 00000000000000..8d4aeae6daaf88 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; + +import { InvestigateAppPlugin } from './plugin'; +import type { + InvestigateAppPublicSetup, + InvestigateAppPublicStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies, + ConfigSchema, +} from './types'; + +export type { InvestigateAppPublicSetup, InvestigateAppPublicStart }; + +export const plugin: PluginInitializer< + InvestigateAppPublicSetup, + InvestigateAppPublicStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies +> = (pluginInitializerContext: PluginInitializerContext) => + new InvestigateAppPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx b/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx new file mode 100644 index 00000000000000..53cae81320a7e5 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx @@ -0,0 +1,148 @@ +/* + * 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 { css } from '@emotion/css'; +import { + AppMountParameters, + APP_WRAPPER_CLASS, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin, + PluginInitializerContext, +} from '@kbn/core/public'; +import { INVESTIGATE_APP_ID } from '@kbn/deeplinks-observability/constants'; +import { i18n } from '@kbn/i18n'; +import type { Logger } from '@kbn/logging'; +import { once } from 'lodash'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import type { InvestigateAppServices } from './services/types'; +import type { + ConfigSchema, + InvestigateAppPublicSetup, + InvestigateAppPublicStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies, +} from './types'; + +const getCreateEsqlService = once(() => import('./services/esql').then((m) => m.createEsqlService)); + +export class InvestigateAppPlugin + implements + Plugin< + InvestigateAppPublicSetup, + InvestigateAppPublicStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies + > +{ + logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup, + pluginsSetup: InvestigateAppSetupDependencies + ): InvestigateAppPublicSetup { + coreSetup.application.register({ + id: INVESTIGATE_APP_ID, + title: i18n.translate('xpack.investigateApp.appTitle', { + defaultMessage: 'Observability AI Assistant', + }), + euiIconType: 'logoObservability', + appRoute: '/app/investigate', + category: DEFAULT_APP_CATEGORIES.observability, + visibleIn: [], + deepLinks: [ + { + id: 'investigate', + title: i18n.translate('xpack.investigateApp.investigateDeepLinkTitle', { + defaultMessage: 'Investigate', + }), + path: '/new', + }, + ], + mount: async (appMountParameters: AppMountParameters) => { + // Load application bundle and Get start services + const [{ Application }, [coreStart, pluginsStart], createEsqlService] = await Promise.all([ + import('./application'), + coreSetup.getStartServices(), + getCreateEsqlService(), + ]); + + const services: InvestigateAppServices = { + esql: createEsqlService({ + data: pluginsStart.data, + dataViews: pluginsStart.dataViews, + lens: pluginsStart.lens, + }), + }; + + ReactDOM.render( + , + appMountParameters.element + ); + + const appWrapperClassName = css` + overflow: auto; + `; + + const appWrapperElement = document.getElementsByClassName(APP_WRAPPER_CLASS)[1]; + + appWrapperElement.classList.add(appWrapperClassName); + + return () => { + ReactDOM.unmountComponentAtNode(appMountParameters.element); + appWrapperElement.classList.remove(appWrapperClassName); + }; + }, + }); + + const pluginsStartPromise = coreSetup + .getStartServices() + .then(([, pluginsStart]) => pluginsStart); + + pluginsSetup.investigate.register((registerWidget) => + Promise.all([ + pluginsStartPromise, + import('./widgets/register_widgets').then((m) => m.registerWidgets), + getCreateEsqlService(), + ]).then(([pluginsStart, registerWidgets, createEsqlService]) => { + registerWidgets({ + dependencies: { + setup: pluginsSetup, + start: pluginsStart, + }, + services: { + esql: createEsqlService({ + data: pluginsStart.data, + dataViews: pluginsStart.dataViews, + lens: pluginsStart.lens, + }), + }, + registerWidget, + }); + }) + ); + + return {}; + } + + start( + coreStart: CoreStart, + pluginsStart: InvestigateAppStartDependencies + ): InvestigateAppPublicStart { + return {}; + } +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/routes/config.tsx b/x-pack/plugins/observability_solution/investigate_app/public/routes/config.tsx new file mode 100644 index 00000000000000..ca68dde72b0b82 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/routes/config.tsx @@ -0,0 +1,58 @@ +/* + * 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 * as t from 'io-ts'; +import { createRouter, Outlet } from '@kbn/typed-react-router-config'; +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import { InvestigatePageTemplate } from '../components/investigate_page_template'; +import { InvestigateView } from '../components/investigate_view'; + +/** + * The array of route definitions to be used when the application + * creates the routes. + */ +const investigateRoutes = { + '/': { + element: ( + + + + ), + children: { + '/new': { + element: , + params: t.partial({ + query: t.partial({ + revision: t.string, + }), + }), + }, + '/{id}': { + element: , + params: t.intersection([ + t.type({ + path: t.type({ id: t.string }), + }), + t.partial({ + query: t.partial({ + revision: t.string, + }), + }), + ]), + }, + '/': { + element: , + }, + }, + }, +}; + +export type InvestigateRoutes = typeof investigateRoutes; + +export const investigateRouter = createRouter(investigateRoutes); + +export type InvestigateRouter = typeof investigateRouter; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/services/esql.ts b/x-pack/plugins/observability_solution/investigate_app/public/services/esql.ts new file mode 100644 index 00000000000000..5cc032f95f054e --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/services/esql.ts @@ -0,0 +1,131 @@ +/* + * 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 { lastValueFrom } from 'rxjs'; +import { getESQLAdHocDataview, getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { type DataView, ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import type { DatatableColumnType } from '@kbn/expressions-plugin/common'; +import type { ESFilter, ESQLSearchResponse } from '@kbn/es-types'; +import { AbortError } from '@kbn/kibana-utils-plugin/common'; +import type { Suggestion } from '@kbn/lens-plugin/public'; +import { v4 } from 'uuid'; +import type { InvestigateAppStartDependencies } from '../types'; +import { getKibanaColumns } from '../utils/get_kibana_columns'; + +interface DefaultQueryParams { + query: string; + filter?: ESFilter; + signal: AbortSignal; +} + +export interface EsqlColumnMeta { + id: string; + name: string; + meta: { type: DatatableColumnType }; +} + +export interface EsqlQueryMeta { + columns: EsqlColumnMeta[]; + suggestions: Array; + dataView: DataView; +} + +export interface EsqlService { + query: (params: DefaultQueryParams) => Promise; + queryWithMeta: ( + params: DefaultQueryParams + ) => Promise<{ query: ESQLSearchResponse; meta: EsqlQueryMeta }>; + meta: (params: DefaultQueryParams) => Promise; +} + +export function createEsqlService({ + data, + dataViews, + lens, +}: Pick): EsqlService { + async function runQuery({ + query, + signal, + dropNullColumns = true, + filter, + }: { + query: string; + signal: AbortSignal; + dropNullColumns?: boolean; + filter?: ESFilter; + }) { + const response = await lastValueFrom( + data.search.search( + { + params: { + query, + dropNullColumns, + filter, + }, + }, + { strategy: ESQL_SEARCH_STRATEGY, abortSignal: signal } + ) + ).then((searchResponse) => { + return searchResponse.rawResponse as unknown as ESQLSearchResponse; + }); + + return response; + } + + const esql: EsqlService = { + query: async ({ query, signal, filter }) => { + return await runQuery({ query, signal, filter }); + }, + queryWithMeta: async ({ query, signal, filter }) => { + const [meta, queryResult] = await Promise.all([ + esql.meta({ query, signal, filter }), + esql.query({ query, signal, filter }), + ]); + + return { + query: queryResult, + meta, + }; + }, + meta: async ({ query, signal, filter }) => { + const indexPattern = getIndexPatternFromESQLQuery(query); + + const [response, lensHelper, dataView] = await Promise.all([ + runQuery({ query: `${query} | LIMIT 0`, signal, dropNullColumns: false, filter }), + lens.stateHelperApi(), + getESQLAdHocDataview(indexPattern, dataViews), + ]); + + const columns = getKibanaColumns(response.columns ?? []); + + const suggestionsFromLensHelper = await lensHelper.suggestions( + { + dataViewSpec: dataView.toSpec(), + fieldName: '', + textBasedColumns: columns, + query: { + esql: query, + }, + }, + dataView + ); + + if (signal.aborted) { + throw new AbortError(); + } + + return { + columns, + suggestions: + suggestionsFromLensHelper?.map((suggestion) => ({ id: v4(), ...suggestion })) ?? [], + dataView, + }; + }, + }; + + return esql; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/services/types.ts b/x-pack/plugins/observability_solution/investigate_app/public/services/types.ts new file mode 100644 index 00000000000000..149cacf2434a7f --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/services/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EsqlService } from './esql'; + +export interface InvestigateAppServices { + esql: EsqlService; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/types.ts b/x-pack/plugins/observability_solution/investigate_app/public/types.ts new file mode 100644 index 00000000000000..af3657b712b597 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/types.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { + DataViewsPublicPluginSetup, + DataViewsPublicPluginStart, +} from '@kbn/data-views-plugin/public'; +import type { + DatasetQualityPluginSetup, + DatasetQualityPluginStart, +} from '@kbn/dataset-quality-plugin/public'; +import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { + InvestigatePublicSetup, + InvestigatePublicStart, +} from '@kbn/investigate-plugin/public'; +import type { LensPublicSetup, LensPublicStart } from '@kbn/lens-plugin/public'; +import type { + ObservabilitySharedPluginSetup, + ObservabilitySharedPluginStart, +} from '@kbn/observability-shared-plugin/public'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ConfigSchema {} + +export interface InvestigateAppSetupDependencies { + investigate: InvestigatePublicSetup; + observabilityShared: ObservabilitySharedPluginSetup; + lens: LensPublicSetup; + dataViews: DataViewsPublicPluginSetup; + data: DataPublicPluginSetup; + embeddable: EmbeddableSetup; + contentManagement: {}; + datasetQuality: DatasetQualityPluginSetup; + unifiedSearch: {}; + uiActions: UiActionsSetup; + security: SecurityPluginSetup; +} + +export interface InvestigateAppStartDependencies { + investigate: InvestigatePublicStart; + observabilityShared: ObservabilitySharedPluginStart; + lens: LensPublicStart; + dataViews: DataViewsPublicPluginStart; + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + contentManagement: ContentManagementPublicStart; + datasetQuality: DatasetQualityPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; + uiActions: UiActionsStart; + security: SecurityPluginStart; +} + +export interface InvestigateAppPublicSetup {} + +export interface InvestigateAppPublicStart {} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/utils/find_scrollable_parent.ts b/x-pack/plugins/observability_solution/investigate_app/public/utils/find_scrollable_parent.ts new file mode 100644 index 00000000000000..4907d610e88e35 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/utils/find_scrollable_parent.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function findScrollableParent(parent: HTMLElement | null) { + while (parent && parent !== window.document.body) { + if (parent.scrollHeight > parent.clientHeight) { + const computed = getComputedStyle(parent); + if (computed.overflowY === 'auto' || computed.overflowY === 'scroll') { + return parent; + } + } + parent = parent.parentElement; + } + + return window.document.documentElement; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/utils/get_data_table_from_esql_response.ts b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_data_table_from_esql_response.ts new file mode 100644 index 00000000000000..98f93b9d88a6f1 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_data_table_from_esql_response.ts @@ -0,0 +1,42 @@ +/* + * 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 type { Datatable } from '@kbn/expressions-plugin/common'; +import type { ESQLColumn, ESQLRow } from '@kbn/es-types'; +import type { EsqlColumnMeta } from '../services/esql'; +import { getKibanaColumns } from './get_kibana_columns'; + +type Primitive = string | boolean | number | null; + +export function getDatatableFromEsqlResponse({ + columns, + values, + all_columns: allColumns, +}: { + all_columns?: ESQLColumn[]; + columns: ESQLColumn[]; + values: ESQLRow[]; +}): Datatable { + const kibanaColumns: EsqlColumnMeta[] = getKibanaColumns(allColumns ?? columns); + + const datatable: Datatable = { + columns: kibanaColumns, + rows: values.map((row) => { + return row.reduce>((prev, current, index) => { + const column = columns[index]; + prev[column.name] = current as Primitive | Primitive[]; + return prev; + }, {}); + }), + type: 'datatable', + meta: { + type: 'esql', + }, + }; + + return datatable; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/utils/get_es_filter_from_overrides.ts b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_es_filter_from_overrides.ts new file mode 100644 index 00000000000000..483be2f0191431 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_es_filter_from_overrides.ts @@ -0,0 +1,36 @@ +/* + * 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 { type BoolQuery, buildEsQuery, type Query, type Filter } from '@kbn/es-query'; + +export function getEsFilterFromOverrides({ + query, + filters, + timeRange, +}: { + query?: Query; + filters?: Filter[]; + timeRange?: { + from: string; + to: string; + }; +}): { bool: BoolQuery } { + const esFilter = buildEsQuery(undefined, query ?? [], filters ?? []); + + if (timeRange) { + esFilter.bool.filter.push({ + range: { + '@timestamp': { + gte: timeRange.from, + lte: timeRange.to, + }, + }, + }); + } + + return esFilter; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/utils/get_kibana_columns.ts b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_kibana_columns.ts new file mode 100644 index 00000000000000..e11acfc9ea8ab1 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_kibana_columns.ts @@ -0,0 +1,21 @@ +/* + * 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 { ESQLColumn } from '@kbn/es-types'; +import { esFieldTypeToKibanaFieldType } from '@kbn/field-types'; +import { DatatableColumnType } from '@kbn/expressions-plugin/common'; +import { EsqlColumnMeta } from '../services/esql'; + +export function getKibanaColumns(columns: ESQLColumn[]): EsqlColumnMeta[] { + return ( + columns.map(({ name, type }) => ({ + id: name, + name, + meta: { type: esFieldTypeToKibanaFieldType(type) as DatatableColumnType }, + })) ?? [] + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.ts b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.ts new file mode 100644 index 00000000000000..0483d771954c06 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { Datatable } from '@kbn/expressions-plugin/common'; +import type { Suggestion, TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; +import { mapValues } from 'lodash'; +import { v4 } from 'uuid'; + +export function getLensAttrsForSuggestion({ + query, + suggestion, + dataView, + table, +}: { + query: string; + suggestion: Suggestion; + dataView: DataView; + table?: Datatable; +}): TypedLensByValueInput { + const attrs = getLensAttributesFromSuggestion({ + filters: [], + query: { + esql: query, + }, + suggestion, + dataView, + }) as TypedLensByValueInput['attributes']; + + const lensEmbeddableInput: TypedLensByValueInput = { + attributes: attrs, + id: v4(), + }; + + if (!table) { + return lensEmbeddableInput; + } + + const textBased = attrs.state.datasourceStates.textBased; + + if (!textBased?.layers) { + throw new Error('Expected layers to exist for datasourceStates.textBased'); + } + + textBased.layers = mapValues(textBased.layers, (value) => { + return { ...value, table }; + }); + + return lensEmbeddableInput; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/utils/get_overrides_from_global_parameters.tsx b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_overrides_from_global_parameters.tsx new file mode 100644 index 00000000000000..6e46189648d864 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_overrides_from_global_parameters.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { isEqual } from 'lodash'; +import type { GlobalWidgetParameters } from '@kbn/investigate-plugin/public'; +import type { Filter } from '@kbn/es-query'; +import objectHash from 'object-hash'; +import { i18n } from '@kbn/i18n'; +import { PrettyDuration } from '@elastic/eui'; +import type { InvestigateWidgetGridItemOverride } from '../components/investigate_widget_grid'; + +enum OverrideType { + query = 'query', + timeRange = 'timeRange', + filters = 'filters', +} + +function getIdForFilter(filter: Filter) { + return objectHash({ meta: filter.meta, query: filter.query }); +} + +function getLabelForFilter(filter: Filter) { + return ( + filter.meta.alias ?? + filter.meta.key ?? + JSON.stringify({ meta: filter.meta, query: filter.query }) + ); +} + +export function getOverridesFromGlobalParameters( + itemParameters: GlobalWidgetParameters, + globalParameters: GlobalWidgetParameters, + uiSettingsDateFormat: string +) { + const overrides: InvestigateWidgetGridItemOverride[] = []; + + if (!isEqual(itemParameters.query, globalParameters.query)) { + overrides.push({ + id: OverrideType.query, + label: itemParameters.query.query + ? itemParameters.query.query + : i18n.translate('xpack.investigateApp.overrides.noQuery', { defaultMessage: 'No query' }), + }); + } + + if (!isEqual(itemParameters.timeRange, globalParameters.timeRange)) { + overrides.push({ + id: OverrideType.timeRange, + label: ( + + ), + }); + } + + if (!isEqual(itemParameters.filters, globalParameters.filters)) { + if (!itemParameters.filters.length) { + overrides.push({ + id: OverrideType.filters, + label: i18n.translate('xpack.investigateApp.overrides.noFilters', { + defaultMessage: 'No filters', + }), + }); + } + + itemParameters.filters.forEach((filter) => { + overrides.push({ + id: `${OverrideType.filters}_${getIdForFilter(filter)}`, + label: getLabelForFilter(filter), + }); + }); + } + + return overrides; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/create_embeddable_widget.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/create_embeddable_widget.ts new file mode 100644 index 00000000000000..61e99df8b28b24 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/create_embeddable_widget.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createWidgetFactory } from '@kbn/investigate-plugin/public'; +import { EMBEDDABLE_WIDGET_NAME } from '../../constants'; +import { EmbeddableWidgetParameters } from './types'; + +export const createEmbeddableWidget = + createWidgetFactory(EMBEDDABLE_WIDGET_NAME); diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/register_embeddable_widget.tsx b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/register_embeddable_widget.tsx new file mode 100644 index 00000000000000..02e69d3226ab89 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/register_embeddable_widget.tsx @@ -0,0 +1,213 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { GlobalWidgetParameters } from '@kbn/investigate-plugin/public'; +import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { v4 } from 'uuid'; +import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; +import { EMBEDDABLE_WIDGET_NAME } from '../../constants'; +import { useKibana } from '../../hooks/use_kibana'; +import { RegisterWidgetOptions } from '../register_widgets'; +import { EmbeddableWidgetParameters } from './types'; +import { ErrorMessage } from '../../components/error_message'; + +const embeddableClassName = css` + height: 100%; + > [data-shared-item] { + height: 100%; + } +`; + +type Props = EmbeddableWidgetParameters & GlobalWidgetParameters; + +type ParentApi = ReturnType['getParentApi']>; + +function ReactEmbeddable({ + type, + config, + query, + filters, + timeRange: { from, to }, + savedObjectId, +}: Props) { + const configWithOverrides = useMemo(() => { + return { + ...config, + query, + filters, + timeRange: { + from, + to, + }, + }; + }, [config, query, filters, from, to]); + + const configWithOverridesRef = useRef(configWithOverrides); + + configWithOverridesRef.current = configWithOverrides; + + const api = useMemo(() => { + return { + getSerializedStateForChild: () => ({ rawState: configWithOverridesRef.current }), + }; + }, []); + + return ( + api} + maybeId={savedObjectId} + onAnyStateChange={(state) => { + // console.log('onAnyStateChange', state); + }} + onApiAvailable={(childApi) => { + // console.log('onApiAvailable', childApi); + }} + hidePanelChrome + /> + ); +} + +function LegacyEmbeddable({ + type, + config, + query, + filters, + timeRange: { from, to }, + savedObjectId, +}: Props) { + const { + dependencies: { + start: { embeddable }, + }, + } = useKibana(); + + const [targetElement, setTargetElement] = useState(null); + + const embeddableInstanceAsync = useAbortableAsync(async () => { + const factory = embeddable.getEmbeddableFactory(type); + + if (!factory) { + throw new Error(`Cannot find embeddable factory for ${type}`); + } + + const configWithId = { + id: savedObjectId ?? v4(), + ...config, + }; + + const configWithOverrides = { + ...configWithId, + query, + filters, + timeRange: { + from, + to, + }, + }; + + if (savedObjectId) { + return factory.createFromSavedObject(configWithOverrides.id, configWithOverrides); + } + + const instance = await factory.create(configWithOverrides); + + return instance; + }, [type, savedObjectId, config, from, to, embeddable, filters, query]); + + const embeddableInstance = embeddableInstanceAsync.value; + + useEffect(() => { + if (!targetElement || !embeddableInstance) { + return; + } + + embeddableInstance.render(targetElement); + + return () => {}; + }, [embeddableInstance, targetElement]); + + useEffect(() => { + return () => { + if (embeddableInstance) { + embeddableInstance.destroy(); + } + }; + }, [embeddableInstance]); + + if (embeddableInstanceAsync.error) { + return ; + } + + if (!embeddableInstance) { + return ; + } + + return ( +
{ + setTargetElement(element); + }} + /> + ); +} + +function EmbeddableWidget(props: Props) { + const { + dependencies: { + start: { embeddable }, + }, + } = useKibana(); + + if (embeddable.reactEmbeddableRegistryHasKey(props.type)) { + return ; + } + + return ; +} + +export function registerEmbeddableWidget({ registerWidget }: RegisterWidgetOptions) { + registerWidget( + { + type: EMBEDDABLE_WIDGET_NAME, + description: 'Display a saved embeddable', + schema: { + type: 'object', + properties: { + type: { + type: 'string', + }, + config: { + type: 'object', + }, + savedObjectId: { + type: 'string', + }, + }, + required: ['type', 'config'], + } as const, + }, + async ({ parameters, signal }) => { + return {}; + }, + ({ widget }) => { + const parameters = { + type: widget.parameters.type, + config: widget.parameters.config, + savedObjectId: widget.parameters.savedObjectId, + timeRange: widget.parameters.timeRange, + filters: widget.parameters.filters, + query: widget.parameters.query, + }; + + return ; + } + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/types.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/types.ts new file mode 100644 index 00000000000000..d3ecd0379c7162 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/types.ts @@ -0,0 +1,18 @@ +/* + * 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 type { InvestigateWidget, InvestigateWidgetCreate } from '@kbn/investigate-plugin/common'; + +export interface EmbeddableWidgetParameters { + type: string; + savedObjectId?: string; + config: Record; +} + +export type EmbeddableWidgetCreate = InvestigateWidgetCreate; + +export type EmbeddableWidget = InvestigateWidget; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/get_date_histogram_results.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/get_date_histogram_results.ts new file mode 100644 index 00000000000000..0ddfe6e38b887d --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/get_date_histogram_results.ts @@ -0,0 +1,54 @@ +/* + * 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 type { Suggestion } from '@kbn/lens-plugin/public'; +import type { ESQLColumn } from '@kbn/es-types'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import type { EsqlService } from '../../services/esql'; + +export async function getDateHistogramResults({ + query, + esql, + timeRange, + filter, + suggestion, + signal, + columns, +}: { + query: string; + esql: EsqlService; + timeRange: { + from: string; + to: string; + }; + filter: QueryDslQueryContainer; + suggestion: Suggestion; + signal: AbortSignal; + columns: ESQLColumn[]; +}) { + const groupingExpression = `BUCKET(@timestamp, 50, "${timeRange.from}", "${timeRange.to}")`; + const dateHistoQuery = `${query} | STATS count = COUNT(*) BY ${groupingExpression}`; + + const dateHistoResponse = + suggestion.visualizationId === 'lnsDatatable' && + columns.find((column) => column.name === '@timestamp') + ? await esql.queryWithMeta({ + query: dateHistoQuery, + signal, + filter, + }) + : undefined; + + return dateHistoResponse + ? { + columns: dateHistoResponse.query.columns, + values: dateHistoResponse.query.values, + query: dateHistoQuery, + groupingExpression, + } + : undefined; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/register_esql_widget.tsx b/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/register_esql_widget.tsx new file mode 100644 index 00000000000000..c2ba65d19aff9a --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/register_esql_widget.tsx @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useEffect, useMemo } from 'react'; +import type { Suggestion } from '@kbn/lens-plugin/public'; +import { css } from '@emotion/css'; +import type { + EsqlWidgetParameters, + GlobalWidgetParameters, + WidgetRenderAPI, +} from '@kbn/investigate-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { ESQLSearchResponse } from '@kbn/es-types'; +import { ESQLDataGrid } from '@kbn/esql-datagrid/public'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; +import { ESQL_WIDGET_NAME } from '../../constants'; +import type { RegisterWidgetOptions } from '../register_widgets'; +import { useKibana } from '../../hooks/use_kibana'; +import { getLensAttrsForSuggestion } from '../../utils/get_lens_attrs_for_suggestion'; +import { getDatatableFromEsqlResponse } from '../../utils/get_data_table_from_esql_response'; +import { getEsFilterFromOverrides } from '../../utils/get_es_filter_from_overrides'; +import { ErrorMessage } from '../../components/error_message'; +import { getDateHistogramResults } from './get_date_histogram_results'; + +const lensClassName = css` + height: 100%; +`; + +export function EsqlWidget({ + suggestion, + dataView, + esqlQuery, + columns, + allColumns, + values, + blocks, + dateHistogramResults, +}: { + suggestion: Suggestion; + dataView: DataView; + esqlQuery: string; + columns: ESQLSearchResponse['columns']; + allColumns: ESQLSearchResponse['all_columns']; + values: ESQLSearchResponse['values']; + blocks: WidgetRenderAPI['blocks']; + dateHistogramResults?: { + query: string; + columns: ESQLSearchResponse['columns']; + values: ESQLSearchResponse['values']; + groupingExpression: string; + }; +}) { + const { + dependencies: { + start: { lens }, + }, + } = useKibana(); + + const datatable = useMemo(() => { + return getDatatableFromEsqlResponse({ + columns, + values, + all_columns: allColumns, + }); + }, [columns, values, allColumns]); + + const input = useMemo(() => { + return getLensAttrsForSuggestion({ + suggestion, + dataView, + query: esqlQuery, + table: datatable, + }); + }, [suggestion, dataView, esqlQuery, datatable]); + + const memoizedQueryObject = useMemo(() => { + return { esql: esqlQuery }; + }, [esqlQuery]); + + useEffect(() => { + if (datatable.columns.find((column) => column.name === 'message')) { + return blocks.publish([ + { + id: 'pattern_analysis', + loading: false, + content: i18n.translate('xpack.investigateApp.esqlWidget.runPatternAnalysis', { + defaultMessage: 'Analyze log patterns', + }), + }, + ]); + } + }, [blocks, datatable]); + + const initialColumns = useMemo(() => { + const timestampColumn = datatable.columns.find((column) => column.name === '@timestamp'); + const messageColumn = datatable.columns.find((column) => column.name === 'message'); + + if (datatable.columns.length > 10 && timestampColumn && messageColumn) { + const hasDataForBothColumns = datatable.rows.every((row) => { + const timestampValue = row['@timestamp']; + const messageValue = row.message; + + return timestampValue !== null && timestampValue !== undefined && !!messageValue; + }); + + if (hasDataForBothColumns) { + return [timestampColumn, messageColumn]; + } + } + return undefined; + }, [datatable.columns, datatable.rows]); + + const previewInput = useAbortableAsync( + async ({ signal }) => { + if (!dateHistogramResults) { + return undefined; + } + + const lensHelper = await lens.stateHelperApi(); + + const suggestionsFromLensHelper = await lensHelper.suggestions( + { + dataViewSpec: dataView.toSpec(), + fieldName: '', + textBasedColumns: [ + { + id: dateHistogramResults.groupingExpression, + name: i18n.translate('xpack.investigateApp.esqlWidget.groupedByDateLabel', { + defaultMessage: '@timestamp', + }), + meta: { + type: 'date', + }, + }, + { + id: 'count', + name: 'count', + meta: { + type: 'number', + }, + }, + ], + query: { + esql: dateHistogramResults.query, + }, + }, + dataView, + ['lnsDatatable'] + ); + + const suggestionForHistogram = suggestionsFromLensHelper?.[0]; + + if (!suggestionForHistogram) { + return undefined; + } + + return getLensAttrsForSuggestion({ + suggestion: suggestionForHistogram, + dataView, + query: dateHistogramResults.query, + table: getDatatableFromEsqlResponse({ + columns: dateHistogramResults.columns, + values: dateHistogramResults.values, + }), + }); + }, + [dataView, lens, dateHistogramResults] + ); + + if (input.attributes.visualizationType === 'lnsDatatable') { + let innerElement: React.ReactElement; + if (previewInput.error) { + innerElement = ; + } else if (previewInput.value) { + innerElement = ; + } else { + innerElement = ; + } + return ( + + div { + height: 128px; + } + `} + > + {innerElement} + + + + + + ); + } + + return ; +} + +export function registerEsqlWidget({ + dependencies: { + setup: { investigate }, + }, + services, + registerWidget, +}: RegisterWidgetOptions) { + registerWidget( + { + type: ESQL_WIDGET_NAME, + description: 'Visualize an ES|QL query', + schema: { + type: 'object', + properties: { + esql: { + description: 'The ES|QL query', + type: 'string', + }, + }, + required: ['esql'], + } as const, + }, + async ({ parameters, signal }) => { + const { + esql: esqlQuery, + query, + filters, + timeRange, + suggestion: suggestionFromParameters, + } = parameters as EsqlWidgetParameters & GlobalWidgetParameters; + + const esql = await services.esql; + + const esFilters = [ + getEsFilterFromOverrides({ + query, + filters, + timeRange, + }), + ]; + + const getFilter = () => ({ + bool: { + filter: [...esFilters], + }, + }); + + const mainResponse = await esql.queryWithMeta({ + query: esqlQuery, + signal, + filter: getFilter(), + }); + + const suggestion = suggestionFromParameters || mainResponse.meta.suggestions[0]; + + const dateHistoResponse = await getDateHistogramResults({ + query: esqlQuery, + columns: mainResponse.query.columns, + esql, + filter: getFilter(), + signal, + suggestion, + timeRange, + }); + + return { + main: { + columns: mainResponse.query.columns, + values: mainResponse.query.values, + suggestion, + dataView: mainResponse.meta.dataView, + }, + dateHistogram: dateHistoResponse, + }; + }, + ({ widget, blocks }) => { + const { + main: { dataView, columns, values, suggestion }, + dateHistogram, + } = widget.data; + return ( + + ); + } + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/create_note_widget.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/create_note_widget.ts new file mode 100644 index 00000000000000..dde50fe19ae2c3 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/create_note_widget.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createWidgetFactory } from '@kbn/investigate-plugin/public'; +import { NOTE_WIDGET_NAME } from '../../constants'; +import type { NoteWidgetCreateParameters } from './types'; + +export const createNoteWidget = createWidgetFactory(NOTE_WIDGET_NAME); diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/index.tsx new file mode 100644 index 00000000000000..ad9bab4eeffe2d --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/index.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { ChromeOption } from '@kbn/investigate-plugin/public'; +import { RegisterWidgetOptions } from '../register_widgets'; +import { NOTE_WIDGET_NAME } from '../../constants'; +import { NoteWidget } from '../../components/note_widget'; + +export function registerNoteWidget(options: RegisterWidgetOptions) { + options.registerWidget( + { + type: NOTE_WIDGET_NAME, + description: '', + chrome: ChromeOption.disabled, + schema: { + type: 'object', + properties: { + note: { + type: 'string', + }, + user: { + type: 'object', + properties: { + username: { + type: 'string', + }, + full_name: { + type: 'string', + }, + }, + required: ['username'], + }, + }, + required: ['note', 'user'], + } as const, + }, + () => Promise.resolve({}), + ({ widget, onDelete }) => { + const { user, note } = widget.parameters; + + return {}} />; + } + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/types.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/types.ts new file mode 100644 index 00000000000000..bbc65f73f2800f --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/note_widget/types.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { InvestigateWidget } from '@kbn/investigate-plugin/common'; +import type { AuthenticatedUser } from '@kbn/core/public'; + +export interface NoteWidgetCreateParameters { + user: Pick; + note: string; +} + +export type NoteWidget = InvestigateWidget; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/register_widgets.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/register_widgets.ts new file mode 100644 index 00000000000000..e28503e2fc9d2e --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/widgets/register_widgets.ts @@ -0,0 +1,28 @@ +/* + * 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 type { RegisterWidget } from '@kbn/investigate-plugin/public/types'; +import type { InvestigateAppServices } from '../services/types'; +import type { InvestigateAppSetupDependencies, InvestigateAppStartDependencies } from '../types'; +import { registerEmbeddableWidget } from './embeddable_widget/register_embeddable_widget'; +import { registerEsqlWidget } from './esql_widget/register_esql_widget'; +import { registerNoteWidget } from './note_widget'; + +export interface RegisterWidgetOptions { + dependencies: { + setup: InvestigateAppSetupDependencies; + start: InvestigateAppStartDependencies; + }; + services: InvestigateAppServices; + registerWidget: RegisterWidget; +} + +export function registerWidgets(options: RegisterWidgetOptions) { + registerEsqlWidget(options); + registerEmbeddableWidget(options); + registerNoteWidget(options); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/config.ts b/x-pack/plugins/observability_solution/investigate_app/server/config.ts new file mode 100644 index 00000000000000..e49356ade1c1a5 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/config.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, type TypeOf } from '@kbn/config-schema'; + +export const config = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type InvestigateAppConfig = TypeOf; diff --git a/x-pack/plugins/observability_solution/investigate_app/server/index.ts b/x-pack/plugins/observability_solution/investigate_app/server/index.ts new file mode 100644 index 00000000000000..0ffe06b430cea9 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/index.ts @@ -0,0 +1,28 @@ +/* + * 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 type { PluginInitializer, PluginInitializerContext } from '@kbn/core/server'; +import { InvestigateAppConfig } from './config'; + +import { InvestigateAppPlugin } from './plugin'; +import type { + InvestigateAppServerSetup, + InvestigateAppServerStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies, +} from './types'; + +export type { InvestigateAppServerRouteRepository } from './routes/get_global_investigate_app_server_route_repository'; + +export type { InvestigateAppServerSetup, InvestigateAppServerStart }; + +export const plugin: PluginInitializer< + InvestigateAppServerSetup, + InvestigateAppServerStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies +> = async (pluginInitializerContext: PluginInitializerContext) => + new InvestigateAppPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/observability_solution/investigate_app/server/plugin.ts b/x-pack/plugins/observability_solution/investigate_app/server/plugin.ts new file mode 100644 index 00000000000000..7b8a1772858ec5 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/plugin.ts @@ -0,0 +1,64 @@ +/* + * 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 type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import { mapValues } from 'lodash'; +import { registerServerRoutes } from './routes/register_routes'; +import { InvestigateAppRouteHandlerResources } from './routes/types'; +import type { + ConfigSchema, + InvestigateAppServerSetup, + InvestigateAppServerStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies, +} from './types'; + +export class InvestigateAppPlugin + implements + Plugin< + InvestigateAppServerSetup, + InvestigateAppServerStart, + InvestigateAppSetupDependencies, + InvestigateAppStartDependencies + > +{ + logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup, + pluginsSetup: InvestigateAppSetupDependencies + ): InvestigateAppServerSetup { + const routeHandlerPlugins = mapValues(pluginsSetup, (value, key) => { + return { + setup: value, + start: () => + coreSetup.getStartServices().then((services) => { + const [, pluginsStartContracts] = services; + return (pluginsStartContracts as any)[key]; + }), + }; + }) as InvestigateAppRouteHandlerResources['plugins']; + + registerServerRoutes({ + core: coreSetup, + logger: this.logger, + dependencies: { + plugins: routeHandlerPlugins, + }, + }); + + return {}; + } + + start(core: CoreStart, pluginsStart: InvestigateAppStartDependencies): InvestigateAppServerStart { + return {}; + } +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/create_investigate_app_server_route.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/create_investigate_app_server_route.ts new file mode 100644 index 00000000000000..b702222068f0a4 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/create_investigate_app_server_route.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createServerRouteFactory } from '@kbn/server-route-repository'; +import type { + InvestigateAppRouteCreateOptions, + InvestigateAppRouteHandlerResources, +} from './types'; + +export const createInvestigateAppServerRoute = createServerRouteFactory< + InvestigateAppRouteHandlerResources, + InvestigateAppRouteCreateOptions +>(); diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts new file mode 100644 index 00000000000000..63931f717385c9 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getGlobalInvestigateAppServerRouteRepository() { + return {}; +} + +export type InvestigateAppServerRouteRepository = ReturnType< + typeof getGlobalInvestigateAppServerRouteRepository +>; diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/register_routes.ts new file mode 100644 index 00000000000000..8823e83d671648 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/register_routes.ts @@ -0,0 +1,31 @@ +/* + * 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 type { CoreSetup } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import { registerRoutes } from '@kbn/server-route-repository'; +import { getGlobalInvestigateAppServerRouteRepository } from './get_global_investigate_app_server_route_repository'; +import type { InvestigateAppRouteHandlerResources } from './types'; + +export function registerServerRoutes({ + core, + logger, + dependencies, +}: { + core: CoreSetup; + logger: Logger; + dependencies: Omit< + InvestigateAppRouteHandlerResources, + 'request' | 'context' | 'logger' | 'params' + >; +}) { + registerRoutes({ + core, + logger, + repository: getGlobalInvestigateAppServerRouteRepository(), + dependencies, + }); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts new file mode 100644 index 00000000000000..2e882296adff0f --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/types.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoreStart, + CustomRequestHandlerContext, + IScopedClusterClient, + IUiSettingsClient, + KibanaRequest, + SavedObjectsClientContract, +} from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import type { InvestigateAppSetupDependencies, InvestigateAppStartDependencies } from '../types'; + +export type InvestigateAppRequestHandlerContext = Omit< + CustomRequestHandlerContext<{}>, + 'core' | 'resolve' +> & { + core: Promise<{ + elasticsearch: { + client: IScopedClusterClient; + }; + uiSettings: { + client: IUiSettingsClient; + globalClient: IUiSettingsClient; + }; + savedObjects: { + client: SavedObjectsClientContract; + }; + coreStart: CoreStart; + }>; +}; + +export interface InvestigateAppRouteHandlerResources { + request: KibanaRequest; + context: InvestigateAppRequestHandlerContext; + logger: Logger; + plugins: { + [key in keyof InvestigateAppSetupDependencies]: { + setup: Required[key]; + }; + } & { + [key in keyof InvestigateAppStartDependencies]: { + start: () => Promise[key]>; + }; + }; +} + +export interface InvestigateAppRouteCreateOptions { + options: { + timeout?: { + idleSocket?: number; + }; + tags: []; + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/types.ts b/x-pack/plugins/observability_solution/investigate_app/server/types.ts new file mode 100644 index 00000000000000..6fa1196b23b743 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/types.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ConfigSchema {} + +export interface InvestigateAppSetupDependencies {} + +export interface InvestigateAppStartDependencies {} + +export interface InvestigateAppServerSetup {} + +export interface InvestigateAppServerStart {} diff --git a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json new file mode 100644 index 00000000000000..f2ea50270fb4f9 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json @@ -0,0 +1,56 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "typings/**/*", + "public/**/*.json", + "server/**/*", + ".storybook/**/*" + ], + "exclude": [ + "target/**/*", + ".storybook/**/*.js" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/react-kibana-context-theme", + "@kbn/shared-ux-link-redirect-app", + "@kbn/kibana-react-plugin", + "@kbn/typed-react-router-config", + "@kbn/i18n", + "@kbn/embeddable-plugin", + "@kbn/observability-ai-assistant-plugin", + "@kbn/lens-plugin", + "@kbn/utility-types", + "@kbn/esql", + "@kbn/esql-utils", + "@kbn/data-plugin", + "@kbn/es-types", + "@kbn/field-types", + "@kbn/expressions-plugin", + "@kbn/deeplinks-observability", + "@kbn/logging", + "@kbn/data-views-plugin", + "@kbn/observability-shared-plugin", + "@kbn/config-schema", + "@kbn/investigate-plugin", + "@kbn/dataset-quality-plugin", + "@kbn/utility-types-jest", + "@kbn/content-management-plugin", + "@kbn/kibana-utils-plugin", + "@kbn/visualization-utils", + "@kbn/unified-search-plugin", + "@kbn/es-query", + "@kbn/server-route-repository", + "@kbn/management-settings-ids", + "@kbn/security-plugin", + "@kbn/ui-actions-plugin", + "@kbn/esql-datagrid", + "@kbn/std" + ], +} diff --git a/yarn.lock b/yarn.lock index 8920fc52b086a9..2797a3bea4ab8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5266,6 +5266,10 @@ version "0.0.0" uid "" +"@kbn/investigate-app-plugin@link:x-pack/plugins/observability_solution/investigate_app": + version "0.0.0" + uid "" + "@kbn/investigate-plugin@link:x-pack/plugins/observability_solution/investigate": version "0.0.0" uid "" @@ -13017,6 +13021,11 @@ bare-path@^2.0.0, bare-path@^2.1.0: dependencies: bare-os "^2.1.0" +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -14884,6 +14893,13 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + css-loader@^3.4.2, css-loader@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" @@ -19467,6 +19483,14 @@ html-webpack-plugin@^4.0.0: tapable "^1.1.3" util.promisify "1.0.0" +html2canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + html@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/html/-/html-1.0.0.tgz#a544fa9ea5492bfb3a2cca8210a10be7b5af1f61" @@ -30012,6 +30036,13 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -31129,6 +31160,13 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid-browser@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410"