[8.10] [Observability AI Assistant] Save/open Lens embeddable (#165542) (#165548)

# Backport

This will backport the following commits from `main` to `8.10`:
- [[Observability AI Assistant] Save/open Lens embeddable
(#165542)](https://github.com/elastic/kibana/pull/165542)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Dario
Gieselaar","email":"dario.gieselaar@elastic.co"},"sourceCommit":{"committedDate":"2023-09-03T10:48:15Z","message":"[Observability
AI Assistant] Save/open Lens embeddable
(#165542)","sha":"1c531919721c463d80b22fb796bba82861508bd6","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v8.10.0","v8.11.0"],"number":165542,"url":"https://github.com/elastic/kibana/pull/165542","mergeCommit":{"message":"[Observability
AI Assistant] Save/open Lens embeddable
(#165542)","sha":"1c531919721c463d80b22fb796bba82861508bd6"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/165542","number":165542,"mergeCommit":{"message":"[Observability
AI Assistant] Save/open Lens embeddable
(#165542)","sha":"1c531919721c463d80b22fb796bba82861508bd6"}}]}]
BACKPORT-->

Co-authored-by: Dario Gieselaar <dario.gieselaar@elastic.co>
This commit is contained in:
Kibana Machine 2023-09-03 07:57:40 -04:00 committed by GitHub
parent a5f3f46f2d
commit 988ac799db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 93 additions and 47 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { last } from 'lodash';
import {
EuiFlexGroup,
@ -104,49 +104,40 @@ export function ChatBody({
: '100%'};
`;
const [stickToBottom, setStickToBottom] = useState(true);
const isAtBottom = (parent: HTMLElement) =>
parent.scrollTop + parent.clientHeight >= parent.scrollHeight;
useEffect(() => {
const parent = timelineContainerRef.current?.parentElement;
if (!parent) {
return;
}
let rafId: number | undefined;
const isAtBottom = () => parent.scrollTop >= parent.scrollHeight - parent.offsetHeight;
const stick = () => {
if (!isAtBottom()) {
parent.scrollTop = parent.scrollHeight - parent.offsetHeight;
}
rafId = requestAnimationFrame(stick);
};
const unstick = () => {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = undefined;
}
};
const onScroll = (event: Event) => {
if (isAtBottom()) {
stick();
} else {
unstick();
}
};
function onScroll() {
setStickToBottom(isAtBottom(parent!));
}
parent.addEventListener('scroll', onScroll);
stick();
return () => {
unstick();
parent.removeEventListener('scroll', onScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timelineContainerRef.current]);
useEffect(() => {
const parent = timelineContainerRef.current?.parentElement;
if (!parent) {
return;
}
if (stickToBottom) {
parent.scrollTop = parent.scrollHeight;
}
});
const handleCopyConversation = () => {
const content = JSON.stringify({ title, messages });
@ -210,7 +201,10 @@ export function ChatBody({
<ChatPromptEditor
loading={isLoading}
disabled={!connectors.selectedConnector || !hasCorrectLicense}
onSubmit={timeline.onSubmit}
onSubmit={(message) => {
setStickToBottom(true);
return timeline.onSubmit(message);
}}
/>
<EuiSpacer size="s" />
</EuiPanel>

View file

@ -16,7 +16,6 @@ import classNames from 'classnames';
import type { Code, InlineCode, Parent, Text } from 'mdast';
import React, { useMemo } from 'react';
import type { Node } from 'unist';
import { v4 } from 'uuid';
interface Props {
content: string;
@ -48,7 +47,10 @@ const cursorCss = css`
const Cursor = () => <span key="cursor" className={classNames(cursorCss, 'cursor')} />;
const CURSOR = `{{${v4()}}}`;
// a weird combination of different whitespace chars to make sure it stays
// invisible even when we cannot properly parse the text while still being
// unique
const CURSOR = `  `;
const loadingCursorPlugin = () => {
const visitor = (node: Node, parent?: Parent) => {

View file

@ -4,13 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiLoadingSpinner } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { LensAttributesBuilder, XYChart, XYDataLayer } from '@kbn/lens-embeddable-utils';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import React from 'react';
import type { LensEmbeddableInput, LensPublicStart } from '@kbn/lens-plugin/public';
import React, { useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { i18n } from '@kbn/i18n';
import { Assign } from 'utility-types';
import type { RegisterFunctionDefinition } from '../../common/types';
import type {
ObservabilityAIAssistantPluginStartDependencies,
@ -56,6 +58,8 @@ function Lens({
});
}, [indexPattern]);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
if (!formulaAsync.value || !dataViewAsync.value) {
return <EuiLoadingSpinner />;
}
@ -68,19 +72,65 @@ function Lens({
}),
}).build();
const lensEmbeddableInput: Assign<LensEmbeddableInput, { attributes: typeof attributes }> = {
id: indexPattern,
attributes,
timeRange: {
from: start,
to: end,
mode: 'relative' as const,
},
};
return (
<lens.EmbeddableComponent
id={indexPattern}
attributes={attributes}
timeRange={{
from: start,
to: end,
mode: 'relative',
}}
style={{
height: 240,
}}
/>
<>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
iconType="lensApp"
onClick={() => {
lens.navigateToPrefilledEditor(lensEmbeddableInput);
}}
>
{i18n.translate('xpack.observabilityAiAssistant.lensFunction.openInLens', {
defaultMessage: 'Open in Lens',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
iconType="save"
onClick={() => {
setIsSaveModalOpen(() => true);
}}
>
{i18n.translate('xpack.observabilityAiAssistant.lensFunction.save', {
defaultMessage: 'Save',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<lens.EmbeddableComponent
{...lensEmbeddableInput}
style={{
height: 240,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{isSaveModalOpen ? (
<lens.SaveModalComponent
initialInput={lensEmbeddableInput}
onClose={() => {
setIsSaveModalOpen(() => false);
}}
/>
) : null}
</>
);
}