Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Discover 2.0] Fixed recent query and Data Selector styles #7918

Merged
merged 1 commit into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe later, i remember user can config the format when date time is displayed, should this be read from user config?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah from ui settings

}));

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this a UI_SETTING?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Fast follow maybe? This was what Abby introduced in her PR but we don't today show any more than 10 to the user anyways, so having this value be user configurable isn't very useful yet. Once we implement more features for the table, then it makes sense adding it as an advanced setting

export const HISTORY_KEY_PREFIX = 'query_';

export class QueryHistory {
constructor(private readonly storage: DataStorage) {}
private changeEmitter: BehaviorSubject<any[]>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the type is just

    interface QueryHistoryItem {
      time: number;
      query: Query;
      dateRange?: TimeRange;
    };

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo i think the type should extend savedquery


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)

Check warning on line 27 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L25-L27

Added lines #L25 - L27 were not covered by tests
});
}

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);

Check warning on line 34 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L33-L34

Added lines #L33 - L34 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getHistoryKeys already sorts by time, is this sort needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I added that sort afterwards. This is no longer needed

}

// 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we just use the LRU library we have?

const existingKeys = this.getHistoryKeys();

Check warning on line 43 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L43

Added line #L43 was not covered by tests

// Check if the query already exists
const existingKey = existingKeys.find((key) => {
const item = this.storage.get(key);

Check warning on line 47 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L46-L47

Added lines #L46 - L47 were not covered by tests
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);

Check warning on line 54 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L53-L54

Added lines #L53 - L54 were not covered by tests
}

// Add the new query to the front
const timestamp = Date.now();
const newKey = `${HISTORY_KEY_PREFIX}${timestamp}`;
const newItem = {

Check warning on line 60 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L58-L60

Added lines #L58 - L60 were not covered by tests
time: timestamp,
query,
dateRange,
});
};
this.storage.set(newKey, newItem);

Check warning on line 65 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L65

Added line #L65 was not covered by tests

// 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));

Check warning on line 70 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L69-L70

Added lines #L69 - L70 were not covered by tests
}

// Emit the updated history
this.changeEmitter.next(this.getHistory());
}

clearHistory() {
public clearHistory(): void {
this.getHistoryKeys().forEach((key) => this.storage.remove(key));
this.changeEmitter.next([]);

Check warning on line 79 in src/plugins/data/public/query/query_string/query_history.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/query/query_string/query_history.ts#L79

Added line #L79 was not covered by tests
}
}

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
Loading