Rich history UX fixes (#22783)

* Initial commit

* Visualised renamed or deleted  datasources as well, if they have queries

* Pass ds image to card and information if the datasource was removed/renamed

* Set up card with datasource info and change run query

* Style comment, run button

* Fix button naming

* Remember last filters

* Update public/app/core/store.ts

* Update public/app/features/explore/RichHistory/RichHistory.tsx

* Update comments

* Rename datasource to data source

* Add test coverage, fix naming

* Remove unused styles, add feedback info

Co-authored-by: Ivana <ivana.huckova@gmail.com>
Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
This commit is contained in:
Jess 2020-03-19 08:47:27 +01:00 committed by GitHub
parent c7951a2575
commit db85c3e7b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 307 additions and 170 deletions

View File

@ -7,6 +7,7 @@ import {
createQueryHeading, createQueryHeading,
createDataQuery, createDataQuery,
deleteAllFromRichHistory, deleteAllFromRichHistory,
deleteQueryInRichHistory,
} from './richHistory'; } from './richHistory';
import store from 'app/core/store'; import store from 'app/core/store';
import { SortOrder } from './explore'; import { SortOrder } from './explore';
@ -135,6 +136,18 @@ describe('updateCommentInRichHistory', () => {
}); });
}); });
describe('deleteQueryInRichHistory', () => {
it('should delete query in query in history', () => {
const deletedHistory = deleteQueryInRichHistory(mock.history, 1);
expect(deletedHistory).toEqual([]);
});
it('should delete query in localStorage', () => {
deleteQueryInRichHistory(mock.history, 1);
expect(store.exists(key)).toBeTruthy();
expect(store.getObject(key)).toEqual([]);
});
});
describe('mapNumbertoTimeInSlider', () => { describe('mapNumbertoTimeInSlider', () => {
it('should correctly map number to value', () => { it('should correctly map number to value', () => {
const value = mapNumbertoTimeInSlider(25); const value = mapNumbertoTimeInSlider(25);

View File

@ -6,6 +6,7 @@ import { DataQuery, ExploreMode } from '@grafana/data';
import { renderUrl } from 'app/core/utils/url'; import { renderUrl } from 'app/core/utils/url';
import store from 'app/core/store'; import store from 'app/core/store';
import { serializeStateToUrlParam, SortOrder } from './explore'; import { serializeStateToUrlParam, SortOrder } from './explore';
import { getExploreDatasources } from '../../features/explore/state/selectors';
// Types // Types
import { ExploreUrlState, RichHistoryQuery } from 'app/types/explore'; import { ExploreUrlState, RichHistoryQuery } from 'app/types/explore';
@ -16,6 +17,7 @@ export const RICH_HISTORY_SETTING_KEYS = {
retentionPeriod: 'grafana.explore.richHistory.retentionPeriod', retentionPeriod: 'grafana.explore.richHistory.retentionPeriod',
starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab', starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab',
activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly', activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly',
datasourceFilters: 'grafana.explore.richHistory.datasourceFilters',
}; };
/* /*
@ -113,6 +115,12 @@ export function updateCommentInRichHistory(
return updatedQueries; return updatedQueries;
} }
export function deleteQueryInRichHistory(richHistory: RichHistoryQuery[], ts: number) {
const updatedQueries = richHistory.filter(query => query.ts !== ts);
store.setObject(RICH_HISTORY_KEY, updatedQueries);
return updatedQueries;
}
export const sortQueries = (array: RichHistoryQuery[], sortOrder: SortOrder) => { export const sortQueries = (array: RichHistoryQuery[], sortOrder: SortOrder) => {
let sortFunc; let sortFunc;
@ -257,3 +265,31 @@ export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortO
return mappedQueriesToHeadings; return mappedQueriesToHeadings;
} }
/* Create datasource list with images. If specific datasource retrieved from Rich history is not part of
* exploreDatasources add generic datasource image and add property isRemoved = true.
*/
export function createDatasourcesList(queriesDatasources: string[]) {
const exploreDatasources = getExploreDatasources();
const datasources: Array<{ label: string; value: string; imgUrl: string; isRemoved: boolean }> = [];
queriesDatasources.forEach(queryDsName => {
const index = exploreDatasources.findIndex(exploreDs => exploreDs.name === queryDsName);
if (index !== -1) {
datasources.push({
label: queryDsName,
value: queryDsName,
imgUrl: exploreDatasources[index].meta.info.logos.small,
isRemoved: false,
});
} else {
datasources.push({
label: queryDsName,
value: queryDsName,
imgUrl: 'public/img/icn-datasource.svg',
isRemoved: true,
});
}
});
return datasources;
}

View File

@ -83,8 +83,8 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
super(props); super(props);
this.state = { this.state = {
activeTab: this.props.firstTab, activeTab: this.props.firstTab,
datasourceFilters: null,
sortOrder: SortOrder.Descending, sortOrder: SortOrder.Descending,
datasourceFilters: store.getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, null),
retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7), retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7),
starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false), starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false),
activeDatasourceOnly: store.getBool(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, false), activeDatasourceOnly: store.getBool(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, false),
@ -115,6 +115,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
}; };
onSelectDatasourceFilters = (value: SelectableValue[] | null) => { onSelectDatasourceFilters = (value: SelectableValue[] | null) => {
store.setObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, value);
this.setState({ datasourceFilters: value }); this.setState({ datasourceFilters: value });
}; };
@ -133,7 +134,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
? this.onSelectDatasourceFilters([ ? this.onSelectDatasourceFilters([
{ label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance }, { label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance },
]) ])
: this.onSelectDatasourceFilters(null); : this.onSelectDatasourceFilters(this.state.datasourceFilters);
} }
componentDidMount() { componentDidMount() {

View File

@ -15,10 +15,11 @@ const setup = (propOverrides?: Partial<Props>) => {
queries: ['query1', 'query2', 'query3'], queries: ['query1', 'query2', 'query3'],
sessionName: '', sessionName: '',
}, },
changeQuery: jest.fn(), dsImg: '/app/img',
isRemoved: false,
changeDatasource: jest.fn(), changeDatasource: jest.fn(),
clearQueries: jest.fn(),
updateRichHistory: jest.fn(), updateRichHistory: jest.fn(),
setQueries: jest.fn(),
exploreId: ExploreId.left, exploreId: ExploreId.left,
datasourceInstance: { name: 'Datasource' } as DataSourceApi, datasourceInstance: { name: 'Datasource' } as DataSourceApi,
}; };
@ -62,6 +63,18 @@ describe('RichHistoryCard', () => {
.text() .text()
).toEqual('query3'); ).toEqual('query3');
}); });
it('should render data source icon', () => {
const wrapper = setup();
expect(wrapper.find({ 'aria-label': 'Data source icon' })).toHaveLength(1);
});
it('should render data source name', () => {
const wrapper = setup();
expect(wrapper.find({ 'aria-label': 'Data source name' }).text()).toEqual('Test datasource');
});
it('should render "Not linked to existing data source" if removed data source', () => {
const wrapper = setup({ isRemoved: true });
expect(wrapper.find({ 'aria-label': 'Data source name' }).text()).toEqual('Not linked to existing data source');
});
describe('commenting', () => { describe('commenting', () => {
it('should render comment, if comment present', () => { it('should render comment, if comment present', () => {

View File

@ -2,54 +2,91 @@ import React, { useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { stylesFactory, useTheme, Forms, styleMixins } from '@grafana/ui'; import { stylesFactory, useTheme, Forms } from '@grafana/ui';
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data'; import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
import { RichHistoryQuery, ExploreId } from 'app/types/explore'; import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { copyStringToClipboard, createUrlFromRichHistory, createDataQuery } from 'app/core/utils/richHistory'; import { copyStringToClipboard, createUrlFromRichHistory, createDataQuery } from 'app/core/utils/richHistory';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { changeQuery, changeDatasource, clearQueries, updateRichHistory } from '../state/actions'; import { changeDatasource, updateRichHistory, setQueries } from '../state/actions';
export interface Props { export interface Props {
query: RichHistoryQuery; query: RichHistoryQuery;
changeQuery: typeof changeQuery; dsImg: string;
isRemoved: boolean;
changeDatasource: typeof changeDatasource; changeDatasource: typeof changeDatasource;
clearQueries: typeof clearQueries;
updateRichHistory: typeof updateRichHistory; updateRichHistory: typeof updateRichHistory;
setQueries: typeof setQueries;
exploreId: ExploreId; exploreId: ExploreId;
datasourceInstance: DataSourceApi; datasourceInstance: DataSourceApi;
} }
const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => { const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
const borderColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark4; /* Hard-coded value so all buttons and icons on right side of card are aligned */
const cardBottomPadding = hasComment ? theme.spacing.sm : theme.spacing.xs; const rigtColumnWidth = '240px';
const rigtColumnContentWidth = '170px';
const borderColor = theme.isLight ? theme.colors.gray5 : theme.colors.gray25;
/* If datasource was removed, card will have inactive color */
const cardColor = theme.isLight
? isRemoved
? theme.colors.gray95
: theme.colors.white
: isRemoved
? theme.colors.gray15
: theme.colors.dark7;
const cardBoxShadow = theme.isLight ? `0px 2px 2px ${borderColor}` : `0px 2px 4px black`;
return { return {
queryCard: css` queryCard: css`
${styleMixins.listItem(theme)}
display: flex; display: flex;
padding: ${theme.spacing.sm} ${theme.spacing.sm} ${cardBottomPadding}; flex-direction: column;
border: 1px solid ${borderColor};
margin: ${theme.spacing.sm} 0; margin: ${theme.spacing.sm} 0;
box-shadow: ${cardBoxShadow};
background-color: ${cardColor};
border-radius: ${theme.border.radius};
.starred { .starred {
color: ${theme.colors.orange}; color: ${theme.colors.orange};
} }
`, `,
queryCardLeft: css` cardRow: css`
padding-right: 10px; display: flex;
width: calc(100% - 150px); align-items: center;
cursor: pointer; justify-content: space-between;
padding: ${theme.spacing.sm};
border-bottom: none;
:first-of-type {
border-bottom: 1px solid ${borderColor};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
}
img {
height: ${theme.typography.size.base};
max-width: ${theme.typography.size.base};
margin-right: ${theme.spacing.sm};
}
`, `,
queryCardRight: css` datasourceContainer: css`
width: 150px; display: flex;
height: ${theme.height.sm}; align-items: center;
font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.bold};
`,
queryActionButtons: css`
max-width: ${rigtColumnContentWidth};
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
font-size: ${theme.typography.size.base};
i { i {
margin: ${theme.spacing.xs}; margin: ${theme.spacing.xs};
cursor: pointer; cursor: pointer;
} }
`, `,
queryContainer: css`
font-weight: ${theme.typography.weight.bold};
width: calc(100% - ${rigtColumnWidth});
`,
queryRow: css` queryRow: css`
border-top: 1px solid ${borderColor}; border-top: 1px solid ${borderColor};
word-break: break-all; word-break: break-all;
@ -59,56 +96,153 @@ const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => {
padding: 0 0 4px 0; padding: 0 0 4px 0;
} }
`, `,
buttonRow: css` updateCommentContainer: css`
> * { width: calc(100% + ${rigtColumnWidth});
margin-right: ${theme.spacing.xs}; margin-top: ${theme.spacing.sm};
}
`, `,
comment: css` comment: css`
overflow-wrap: break-word; overflow-wrap: break-word;
font-size: ${theme.typography.size.sm}; font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.regular};
margin-top: ${theme.spacing.xs}; margin-top: ${theme.spacing.xs};
`, `,
commentButtonRow: css`
> * {
margin-right: ${theme.spacing.sm};
}
`,
textArea: css`
border: 1px solid ${borderColor};
background: inherit;
color: inherit;
width: 100%;
font-size: ${theme.typography.size.sm};
&placeholder {
padding: 0 ${theme.spacing.sm};
}
`,
runButton: css`
max-width: ${rigtColumnContentWidth};
display: flex;
justify-content: flex-end;
button {
height: auto;
padding: ${theme.spacing.sm} ${theme.spacing.md};
span {
white-space: normal !important;
}
}
`,
}; };
}); });
export function RichHistoryCard(props: Props) { export function RichHistoryCard(props: Props) {
const { const {
query, query,
dsImg,
isRemoved,
updateRichHistory, updateRichHistory,
changeQuery,
changeDatasource, changeDatasource,
exploreId, exploreId,
clearQueries,
datasourceInstance, datasourceInstance,
setQueries,
} = props; } = props;
const [activeUpdateComment, setActiveUpdateComment] = useState(false); const [activeUpdateComment, setActiveUpdateComment] = useState(false);
const [comment, setComment] = useState<string | undefined>(query.comment); const [comment, setComment] = useState<string | undefined>(query.comment);
const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment); const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment);
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme, Boolean(query.comment)); const styles = getStyles(theme, isRemoved);
const changeQueries = () => { const onRunQuery = async () => {
query.queries.forEach((q, i) => { const dataQueries = query.queries.map((q, i) => createDataQuery(query, q, i));
const dataQuery = createDataQuery(query, q, i);
changeQuery(exploreId, dataQuery, i);
});
};
const onChangeQuery = async (query: RichHistoryQuery) => {
if (query.datasourceName !== datasourceInstance?.name) { if (query.datasourceName !== datasourceInstance?.name) {
await changeDatasource(exploreId, query.datasourceName); await changeDatasource(exploreId, query.datasourceName);
changeQueries(); setQueries(exploreId, dataQueries);
} else { } else {
clearQueries(exploreId); setQueries(exploreId, dataQueries);
changeQueries();
} }
}; };
const onCopyQuery = () => {
const queries = query.queries.join('\n\n');
copyStringToClipboard(queries);
appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']);
};
const onCreateLink = () => {
const url = createUrlFromRichHistory(query);
copyStringToClipboard(url);
appEvents.emit(AppEvents.alertSuccess, ['Link copied to clipboard']);
};
const onDeleteQuery = () => {
updateRichHistory(query.ts, 'delete');
appEvents.emit(AppEvents.alertSuccess, ['Query deleted']);
};
const onStarrQuery = () => {
updateRichHistory(query.ts, 'starred');
};
const onUpdateComment = () => {
updateRichHistory(query.ts, 'comment', comment);
toggleActiveUpdateComment();
};
const onCancelUpdateComment = () => {
toggleActiveUpdateComment();
setComment(query.comment);
};
const updateComment = (
<div className={styles.updateCommentContainer}>
<Forms.TextArea
value={comment}
placeholder={comment ? undefined : 'An optional description of what the query does.'}
onChange={e => setComment(e.currentTarget.value)}
className={styles.textArea}
/>
<div className={styles.commentButtonRow}>
<Forms.Button onClick={onUpdateComment}>Save comment</Forms.Button>
<Forms.Button variant="secondary" onClick={onCancelUpdateComment}>
Cancel
</Forms.Button>
</div>
</div>
);
const queryActionButtons = (
<div className={styles.queryActionButtons}>
<i
className="fa fa-fw fa-comment-o"
onClick={toggleActiveUpdateComment}
title={query.comment?.length > 0 ? 'Edit comment' : 'Add comment'}
></i>
<i className="fa fa-fw fa-copy" onClick={onCopyQuery} title="Copy query to clipboard"></i>
{!isRemoved && <i className="fa fa-fw fa-link" onClick={onCreateLink} title="Copy link to clipboard"></i>}
<i className={'fa fa-trash'} title={'Delete query'} onClick={onDeleteQuery}></i>
<i
className={cx('fa fa-fw', query.starred ? 'fa-star starred' : 'fa-star-o')}
onClick={onStarrQuery}
title={query.starred ? 'Unstar query' : 'Star query'}
></i>
</div>
);
return ( return (
<div className={styles.queryCard}> <div className={styles.queryCard}>
<div className={styles.queryCardLeft} title="Add queries to query editor" onClick={() => onChangeQuery(query)}> <div className={styles.cardRow}>
<div className={styles.datasourceContainer}>
<img src={dsImg} aria-label="Data source icon" />
<div aria-label="Data source name">
{isRemoved ? 'Not linked to existing data source' : query.datasourceName}
</div>
</div>
{queryActionButtons}
</div>
<div className={cx(styles.cardRow)}>
<div className={styles.queryContainer}>
{query.queries.map((q, i) => { {query.queries.map((q, i) => {
return ( return (
<div aria-label="Query text" key={`${q}-${i}`} className={styles.queryRow}> <div aria-label="Query text" key={`${q}-${i}`} className={styles.queryRow}>
@ -121,81 +255,16 @@ export function RichHistoryCard(props: Props) {
{query.comment} {query.comment}
</div> </div>
)} )}
{activeUpdateComment && ( {activeUpdateComment && updateComment}
<div>
<Forms.TextArea
value={comment}
placeholder={comment ? undefined : 'add comment'}
onChange={e => {
setComment(e.currentTarget.value);
}}
onClick={e => {
e.stopPropagation();
}}
/>
<div className={styles.buttonRow}>
<Forms.Button
onClick={e => {
e.preventDefault();
e.stopPropagation();
updateRichHistory(query.ts, 'comment', comment);
toggleActiveUpdateComment();
}}
>
Save
</Forms.Button>
<Forms.Button
variant="secondary"
className={css`
margin-left: 8px;
`}
onClick={e => {
e.stopPropagation();
toggleActiveUpdateComment();
setComment(query.comment);
}}
>
Cancel
</Forms.Button>
</div> </div>
{!activeUpdateComment && (
<div className={styles.runButton}>
<Forms.Button variant="secondary" onClick={onRunQuery} disabled={isRemoved}>
{datasourceInstance?.name === query.datasourceName ? 'Run query' : 'Switch data source and run query'}
</Forms.Button>
</div> </div>
)} )}
</div> </div>
<div className={styles.queryCardRight}>
<i
className="fa fa-fw fa-comment-o"
onClick={() => {
toggleActiveUpdateComment();
}}
title={query.comment?.length > 0 ? 'Edit comment' : 'Add comment'}
></i>
<i
className="fa fa-fw fa-copy"
onClick={() => {
const queries = query.queries.join('\n\n');
copyStringToClipboard(queries);
appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']);
}}
title="Copy query to clipboard"
></i>
<i
className="fa fa-fw fa-link"
onClick={() => {
const url = createUrlFromRichHistory(query);
copyStringToClipboard(url);
appEvents.emit(AppEvents.alertSuccess, ['Link copied to clipboard']);
}}
style={{ fontWeight: 'normal' }}
title="Copy link to clipboard"
></i>
<i
className={cx('fa fa-fw', query.starred ? 'fa-star starred' : 'fa-star-o')}
onClick={() => {
updateRichHistory(query.ts, 'starred');
}}
title={query.starred ? 'Unstar query' : 'Star query'}
></i>
</div>
</div> </div>
); );
} }
@ -212,10 +281,9 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI
} }
const mapDispatchToProps = { const mapDispatchToProps = {
changeQuery,
changeDatasource, changeDatasource,
clearQueries,
updateRichHistory, updateRichHistory,
setQueries,
}; };
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(RichHistoryCard)); export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(RichHistoryCard));

View File

@ -8,7 +8,6 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
// Utils // Utils
import { stylesFactory, useTheme } from '@grafana/ui'; import { stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { getExploreDatasources } from '../state/selectors';
import { SortOrder } from 'app/core/utils/explore'; import { SortOrder } from 'app/core/utils/explore';
import { import {
@ -16,6 +15,7 @@ import {
mapNumbertoTimeInSlider, mapNumbertoTimeInSlider,
createRetentionPeriodBoundary, createRetentionPeriodBoundary,
mapQueriesToHeadings, mapQueriesToHeadings,
createDatasourcesList,
} from 'app/core/utils/richHistory'; } from 'app/core/utils/richHistory';
// Components // Components
@ -136,14 +136,8 @@ export function RichHistoryQueriesTab(props: Props) {
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme, height); const styles = getStyles(theme, height);
const listOfDsNamesWithQueries = uniqBy(queries, 'datasourceName').map(d => d.datasourceName); const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
/* Display only explore datasoources, that have saved queries */
const datasources = getExploreDatasources()
?.filter(ds => listOfDsNamesWithQueries.includes(ds.name))
.map(d => {
return { value: d.value!, label: d.value!, imgUrl: d.meta.info.logos.small };
});
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value); const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
const filteredQueriesByDatasource = datasourceFilters const filteredQueriesByDatasource = datasourceFilters
@ -193,7 +187,7 @@ export function RichHistoryQueriesTab(props: Props) {
<div aria-label="Filter datasources" className={styles.multiselect}> <div aria-label="Filter datasources" className={styles.multiselect}>
<Select <Select
isMulti={true} isMulti={true}
options={datasources} options={listOfDatasources}
value={datasourceFilters} value={datasourceFilters}
placeholder="Filter queries for specific data sources(s)" placeholder="Filter queries for specific data sources(s)"
onChange={onSelectDatasourceFilters} onChange={onSelectDatasourceFilters}
@ -215,9 +209,18 @@ export function RichHistoryQueriesTab(props: Props) {
<div className={styles.heading}> <div className={styles.heading}>
{heading} <span className={styles.queries}>{mappedQueriesToHeadings[heading].length} queries</span> {heading} <span className={styles.queries}>{mappedQueriesToHeadings[heading].length} queries</span>
</div> </div>
{mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => ( {mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => {
<RichHistoryCard query={q} key={q.ts} exploreId={exploreId} /> const idx = listOfDatasources.findIndex(d => d.label === q.datasourceName);
))} return (
<RichHistoryCard
query={q}
key={q.ts}
exploreId={exploreId}
dsImg={listOfDatasources[idx].imgUrl}
isRemoved={listOfDatasources[idx].isRemoved}
/>
);
})}
</div> </div>
); );
})} })}

View File

@ -8,10 +8,9 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
// Utils // Utils
import { stylesFactory, useTheme } from '@grafana/ui'; import { stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { getExploreDatasources } from '../state/selectors';
import { SortOrder } from '../../../core/utils/explore'; import { SortOrder } from '../../../core/utils/explore';
import { sortQueries } from '../../../core/utils/richHistory'; import { sortQueries, createDatasourcesList } from '../../../core/utils/richHistory';
// Components // Components
import RichHistoryCard from './RichHistoryCard'; import RichHistoryCard from './RichHistoryCard';
@ -33,17 +32,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
return { return {
container: css` container: css`
display: flex; display: flex;
.label-slider {
font-size: ${theme.typography.size.sm};
&:last-of-type {
margin-top: ${theme.spacing.lg};
}
&:first-of-type {
margin-top: ${theme.spacing.sm};
font-weight: ${theme.typography.weight.semibold};
margin-bottom: ${theme.spacing.xs};
}
}
`, `,
containerContent: css` containerContent: css`
width: 100%; width: 100%;
@ -63,19 +51,18 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
sort: css` sort: css`
width: 170px; width: 170px;
`, `,
sessionName: css` feedback: css`
display: flex; height: 60px;
align-items: flex-start;
justify-content: flex-start;
margin-top: ${theme.spacing.lg}; margin-top: ${theme.spacing.lg};
h4 { display: flex;
margin: 0 10px 0 0; justify-content: center;
font-weight: ${theme.typography.weight.light};
font-size: ${theme.typography.size.sm};
a {
font-weight: ${theme.typography.weight.semibold};
margin-left: ${theme.spacing.xxs};
} }
`, `,
heading: css`
font-size: ${theme.typography.heading.h4};
margin: ${theme.spacing.md} ${theme.spacing.xxs} ${theme.spacing.sm} ${theme.spacing.xxs};
`,
}; };
}); });
@ -92,18 +79,17 @@ export function RichHistoryStarredTab(props: Props) {
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme); const styles = getStyles(theme);
const listOfDsNamesWithQueries = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
const exploreDatasources = getExploreDatasources() const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
?.filter(ds => listOfDsNamesWithQueries.includes(ds.name)) const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
.map(d => {
return { value: d.value!, label: d.value!, imgUrl: d.meta.info.logos.small };
});
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value); const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
const starredQueries = queries.filter(q => q.starred === true); const starredQueries = queries.filter(q => q.starred === true);
const starredQueriesFilteredByDatasource = datasourceFilters const starredQueriesFilteredByDatasource = datasourceFilters
? starredQueries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName)) ? starredQueries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
: starredQueries; : starredQueries;
const sortedStarredQueries = sortQueries(starredQueriesFilteredByDatasource, sortOrder); const sortedStarredQueries = sortQueries(starredQueriesFilteredByDatasource, sortOrder);
return ( return (
@ -114,7 +100,7 @@ export function RichHistoryStarredTab(props: Props) {
<div aria-label="Filter datasources" className={styles.multiselect}> <div aria-label="Filter datasources" className={styles.multiselect}>
<Select <Select
isMulti={true} isMulti={true}
options={exploreDatasources} options={listOfDatasources}
value={datasourceFilters} value={datasourceFilters}
placeholder="Filter queries for specific data sources(s)" placeholder="Filter queries for specific data sources(s)"
onChange={onSelectDatasourceFilters} onChange={onSelectDatasourceFilters}
@ -131,8 +117,21 @@ export function RichHistoryStarredTab(props: Props) {
</div> </div>
</div> </div>
{sortedStarredQueries.map(q => { {sortedStarredQueries.map(q => {
return <RichHistoryCard query={q} key={q.ts} exploreId={exploreId} />; const idx = listOfDatasources.findIndex(d => d.label === q.datasourceName);
return (
<RichHistoryCard
query={q}
key={q.ts}
exploreId={exploreId}
dsImg={listOfDatasources[idx].imgUrl}
isRemoved={listOfDatasources[idx].isRemoved}
/>
);
})} })}
<div className={styles.feedback}>
Query history is a beta feature. The history is local to your browser and is not shared with others.
<a href="https://github.com/grafana/grafana/issues/new/choose">Feedback?</a>
</div>
</div> </div>
</div> </div>
); );

View File

@ -43,6 +43,7 @@ import {
deleteAllFromRichHistory, deleteAllFromRichHistory,
updateStarredInRichHistory, updateStarredInRichHistory,
updateCommentInRichHistory, updateCommentInRichHistory,
deleteQueryInRichHistory,
getQueryDisplayText, getQueryDisplayText,
getRichHistory, getRichHistory,
} from 'app/core/utils/richHistory'; } from 'app/core/utils/richHistory';
@ -525,6 +526,9 @@ export const updateRichHistory = (ts: number, property: string, updatedProperty?
if (property === 'comment') { if (property === 'comment') {
nextRichHistory = updateCommentInRichHistory(getState().explore.richHistory, ts, updatedProperty); nextRichHistory = updateCommentInRichHistory(getState().explore.richHistory, ts, updatedProperty);
} }
if (property === 'delete') {
nextRichHistory = deleteQueryInRichHistory(getState().explore.richHistory, ts);
}
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory })); dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
}; };
}; };