Skip to content

Commit

Permalink
[Discover 2.0] Fixed recent query and Data Selector styles (opensearc…
Browse files Browse the repository at this point in the history
…h-project#7918)

Signed-off-by: Ashwin P Chandran <ashwinpc@amazon.com>
  • Loading branch information
ashwin-pc authored Aug 29, 2024
1 parent a0365d8 commit 7f9aabb
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 180 deletions.
3 changes: 2 additions & 1 deletion packages/osd-stylelint-config/config/global_selectors.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
.recentQuery__table {
padding: $euiSizeXS;
width: 1320px;
border: $euiBorderThin;
border-radius: $euiSizeXS;
margin: 0 $euiSizeXS $euiSizeXS;

thead {
background-color: $euiColorLightestShade;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecentQueryItem[]>(
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<RecentQueryItem[]>(
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 (
<EuiCopy textToCopy={item.query}>
{(copy) => (
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label="Copy recent query"
/>
)}
</EuiCopy>
);
},
},
];

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<EuiBasicTableColumn<RecentQueryTableItem>> = [
{ 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) => (
<EuiCopy textToCopy={item.query as string}>
{(copy) => (
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label="Copy recent query"
/>
)}
</EuiCopy>
),
},
],
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 (
<EuiPopover
button={
<EuiButtonEmpty iconSide="left" iconType="clock" size="xs" onClick={onButtonClick}>
<EuiText size="xs" color="subdued">
{'Recent queries'}
</EuiText>
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
closePopover={() => setPopover(false)}
panelPaddingSize="none"
anchorPosition={'downRight'}
>
<EuiBasicTable
items={recentQueryItems}
rowHeader="query"
columns={tableColumns}
rowProps={getRowProps}
cellProps={getCellProps}
className="recentQuery__table"
/>
</EuiPopover>
<EuiBasicTable
items={recentQueryItems}
rowHeader="query"
columns={tableColumns}
rowProps={getRowProps}
cellProps={getCellProps}
className="recentQuery__table"
tableLayout="fixed"
compressed
/>
);
}
76 changes: 49 additions & 27 deletions src/plugins/data/public/query/query_string/query_history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any[]>;

private changeEmitter = new BehaviorSubject<any[]>(this.getHistory() || []);
constructor(private readonly storage: DataStorage) {
this.changeEmitter = new BehaviorSubject<any[]>(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);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.datasetConfigurator {
height: 600px;
height: 100%;
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Loading

0 comments on commit 7f9aabb

Please sign in to comment.