mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c7951a2575
commit
db85c3e7b9
@ -7,6 +7,7 @@ import {
|
||||
createQueryHeading,
|
||||
createDataQuery,
|
||||
deleteAllFromRichHistory,
|
||||
deleteQueryInRichHistory,
|
||||
} from './richHistory';
|
||||
import store from 'app/core/store';
|
||||
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', () => {
|
||||
it('should correctly map number to value', () => {
|
||||
const value = mapNumbertoTimeInSlider(25);
|
||||
|
@ -6,6 +6,7 @@ import { DataQuery, ExploreMode } from '@grafana/data';
|
||||
import { renderUrl } from 'app/core/utils/url';
|
||||
import store from 'app/core/store';
|
||||
import { serializeStateToUrlParam, SortOrder } from './explore';
|
||||
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
||||
|
||||
// Types
|
||||
import { ExploreUrlState, RichHistoryQuery } from 'app/types/explore';
|
||||
@ -16,6 +17,7 @@ export const RICH_HISTORY_SETTING_KEYS = {
|
||||
retentionPeriod: 'grafana.explore.richHistory.retentionPeriod',
|
||||
starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab',
|
||||
activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly',
|
||||
datasourceFilters: 'grafana.explore.richHistory.datasourceFilters',
|
||||
};
|
||||
|
||||
/*
|
||||
@ -113,6 +115,12 @@ export function updateCommentInRichHistory(
|
||||
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) => {
|
||||
let sortFunc;
|
||||
|
||||
@ -257,3 +265,31 @@ export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortO
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -83,8 +83,8 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
|
||||
super(props);
|
||||
this.state = {
|
||||
activeTab: this.props.firstTab,
|
||||
datasourceFilters: null,
|
||||
sortOrder: SortOrder.Descending,
|
||||
datasourceFilters: store.getObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, null),
|
||||
retentionPeriod: store.getObject(RICH_HISTORY_SETTING_KEYS.retentionPeriod, 7),
|
||||
starredTabAsFirstTab: store.getBool(RICH_HISTORY_SETTING_KEYS.starredTabAsFirstTab, false),
|
||||
activeDatasourceOnly: store.getBool(RICH_HISTORY_SETTING_KEYS.activeDatasourceOnly, false),
|
||||
@ -115,6 +115,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
|
||||
};
|
||||
|
||||
onSelectDatasourceFilters = (value: SelectableValue[] | null) => {
|
||||
store.setObject(RICH_HISTORY_SETTING_KEYS.datasourceFilters, value);
|
||||
this.setState({ datasourceFilters: value });
|
||||
};
|
||||
|
||||
@ -133,7 +134,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
|
||||
? this.onSelectDatasourceFilters([
|
||||
{ label: this.props.activeDatasourceInstance, value: this.props.activeDatasourceInstance },
|
||||
])
|
||||
: this.onSelectDatasourceFilters(null);
|
||||
: this.onSelectDatasourceFilters(this.state.datasourceFilters);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -15,10 +15,11 @@ const setup = (propOverrides?: Partial<Props>) => {
|
||||
queries: ['query1', 'query2', 'query3'],
|
||||
sessionName: '',
|
||||
},
|
||||
changeQuery: jest.fn(),
|
||||
dsImg: '/app/img',
|
||||
isRemoved: false,
|
||||
changeDatasource: jest.fn(),
|
||||
clearQueries: jest.fn(),
|
||||
updateRichHistory: jest.fn(),
|
||||
setQueries: jest.fn(),
|
||||
exploreId: ExploreId.left,
|
||||
datasourceInstance: { name: 'Datasource' } as DataSourceApi,
|
||||
};
|
||||
@ -62,6 +63,18 @@ describe('RichHistoryCard', () => {
|
||||
.text()
|
||||
).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', () => {
|
||||
it('should render comment, if comment present', () => {
|
||||
|
@ -2,54 +2,91 @@ import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
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 { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
import { copyStringToClipboard, createUrlFromRichHistory, createDataQuery } from 'app/core/utils/richHistory';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { changeQuery, changeDatasource, clearQueries, updateRichHistory } from '../state/actions';
|
||||
import { changeDatasource, updateRichHistory, setQueries } from '../state/actions';
|
||||
export interface Props {
|
||||
query: RichHistoryQuery;
|
||||
changeQuery: typeof changeQuery;
|
||||
dsImg: string;
|
||||
isRemoved: boolean;
|
||||
changeDatasource: typeof changeDatasource;
|
||||
clearQueries: typeof clearQueries;
|
||||
updateRichHistory: typeof updateRichHistory;
|
||||
setQueries: typeof setQueries;
|
||||
exploreId: ExploreId;
|
||||
datasourceInstance: DataSourceApi;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => {
|
||||
const borderColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark4;
|
||||
const cardBottomPadding = hasComment ? theme.spacing.sm : theme.spacing.xs;
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, isRemoved: boolean) => {
|
||||
/* Hard-coded value so all buttons and icons on right side of card are aligned */
|
||||
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 {
|
||||
queryCard: css`
|
||||
${styleMixins.listItem(theme)}
|
||||
display: flex;
|
||||
padding: ${theme.spacing.sm} ${theme.spacing.sm} ${cardBottomPadding};
|
||||
flex-direction: column;
|
||||
border: 1px solid ${borderColor};
|
||||
margin: ${theme.spacing.sm} 0;
|
||||
box-shadow: ${cardBoxShadow};
|
||||
background-color: ${cardColor};
|
||||
border-radius: ${theme.border.radius};
|
||||
.starred {
|
||||
color: ${theme.colors.orange};
|
||||
}
|
||||
`,
|
||||
queryCardLeft: css`
|
||||
padding-right: 10px;
|
||||
width: calc(100% - 150px);
|
||||
cursor: pointer;
|
||||
cardRow: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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`
|
||||
width: 150px;
|
||||
height: ${theme.height.sm};
|
||||
datasourceContainer: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
font-weight: ${theme.typography.weight.bold};
|
||||
`,
|
||||
queryActionButtons: css`
|
||||
max-width: ${rigtColumnContentWidth};
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
font-size: ${theme.typography.size.base};
|
||||
i {
|
||||
margin: ${theme.spacing.xs};
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
queryContainer: css`
|
||||
font-weight: ${theme.typography.weight.bold};
|
||||
width: calc(100% - ${rigtColumnWidth});
|
||||
`,
|
||||
queryRow: css`
|
||||
border-top: 1px solid ${borderColor};
|
||||
word-break: break-all;
|
||||
@ -59,142 +96,174 @@ const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => {
|
||||
padding: 0 0 4px 0;
|
||||
}
|
||||
`,
|
||||
buttonRow: css`
|
||||
> * {
|
||||
margin-right: ${theme.spacing.xs};
|
||||
}
|
||||
updateCommentContainer: css`
|
||||
width: calc(100% + ${rigtColumnWidth});
|
||||
margin-top: ${theme.spacing.sm};
|
||||
`,
|
||||
comment: css`
|
||||
overflow-wrap: break-word;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
font-weight: ${theme.typography.weight.regular};
|
||||
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) {
|
||||
const {
|
||||
query,
|
||||
dsImg,
|
||||
isRemoved,
|
||||
updateRichHistory,
|
||||
changeQuery,
|
||||
changeDatasource,
|
||||
exploreId,
|
||||
clearQueries,
|
||||
datasourceInstance,
|
||||
setQueries,
|
||||
} = props;
|
||||
const [activeUpdateComment, setActiveUpdateComment] = useState(false);
|
||||
const [comment, setComment] = useState<string | undefined>(query.comment);
|
||||
|
||||
const toggleActiveUpdateComment = () => setActiveUpdateComment(!activeUpdateComment);
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, Boolean(query.comment));
|
||||
const styles = getStyles(theme, isRemoved);
|
||||
|
||||
const changeQueries = () => {
|
||||
query.queries.forEach((q, i) => {
|
||||
const dataQuery = createDataQuery(query, q, i);
|
||||
changeQuery(exploreId, dataQuery, i);
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeQuery = async (query: RichHistoryQuery) => {
|
||||
const onRunQuery = async () => {
|
||||
const dataQueries = query.queries.map((q, i) => createDataQuery(query, q, i));
|
||||
if (query.datasourceName !== datasourceInstance?.name) {
|
||||
await changeDatasource(exploreId, query.datasourceName);
|
||||
changeQueries();
|
||||
setQueries(exploreId, dataQueries);
|
||||
} else {
|
||||
clearQueries(exploreId);
|
||||
changeQueries();
|
||||
setQueries(exploreId, dataQueries);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className={styles.queryCard}>
|
||||
<div className={styles.queryCardLeft} title="Add queries to query editor" onClick={() => onChangeQuery(query)}>
|
||||
{query.queries.map((q, i) => {
|
||||
return (
|
||||
<div aria-label="Query text" key={`${q}-${i}`} className={styles.queryRow}>
|
||||
{q}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!activeUpdateComment && query.comment && (
|
||||
<div aria-label="Query comment" className={styles.comment}>
|
||||
{query.comment}
|
||||
<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>
|
||||
)}
|
||||
{activeUpdateComment && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
{queryActionButtons}
|
||||
</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 className={cx(styles.cardRow)}>
|
||||
<div className={styles.queryContainer}>
|
||||
{query.queries.map((q, i) => {
|
||||
return (
|
||||
<div aria-label="Query text" key={`${q}-${i}`} className={styles.queryRow}>
|
||||
{q}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!activeUpdateComment && query.comment && (
|
||||
<div aria-label="Query comment" className={styles.comment}>
|
||||
{query.comment}
|
||||
</div>
|
||||
)}
|
||||
{activeUpdateComment && updateComment}
|
||||
</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>
|
||||
);
|
||||
@ -212,10 +281,9 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreI
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
changeQuery,
|
||||
changeDatasource,
|
||||
clearQueries,
|
||||
updateRichHistory,
|
||||
setQueries,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(RichHistoryCard));
|
||||
|
@ -8,7 +8,6 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
// Utils
|
||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
import { getExploreDatasources } from '../state/selectors';
|
||||
|
||||
import { SortOrder } from 'app/core/utils/explore';
|
||||
import {
|
||||
@ -16,6 +15,7 @@ import {
|
||||
mapNumbertoTimeInSlider,
|
||||
createRetentionPeriodBoundary,
|
||||
mapQueriesToHeadings,
|
||||
createDatasourcesList,
|
||||
} from 'app/core/utils/richHistory';
|
||||
|
||||
// Components
|
||||
@ -136,14 +136,8 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, height);
|
||||
const listOfDsNamesWithQueries = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
|
||||
|
||||
/* 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 datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
|
||||
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
|
||||
|
||||
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
|
||||
const filteredQueriesByDatasource = datasourceFilters
|
||||
@ -193,7 +187,7 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
<div aria-label="Filter datasources" className={styles.multiselect}>
|
||||
<Select
|
||||
isMulti={true}
|
||||
options={datasources}
|
||||
options={listOfDatasources}
|
||||
value={datasourceFilters}
|
||||
placeholder="Filter queries for specific data sources(s)"
|
||||
onChange={onSelectDatasourceFilters}
|
||||
@ -215,9 +209,18 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
<div className={styles.heading}>
|
||||
{heading} <span className={styles.queries}>{mappedQueriesToHeadings[heading].length} queries</span>
|
||||
</div>
|
||||
{mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => (
|
||||
<RichHistoryCard query={q} key={q.ts} exploreId={exploreId} />
|
||||
))}
|
||||
{mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => {
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
@ -8,10 +8,9 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
// Utils
|
||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
import { getExploreDatasources } from '../state/selectors';
|
||||
|
||||
import { SortOrder } from '../../../core/utils/explore';
|
||||
import { sortQueries } from '../../../core/utils/richHistory';
|
||||
import { sortQueries, createDatasourcesList } from '../../../core/utils/richHistory';
|
||||
|
||||
// Components
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
@ -33,17 +32,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
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`
|
||||
width: 100%;
|
||||
@ -63,19 +51,18 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
sort: css`
|
||||
width: 170px;
|
||||
`,
|
||||
sessionName: css`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
feedback: css`
|
||||
height: 60px;
|
||||
margin-top: ${theme.spacing.lg};
|
||||
h4 {
|
||||
margin: 0 10px 0 0;
|
||||
display: flex;
|
||||
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 styles = getStyles(theme);
|
||||
const listOfDsNamesWithQueries = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
|
||||
const exploreDatasources = getExploreDatasources()
|
||||
?.filter(ds => listOfDsNamesWithQueries.includes(ds.name))
|
||||
.map(d => {
|
||||
return { value: d.value!, label: d.value!, imgUrl: d.meta.info.logos.small };
|
||||
});
|
||||
|
||||
const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
|
||||
const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory);
|
||||
|
||||
const listOfDatasourceFilters = datasourceFilters?.map(d => d.value);
|
||||
|
||||
const starredQueries = queries.filter(q => q.starred === true);
|
||||
const starredQueriesFilteredByDatasource = datasourceFilters
|
||||
? starredQueries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName))
|
||||
: starredQueries;
|
||||
|
||||
const sortedStarredQueries = sortQueries(starredQueriesFilteredByDatasource, sortOrder);
|
||||
|
||||
return (
|
||||
@ -114,7 +100,7 @@ export function RichHistoryStarredTab(props: Props) {
|
||||
<div aria-label="Filter datasources" className={styles.multiselect}>
|
||||
<Select
|
||||
isMulti={true}
|
||||
options={exploreDatasources}
|
||||
options={listOfDatasources}
|
||||
value={datasourceFilters}
|
||||
placeholder="Filter queries for specific data sources(s)"
|
||||
onChange={onSelectDatasourceFilters}
|
||||
@ -131,8 +117,21 @@ export function RichHistoryStarredTab(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
{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>
|
||||
);
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
deleteAllFromRichHistory,
|
||||
updateStarredInRichHistory,
|
||||
updateCommentInRichHistory,
|
||||
deleteQueryInRichHistory,
|
||||
getQueryDisplayText,
|
||||
getRichHistory,
|
||||
} from 'app/core/utils/richHistory';
|
||||
@ -525,6 +526,9 @@ export const updateRichHistory = (ts: number, property: string, updatedProperty?
|
||||
if (property === 'comment') {
|
||||
nextRichHistory = updateCommentInRichHistory(getState().explore.richHistory, ts, updatedProperty);
|
||||
}
|
||||
if (property === 'delete') {
|
||||
nextRichHistory = deleteQueryInRichHistory(getState().explore.richHistory, ts);
|
||||
}
|
||||
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user