Skip to content

Commit

Permalink
--wip--
Browse files Browse the repository at this point in the history
Signed-off-by: Joshua Li <[email protected]>
  • Loading branch information
joshuali925 committed May 31, 2024
1 parent 634fc86 commit 830664c
Show file tree
Hide file tree
Showing 19 changed files with 466 additions and 4,467 deletions.
4 changes: 2 additions & 2 deletions config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ data.enhancements.enabled: true
# # uiSettings:
# # overrides:o
# # "timepicker:quickRanges": []
opensearch_alerting.enabled: false
opensearch_security.enabled: false
# opensearch_alerting.enabled: false
# opensearch_security.enabled: false
# ql_dashboards.enabled: false

# opensearch.hosts: [https://localhost:9200]fea
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dashboarding"
],
"private": true,
"version": "3.0.0",
"version": "2.14.0",
"branch": "main",
"types": "./opensearch_dashboards.d.ts",
"tsdocMetadata": "./build/tsdoc-metadata.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"ui": true,
"requiredPlugins": ["data"],
"optionalPlugins": ["home"],
"requiredBundles": []
"requiredBundles": ["opensearchDashboardsUtils"]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import moment from 'moment';
import { CoreSetup, CoreStart, Plugin } from '../../../src/core/public';
import React from 'react';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '../../../src/core/public';
import { IStorageWrapper, Storage } from '../../../src/plugins/opensearch_dashboards_utils/public';
import { ConfigSchema } from '../common/config';
import { QueryAssistBar } from './query_assist/query_assist_bar';
import { PPLQlSearchInterceptor } from './search/ppl_search_interceptor';
import { SQLQlSearchInterceptor } from './search/sql_search_interceptor';
import { setCore, setData, setStorage } from './services';
import {
QueryEnhancementsPluginSetup,
QueryEnhancementsPluginStart,
QueryEnhancementsPluginSetupDependencies,
QueryEnhancementsPluginStart,
QueryEnhancementsPluginStartDependencies,
} from './types';
import { PPLQlSearchInterceptor } from './search/ppl_search_interceptor';
import { SQLQlSearchInterceptor } from './search/sql_search_interceptor';

export class QueryEnhancementsPlugin
implements Plugin<QueryEnhancementsPluginSetup, QueryEnhancementsPluginStart> {
private readonly storage: IStorageWrapper;

constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {
this.storage = new Storage(window.localStorage);
}

public setup(
core: CoreSetup,
{ data }: QueryEnhancementsPluginSetupDependencies
): QueryEnhancementsPluginSetup {

const pplSearchInterceptor = new PPLQlSearchInterceptor({
toasts: core.notifications.toasts,
http: core.http,
Expand Down Expand Up @@ -43,6 +54,25 @@ export class QueryEnhancementsPlugin
initialTo: moment().add(2, 'days').toISOString(),
},
showFilterBar: false,
extensions: [
{
id: 'query-assist',
order: 1000,
isEnabled: (() => {
let agentConfigured: boolean;
return async () => {
if (agentConfigured === undefined) {
agentConfigured = await core.http
.get<{ configured: boolean }>('/api/ql/query_assist/ppl/configured')
.then((response) => response.configured)
.catch(() => false);
}
return agentConfigured;
};
})(),
getComponent: (dependencies) => <QueryAssistBar dependencies={dependencies} />,
},
],
},
fields: {
visualizable: false,
Expand Down Expand Up @@ -75,7 +105,13 @@ export class QueryEnhancementsPlugin
return {};
}

public start(core: CoreStart): QueryEnhancementsPluginStart {
public start(
core: CoreStart,
deps: QueryEnhancementsPluginStartDependencies
): QueryEnhancementsPluginStart {
setCore(core);
setData(deps.data);
setStorage(this.storage);
return {};
}

Expand Down
52 changes: 52 additions & 0 deletions plugins-extra/query_enhancements/public/query_assist/call_outs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { EuiCallOut, EuiCallOutProps } from '@elastic/eui';
import React from 'react';

type QueryAssistCallOutProps = Pick<EuiCallOutProps, 'onDismiss'>;

export const ProhibitedQueryCallOut: React.FC<QueryAssistCallOutProps> = (props) => (
<EuiCallOut
data-test-subj="query-assist-guard-callout"
title="I am unable to respond to this query. Try another question."
size="s"
color="danger"
iconType="alert"
dismissible
onDismiss={props.onDismiss}
/>
);

export const EmptyIndexCallOut: React.FC<QueryAssistCallOutProps> = (props) => (
<EuiCallOut
data-test-subj="query-assist-empty-index-callout"
title="Select a data source or index to ask a question."
size="s"
color="warning"
iconType="iInCircle"
dismissible
onDismiss={props.onDismiss}
/>
);

export const EmptyQueryCallOut: React.FC<QueryAssistCallOutProps> = (props) => (
<EuiCallOut
data-test-subj="query-assist-empty-query-callout"
title="Enter a natural language question to automatically generate a query to view results."
size="s"
color="warning"
iconType="iInCircle"
dismissible
onDismiss={props.onDismiss}
/>
);

export const PPLGeneratedCallOut: React.FC<QueryAssistCallOutProps> = (props) => (
<EuiCallOut
data-test-subj="query-assist-ppl-callout"
title="PPL query generated"
size="s"
color="success"
iconType="check"
dismissible
onDismiss={props.onDismiss}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IUiSettingsClient } from '../../../../src/core/public';
import { UI_SETTINGS } from '../../../../src/plugins/data/common';
import { PersistedLog } from '../../../../src/plugins/data/public';
import { IStorageWrapper } from '../../../../src/plugins/opensearch_dashboards_utils/public';

export function getPersistedLog(
uiSettings: IUiSettingsClient,
storage: IStorageWrapper,
language: string
) {
return new PersistedLog(
`typeahead:${language}`,
{
maxLength: uiSettings.get(UI_SETTINGS.HISTORY_LIMIT),
filterDuplicates: true,
},
storage
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState } from 'react';
import { PersistedLog } from '../../../../../src/plugins/data/public';
import { getCore, getData } from '../../services';

export const useSubmit = (persistedLog: PersistedLog) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const core = getCore();
const data = getData();

const submit = async (options: { question: string; index: string }) => {
persistedLog.add(options.question);

setLoading(true);
setError(undefined);
try {
const generatedPPL = await core.http.post<string>('/api/ql/query_assist/ppl/generate', {
body: JSON.stringify(options),
});
data.query.queryString.setQuery({ query: generatedPPL, language: 'PPL' });
data.query.timefilter.timefilter.setTime({ from: 'now-1d', to: 'now' });
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};

return { submit, loading, error, setError };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiForm, EuiFormRow } from '@elastic/eui';
import React, { SyntheticEvent, useMemo, useRef } from 'react';
import { PersistedLog } from '../../../../src/plugins/data/public/query';
import { SearchBarExtensionDependencies } from '../../../../src/plugins/data/public/ui/search_bar_extensions/search_bar_extension';
import { getCore, getStorage } from '../services';
import { ProhibitedQueryCallOut } from './call_outs';
import { getPersistedLog } from './get_persisted_log';
import { useSubmit } from './hooks/use_submit';
import { QueryAssistInput } from './query_assist_input';

interface QueryAssistInputProps {
dependencies: SearchBarExtensionDependencies;
}

export const QueryAssistBar: React.FC<QueryAssistInputProps> = (props) => {
const inputRef = useRef<HTMLInputElement>(null);
const core = getCore();
const storage = getStorage();

const persistedLog: PersistedLog = useMemo(
() => getPersistedLog(core.uiSettings, storage, 'query-assist'),
[core.uiSettings, storage]
);

const { submit, loading, error, setError } = useSubmit(persistedLog);
const selectedIndex = props.dependencies.indexPatterns?.at(0)?.title;

const onSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
if (!inputRef.current?.value || !selectedIndex) {
setError(new Error('empty'));
return;
}
await submit({ question: inputRef.current.value, index: selectedIndex });
};

const renderCallout = () => {
if (!error) return null;
if (error instanceof Error)
return <ProhibitedQueryCallOut onDismiss={() => setError(undefined)} />;
};

return (
<EuiForm component="form" onSubmit={onSubmit}>
<EuiFormRow fullWidth>
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem>
<QueryAssistInput
inputRef={inputRef}
persistedLog={persistedLog}
selectedIndex={selectedIndex}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="returnKey"
display="fill"
isDisabled={loading}
onClick={onSubmit}
size="m"
type="submit"
aria-label="submit-question"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
{renderCallout()}
</EuiForm>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { EuiFieldText, EuiIcon, EuiOutsideClickDetector, EuiPortal } from '@elastic/eui';
import React, { useState } from 'react';
import {
PersistedLog,
QuerySuggestionTypes,
SuggestionsComponent,
} from '../../../../src/plugins/data/public';
import assistantLogo from '../assets/query_assist_logo.svg';

interface QueryAssistInputProps {
inputRef: React.RefObject<HTMLInputElement>;
persistedLog: PersistedLog;
selectedIndex?: string;
}

export const QueryAssistInput: React.FC<QueryAssistInputProps> = (props) => {
const [isSuggestionsVisible, setIsSuggestionsVisible] = useState(false);

const [index, setIndex] = useState<number | null>(null);

const getRecentSearchSuggestions = (query: string) => {
if (!props.persistedLog) {
return [];
}
const recentSearches = props.persistedLog.get();
const matchingRecentSearches = recentSearches.filter((recentQuery) => {
const recentQueryString = typeof recentQuery === 'object' ? recentQuery : recentQuery;
return recentQueryString.includes(query);
});
return matchingRecentSearches.map((recentSearch) => {
const text = recentSearch;
const start = 0;
const end = query.length;
return { type: QuerySuggestionTypes.RecentSearch, text, start, end };
});
};

return (
<>
<EuiOutsideClickDetector onOutsideClick={() => setIsSuggestionsVisible(false)}>
<div>
<EuiFieldText
inputRef={props.inputRef}
onClick={() => setIsSuggestionsVisible(true)}
onKeyDown={() => setIsSuggestionsVisible(true)}
placeholder={
props.selectedIndex
? `Ask a natural language question about ${props.selectedIndex} to generate a query`
: 'Select an index pattern to ask a question'
}
prepend={<EuiIcon type={assistantLogo} />}
fullWidth
/>
<EuiPortal>
<SuggestionsComponent
show={isSuggestionsVisible}
suggestions={getRecentSearchSuggestions(props.inputRef.current?.value || '')}
index={index}
onClick={(suggestion) => {
if (!props.inputRef.current) return;
props.inputRef.current.value = suggestion.text;
setIsSuggestionsVisible(false);
setIndex(null);
props.inputRef.current.focus();
}}
onMouseEnter={(i) => setIndex(i)}
loadMore={() => {}}
queryBarRect={props.inputRef.current?.getBoundingClientRect()}
size="s"
/>
</EuiPortal>
</div>
</EuiOutsideClickDetector>
</>
);
};
8 changes: 8 additions & 0 deletions plugins-extra/query_enhancements/public/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CoreStart } from '../../../src/core/public';
import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common';
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
import { IStorageWrapper } from '../../../src/plugins/opensearch_dashboards_utils/public';

export const [getCore, setCore] = createGetterSetter<CoreStart>('core');
export const [getData, setData] = createGetterSetter<DataPublicPluginStart>('data');
export const [getStorage, setStorage] = createGetterSetter<IStorageWrapper>('storage');
Loading

0 comments on commit 830664c

Please sign in to comment.