From 7f9aabbb02a7fe96f86b007663fe2774ceb6ad98 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Thu, 29 Aug 2024 15:27:29 -0700 Subject: [PATCH] [Discover 2.0] Fixed recent query and Data Selector styles (#7918) Signed-off-by: Ashwin P Chandran --- .../config/global_selectors.json | 3 +- .../language_service/_recent_query.scss | 9 +- .../language_service/recent_query.tsx | 220 +++++++----------- .../query/query_string/query_history.ts | 76 +++--- .../_dataset_configurator.scss | 2 +- .../dataset_selector/_dataset_explorer.scss | 3 +- .../dataset_selector/_dataset_selector.scss | 6 + .../public/ui/query_editor/query_editor.tsx | 42 ++-- 8 files changed, 181 insertions(+), 180 deletions(-) diff --git a/packages/osd-stylelint-config/config/global_selectors.json b/packages/osd-stylelint-config/config/global_selectors.json index ca442760f731..0427337a298b 100644 --- a/packages/osd-stylelint-config/config/global_selectors.json +++ b/packages/osd-stylelint-config/config/global_selectors.json @@ -27,7 +27,8 @@ "src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss", "src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss", "src/plugins/data/public/ui/query_string_input/_query_bar.scss", - "src/plugins/data/public/ui/query_editor/_query_editor.scss" + "src/plugins/data/public/ui/query_editor/_query_editor.scss", + "src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss" ] } } \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/language_service/_recent_query.scss b/src/plugins/data/public/query/query_string/language_service/_recent_query.scss index faac658f685f..4b7a30ce85af 100644 --- a/src/plugins/data/public/query/query_string/language_service/_recent_query.scss +++ b/src/plugins/data/public/query/query_string/language_service/_recent_query.scss @@ -1,4 +1,9 @@ .recentQuery__table { - padding: $euiSizeXS; - width: 1320px; + border: $euiBorderThin; + border-radius: $euiSizeXS; + margin: 0 $euiSizeXS $euiSizeXS; + + thead { + background-color: $euiColorLightestShade; + } } diff --git a/src/plugins/data/public/query/query_string/language_service/recent_query.tsx b/src/plugins/data/public/query/query_string/language_service/recent_query.tsx index d058cb5c692a..0db291af8d31 100644 --- a/src/plugins/data/public/query/query_string/language_service/recent_query.tsx +++ b/src/plugins/data/public/query/query_string/language_service/recent_query.tsx @@ -5,165 +5,115 @@ import './_recent_query.scss'; -import { - EuiBasicTable, - EuiButtonEmpty, - EuiButtonIcon, - EuiCopy, - EuiPopover, - EuiText, -} from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiCopy } from '@elastic/eui'; import moment from 'moment'; - -import React, { useCallback, useEffect, useState } from 'react'; import { Query, TimeRange } from 'src/plugins/data/common'; import { QueryStringContract } from '../query_string_manager'; -// TODO: Need to confirm this number -export const MAX_RECENT_QUERY_SIZE = 10; - interface RecentQueryItem { query: Query; time: number; timeRange?: TimeRange; } -export function RecentQuery(props: { +interface RecentQueryTableItem { + id: number; + query: Query['query']; + time: string; +} + +interface RecentQueriesTableProps { queryString: QueryStringContract; - query: Query; onClickRecentQuery: (query: Query, timeRange?: TimeRange) => void; -}) { - const [recentQueries, setRecentQueries] = useState( - props.queryString.getQueryHistory() - ); - const [isPopoverOpen, setPopover] = useState(false); - const onButtonClick = () => { - setPopover(!isPopoverOpen); - }; + isVisible: boolean; +} - const clearHistory = useCallback(() => { - props.queryString?.clearQueryHistory(); - setRecentQueries(props.queryString?.getQueryHistory()); - }, [props.queryString]); +export const MAX_RECENT_QUERY_SIZE = 10; - const clear = () => { - clearHistory(); - }; +export function RecentQueriesTable({ + queryString, + onClickRecentQuery, + isVisible, +}: RecentQueriesTableProps) { + const currentLanguage = queryString.getQuery().language; + const [recentQueries, setRecentQueries] = useState( + queryString.getQueryHistory() + ); useEffect(() => { - const done = props.queryString.changeQueryHistory(setRecentQueries); + const done = queryString.changeQueryHistory(setRecentQueries); return () => done(); - }, [props.queryString]); - - const getRowProps = (item: any) => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - className: 'customRowClass', - onClick: () => {}, - }; - }; - - const getCellProps = (item: any, column: any) => { - const { id } = item; - const { field } = column; - return { - className: 'customCellClass', - 'data-test-subj': `cell-${id}-${field}`, - textOnly: true, - }; - }; - - const actions = [ - { - name: 'Run', - description: 'Run recent query', - icon: 'play', - type: 'icon', - onClick: (item) => { - props.onClickRecentQuery(recentQueries[item.id].query, recentQueries[item.id].timeRange); - setPopover(false); - }, - 'data-test-subj': 'action-run', - }, - { - render: (item) => { - return ( - - {(copy) => ( - - )} - - ); - }, - }, - ]; - - const tableColumns = [ + }, [queryString]); + + const getRowProps = (item: any) => ({ + 'data-test-subj': `row-${item.id}`, + className: 'customRowClass', + onClick: () => {}, + }); + + const getCellProps = (item: any, column: any) => ({ + className: 'customCellClass', + 'data-test-subj': `cell-${item.id}-${column.field}`, + textOnly: true, + }); + + const tableColumns: Array> = [ + { field: 'query', name: 'Recent query' }, + { field: 'time', name: 'Last run', width: '200px' }, { - field: 'query', - name: 'Recent query', + name: 'Actions', + actions: [ + { + name: 'Run', + description: 'Run recent query', + icon: 'play', + type: 'icon', + onClick: (item: RecentQueryTableItem) => { + onClickRecentQuery(recentQueries[item.id].query, recentQueries[item.id].timeRange); + }, + 'data-test-subj': 'action-run', + }, + { + render: (item: RecentQueryTableItem) => ( + + {(copy) => ( + + )} + + ), + }, + ], + width: '70px', }, - { - field: 'language', - name: 'Language', - }, - { - field: 'time', - name: 'Last run', - }, - { name: 'Actions', actions }, ]; - const recentQueryItems = recentQueries + const recentQueryItems: RecentQueryTableItem[] = recentQueries .filter((item, idx) => idx < MAX_RECENT_QUERY_SIZE) - .map((query, idx) => { - const date = moment(query.time); - - const formattedDate = date.format('MMM D, YYYY HH:mm:ss'); - - let queryLanguage = query.query.language; - if (queryLanguage === 'kuery') { - queryLanguage = 'DQL'; - } - - const tableItem = { - id: idx, - query: query.query.query, - timeRange: query.timeRange, - language: queryLanguage, - time: formattedDate, - }; + .filter((item) => item.query.language === currentLanguage) + .map((query, idx) => ({ + id: idx, + query: query.query.query, + timeRange: query.timeRange, + time: moment(query.time).format('MMM D, YYYY HH:mm:ss'), + })); - return tableItem; - }); + if (!isVisible) return null; return ( - - - {'Recent queries'} - - - } - isOpen={isPopoverOpen} - closePopover={() => setPopover(false)} - panelPaddingSize="none" - anchorPosition={'downRight'} - > - - + ); } diff --git a/src/plugins/data/public/query/query_string/query_history.ts b/src/plugins/data/public/query/query_string/query_history.ts index 04e2285add1c..277287ee09d8 100644 --- a/src/plugins/data/public/query/query_string/query_history.ts +++ b/src/plugins/data/public/query/query_string/query_history.ts @@ -4,60 +4,82 @@ */ import { BehaviorSubject } from 'rxjs'; -import { DataStorage, Dataset } from '../../../common'; +import { DataStorage } from '../../../common'; import { Query, TimeRange } from '../..'; -// Todo: Implement a more advanced QueryHistory class when needed for recent query history +const MAX_HISTORY_SIZE = 500; +export const HISTORY_KEY_PREFIX = 'query_'; + export class QueryHistory { - constructor(private readonly storage: DataStorage) {} + private changeEmitter: BehaviorSubject; - private changeEmitter = new BehaviorSubject(this.getHistory() || []); + constructor(private readonly storage: DataStorage) { + this.changeEmitter = new BehaviorSubject(this.getHistory()); + } - getHistoryKeys() { + public getHistoryKeys(): string[] { return this.storage .keys() - .filter((key: string) => key.indexOf('query_') === 0) - .sort() - .reverse(); + .filter((key: string) => key.startsWith(HISTORY_KEY_PREFIX)) + .sort((a, b) => { + const timeA = parseInt(a.split('_')[1], 10); + const timeB = parseInt(b.split('_')[1], 10); + return timeB - timeA; // Sort in descending order (most recent first) + }); } - getHistory() { - return this.getHistoryKeys().map((key) => this.storage.get(key)); + public getHistory(): any[] { + return this.getHistoryKeys() + .map((key) => this.storage.get(key)) + .sort((a, b) => b.time - a.time); } - // This is used as an optimization mechanism so that different components - // can listen for changes to history and update because changes to history can - // be triggered from different places in the app. The alternative would be to store - // this in state so that we hook into the React model, but it would require loading history - // every time the application starts even if a user is not going to view history. - change(listener: (reqs: any[]) => void) { + public change(listener: (reqs: any[]) => void): () => void { const subscription = this.changeEmitter.subscribe(listener); return () => subscription.unsubscribe(); } - addQueryToHistory(query: Query, dateRange?: TimeRange) { - const keys = this.getHistoryKeys(); - keys.splice(0, 500); // only maintain most recent X; - keys.forEach((key) => { - this.storage.remove(key); + public addQueryToHistory(query: Query, dateRange?: TimeRange): void { + const existingKeys = this.getHistoryKeys(); + + // Check if the query already exists + const existingKey = existingKeys.find((key) => { + const item = this.storage.get(key); + return item && item.query.query === query.query && item.query.language === query.language; }); - const timestamp = new Date().getTime(); - const k = 'query_' + timestamp; - this.storage.set(k, { + if (existingKey) { + // If the query exists, remove it from its current position + this.storage.remove(existingKey); + existingKeys.splice(existingKeys.indexOf(existingKey), 1); + } + + // Add the new query to the front + const timestamp = Date.now(); + const newKey = `${HISTORY_KEY_PREFIX}${timestamp}`; + const newItem = { time: timestamp, query, dateRange, - }); + }; + this.storage.set(newKey, newItem); + + // Trim the history if it exceeds the maximum size + if (existingKeys.length >= MAX_HISTORY_SIZE) { + const keysToRemove = existingKeys.slice(MAX_HISTORY_SIZE - 1); + keysToRemove.forEach((key) => this.storage.remove(key)); + } + // Emit the updated history this.changeEmitter.next(this.getHistory()); } - clearHistory() { + public clearHistory(): void { this.getHistoryKeys().forEach((key) => this.storage.remove(key)); + this.changeEmitter.next([]); } } -export function createHistory(deps: { storage: DataStorage }) { +export function createHistory(deps: { storage: DataStorage }): QueryHistory { return new QueryHistory(deps.storage); } diff --git a/src/plugins/data/public/ui/dataset_selector/_dataset_configurator.scss b/src/plugins/data/public/ui/dataset_selector/_dataset_configurator.scss index 44e89b19ffb3..5f70c956c2f2 100644 --- a/src/plugins/data/public/ui/dataset_selector/_dataset_configurator.scss +++ b/src/plugins/data/public/ui/dataset_selector/_dataset_configurator.scss @@ -1,3 +1,3 @@ .datasetConfigurator { - height: 600px; + height: 100%; } diff --git a/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss b/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss index 0011db04fa29..704a49f010a6 100644 --- a/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss +++ b/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss @@ -1,7 +1,8 @@ .datasetExplorer { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 240px)) minmax(300px, 1fr); - height: 600px; + height: 100%; + max-height: calc(100vh - 200px); overflow-x: auto; border: $euiBorderThin; diff --git a/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss b/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss index ea780f1c22e8..0fd82fc1b9ca 100644 --- a/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss +++ b/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss @@ -18,6 +18,12 @@ &__advancedModal { width: 1200px; + height: 800px; + max-height: calc(100vh - $euiSizeS); + + .euiModal__flex { + max-height: none; + } } &__checkbox { diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index 3c9e81b4824d..2ff643f74254 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -6,6 +6,7 @@ import { i18n } from '@osd/i18n'; import { + EuiButtonEmpty, EuiButtonIcon, EuiCompressedFieldText, EuiFlexGroup, @@ -27,7 +28,7 @@ import { QueryEditorExtensions } from './query_editor_extensions'; import { getQueryService, getIndexPatterns } from '../../services'; import { DatasetSelector } from '../dataset_selector'; import { QueryControls } from '../../query/query_string/language_service/get_query_control_links'; -import { RecentQuery } from '../../query/query_string/language_service/recent_query'; +import { RecentQueriesTable } from '../../query/query_string/language_service/recent_query'; import { DefaultInputProps } from './editors'; const LANGUAGE_ID_SQL = 'SQL'; @@ -73,6 +74,7 @@ interface State { isCollapsed: boolean; timeStamp: IFieldType | null; lineCount: number | undefined; + isRecentQueryVisible: boolean; } // Needed for React.lazy @@ -87,6 +89,7 @@ export default class QueryEditorUI extends Component { isCollapsed: false, // default to expand mode timeStamp: null, lineCount: undefined, + isRecentQueryVisible: false, }; public inputRef: monaco.editor.IStandaloneCodeEditor | null = null; @@ -184,12 +187,6 @@ export default class QueryEditorUI extends Component { this.setState({ lineCount: currentLineCount }); }; - private onClickInput = (event: React.MouseEvent) => { - if (event.target instanceof HTMLTextAreaElement) { - this.onQueryStringChange(event.target.value); - } - }; - // TODO: MQL consider moving language select language of setting search source here private onSelectLanguage = (languageId: string) => { // Send telemetry info every time the user opts in or out of kuery @@ -216,6 +213,13 @@ export default class QueryEditorUI extends Component { this.setState({ index }); }; + private toggleRecentQueries = () => { + this.setState((prevState) => ({ + ...prevState, + isRecentQueryVisible: !prevState.isRecentQueryVisible, + })); + }; + public componentDidMount() { const parsedQuery = fromUser(toUser(this.props.query.query)); if (!isEqual(this.props.query.query, parsedQuery)) { @@ -358,11 +362,16 @@ export default class QueryEditorUI extends Component { , ], end: [ - , + + + {'Recent queries'} + + , ], }, provideCompletionItems: this.provideCompletionItems, @@ -441,7 +450,14 @@ export default class QueryEditorUI extends Component { className={classNames('osdQueryEditor__header', this.props.headerClassName)} /> {!this.state.isCollapsed && ( -
{languageEditor.Body()}
+ <> +
{languageEditor.Body()}
+ + )} {this.renderQueryEditorExtensions()}