Rich History: UX adjustments and fixes (#22729)
* Initial commit
* Fix spelling of data sources
* Display sorting value for starred and query tab
* Fix handle color for light theme
* Add close button and fix animation
* Remove toggling of tabs
* Stop event propagation when clicking on comment buttons
* Add title for card functionality
* Remove interpolation for easier searchability of variables
* Improve syncing of comments and starred
* Add modal to check if user wants to permanently delete history
* Fix the height of the query card buttons
* Adjust slider's width based on drawer width
* Add spacing between slider and legend
* Semantic variable naming
* Fix disabled button when live tailing
* Add error handling
* Remove unused imports
* Fix starring, remove useEffect
* Remove emiting of appEvents.alertError in store
* Remove unused imports
(cherry picked from commit 544690060a)
This commit is contained in:
committed by
Arve Knudsen
parent
e347b62cee
commit
26d701dcf9
@@ -13,9 +13,9 @@ import { ExploreUrlState, RichHistoryQuery } from 'app/types/explore';
|
||||
const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
|
||||
|
||||
export const RICH_HISTORY_SETTING_KEYS = {
|
||||
retentionPeriod: `${RICH_HISTORY_KEY}.retentionPeriod`,
|
||||
starredTabAsFirstTab: `${RICH_HISTORY_KEY}.starredTabAsFirstTab`,
|
||||
activeDatasourceOnly: `${RICH_HISTORY_KEY}.activeDatasourceOnly`,
|
||||
retentionPeriod: 'grafana.explore.richHistory.retentionPeriod',
|
||||
starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab',
|
||||
activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly',
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -60,8 +60,14 @@ export function addToRichHistory(
|
||||
];
|
||||
|
||||
/* Combine all queries of a datasource type into one rich history */
|
||||
store.setObject(RICH_HISTORY_KEY, newHistory);
|
||||
return newHistory;
|
||||
const isSaved = store.setObject(RICH_HISTORY_KEY, newHistory);
|
||||
|
||||
/* If newHistory is succesfully saved, return it. Otherwise return not updated richHistory. */
|
||||
if (isSaved) {
|
||||
return newHistory;
|
||||
} else {
|
||||
return richHistory;
|
||||
}
|
||||
}
|
||||
|
||||
return richHistory;
|
||||
|
||||
@@ -322,7 +322,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
['explore-active-button']: showRichHistory,
|
||||
})}
|
||||
onClick={this.toggleShowRichHistory}
|
||||
disabled={isLive}
|
||||
>
|
||||
<i className={'fa fa-fw fa-history icon-margin-right '} />
|
||||
<span className="btn-title">{'\xA0' + 'Query history'}</span>
|
||||
@@ -382,7 +381,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showRichHistory && <RichHistoryContainer width={width} exploreId={exploreId} />}
|
||||
{showRichHistory && (
|
||||
<RichHistoryContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onClose={this.toggleShowRichHistory}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryAlert>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,9 @@ interface RichHistoryProps extends Themeable {
|
||||
activeDatasourceInstance: string;
|
||||
firstTab: Tabs;
|
||||
exploreId: ExploreId;
|
||||
height: number;
|
||||
deleteRichHistory: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface RichHistoryState {
|
||||
@@ -60,6 +62,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
background-color: ${tabContentBg};
|
||||
padding: ${theme.spacing.md};
|
||||
`,
|
||||
close: css`
|
||||
position: absolute;
|
||||
right: ${theme.spacing.sm};
|
||||
cursor: pointer;
|
||||
`,
|
||||
tabs: css`
|
||||
background-color: ${tabBarBg};
|
||||
padding-top: ${theme.spacing.sm};
|
||||
@@ -142,15 +149,8 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
datasourceFilters,
|
||||
sortOrder,
|
||||
activeTab,
|
||||
starredTabAsFirstTab,
|
||||
activeDatasourceOnly,
|
||||
retentionPeriod,
|
||||
} = this.state;
|
||||
const { theme, richHistory, exploreId, deleteRichHistory } = this.props;
|
||||
const { datasourceFilters, sortOrder, activeTab, activeDatasourceOnly, retentionPeriod } = this.state;
|
||||
const { theme, richHistory, height, exploreId, deleteRichHistory, onClose } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const QueriesTab = {
|
||||
@@ -166,6 +166,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
|
||||
onChangeSortOrder={this.onChangeSortOrder}
|
||||
onSelectDatasourceFilters={this.onSelectDatasourceFilters}
|
||||
exploreId={exploreId}
|
||||
height={height}
|
||||
/>
|
||||
),
|
||||
icon: 'fa fa-history',
|
||||
@@ -205,8 +206,7 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
|
||||
icon: 'gicon gicon-preferences',
|
||||
};
|
||||
|
||||
let tabs = starredTabAsFirstTab ? [StarredTab, QueriesTab, SettingsTab] : [QueriesTab, StarredTab, SettingsTab];
|
||||
|
||||
let tabs = [QueriesTab, StarredTab, SettingsTab];
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<TabsBar className={styles.tabs}>
|
||||
@@ -219,6 +219,9 @@ class UnThemedRichHistory extends PureComponent<RichHistoryProps, RichHistorySta
|
||||
icon={t.icon}
|
||||
/>
|
||||
))}
|
||||
<div className={styles.close} onClick={onClose}>
|
||||
<i className="fa fa-times" title="Close query history" />
|
||||
</div>
|
||||
</TabsBar>
|
||||
<CustomScrollbar
|
||||
className={css`
|
||||
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => {
|
||||
const bgColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark4;
|
||||
const borderColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark4;
|
||||
const cardBottomPadding = hasComment ? theme.spacing.sm : theme.spacing.xs;
|
||||
|
||||
return {
|
||||
@@ -30,7 +30,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => {
|
||||
display: flex;
|
||||
padding: ${theme.spacing.sm} ${theme.spacing.sm} ${cardBottomPadding};
|
||||
margin: ${theme.spacing.sm} 0;
|
||||
|
||||
.starred {
|
||||
color: ${theme.colors.orange};
|
||||
}
|
||||
@@ -42,6 +41,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => {
|
||||
`,
|
||||
queryCardRight: css`
|
||||
width: 150px;
|
||||
height: ${theme.height.sm};
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -51,7 +51,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, hasComment?: boolean) => {
|
||||
}
|
||||
`,
|
||||
queryRow: css`
|
||||
border-top: 1px solid ${bgColor};
|
||||
border-top: 1px solid ${borderColor};
|
||||
word-break: break-all;
|
||||
padding: 4px 2px;
|
||||
:first-child {
|
||||
@@ -82,7 +82,6 @@ export function RichHistoryCard(props: Props) {
|
||||
clearQueries,
|
||||
datasourceInstance,
|
||||
} = props;
|
||||
const [starred, setStared] = useState(query.starred);
|
||||
const [activeUpdateComment, setActiveUpdateComment] = useState(false);
|
||||
const [comment, setComment] = useState<string | undefined>(query.comment);
|
||||
|
||||
@@ -109,7 +108,7 @@ export function RichHistoryCard(props: Props) {
|
||||
|
||||
return (
|
||||
<div className={styles.queryCard}>
|
||||
<div className={styles.queryCardLeft} onClick={() => onChangeQuery(query)}>
|
||||
<div className={styles.queryCardLeft} title="Add queries to query editor" onClick={() => onChangeQuery(query)}>
|
||||
{query.queries.map((q, i) => {
|
||||
return (
|
||||
<div key={`${q}-${i}`} className={styles.queryRow}>
|
||||
@@ -123,12 +122,18 @@ export function RichHistoryCard(props: Props) {
|
||||
<Forms.TextArea
|
||||
value={comment}
|
||||
placeholder={comment ? undefined : 'add comment'}
|
||||
onChange={e => setComment(e.currentTarget.value)}
|
||||
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();
|
||||
}}
|
||||
@@ -140,7 +145,8 @@ export function RichHistoryCard(props: Props) {
|
||||
className={css`
|
||||
margin-left: 8px;
|
||||
`}
|
||||
onClick={() => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
toggleActiveUpdateComment();
|
||||
setComment(query.comment);
|
||||
}}
|
||||
@@ -179,10 +185,9 @@ export function RichHistoryCard(props: Props) {
|
||||
title="Copy link to clipboard"
|
||||
></i>
|
||||
<i
|
||||
className={cx('fa fa-fw', starred ? 'fa-star starred' : 'fa-star-o')}
|
||||
className={cx('fa fa-fw', query.starred ? 'fa-star starred' : 'fa-star-o')}
|
||||
onClick={() => {
|
||||
updateRichHistory(query.ts, 'starred');
|
||||
setStared(!starred);
|
||||
}}
|
||||
title={query.starred ? 'Unstar query' : 'Star query'}
|
||||
></i>
|
||||
|
||||
@@ -22,33 +22,33 @@ import { RichHistory, Tabs } from './RichHistory';
|
||||
import { deleteRichHistory } from '../state/actions';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const bgColor = theme.isLight ? theme.colors.gray5 : theme.colors.gray15;
|
||||
const bg = theme.isLight ? theme.colors.gray7 : theme.colors.dark2;
|
||||
const borderColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark6;
|
||||
const handleHover = theme.isLight ? theme.colors.gray10 : theme.colors.gray33;
|
||||
const containerBackground = theme.isLight ? theme.colors.gray7 : theme.colors.dark2;
|
||||
const containerBorderColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark6;
|
||||
const handleBackground = theme.isLight ? theme.colors.gray5 : theme.colors.gray15;
|
||||
const handleDots = theme.isLight ? theme.colors.gray70 : theme.colors.gray33;
|
||||
const handleDotsHover = theme.isLight ? theme.colors.gray33 : theme.colors.dark7;
|
||||
const handleBackgroundHover = theme.isLight ? theme.colors.gray70 : theme.colors.gray33;
|
||||
const handleDotsHover = theme.isLight ? theme.colors.gray5 : theme.colors.dark7;
|
||||
|
||||
return {
|
||||
container: css`
|
||||
position: fixed !important;
|
||||
bottom: 0;
|
||||
background: ${bg};
|
||||
border-top: 1px solid ${borderColor};
|
||||
background: ${containerBackground};
|
||||
border-top: 1px solid ${containerBorderColor};
|
||||
margin: 0px;
|
||||
margin-right: -${theme.spacing.md};
|
||||
margin-left: -${theme.spacing.md};
|
||||
`,
|
||||
drawerActive: css`
|
||||
opacity: 1;
|
||||
transition: transform 0.3s ease-in;
|
||||
transition: transform 0.5s ease-in;
|
||||
`,
|
||||
drawerNotActive: css`
|
||||
opacity: 0;
|
||||
transform: translateY(150px);
|
||||
transform: translateY(400px);
|
||||
`,
|
||||
rzHandle: css`
|
||||
background: ${bgColor};
|
||||
background: ${handleBackground};
|
||||
transition: 0.3s background ease-in-out;
|
||||
position: relative;
|
||||
width: 200px !important;
|
||||
@@ -57,7 +57,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${handleHover};
|
||||
background-color: ${handleBackgroundHover};
|
||||
|
||||
&:after {
|
||||
border-color: ${handleDotsHover};
|
||||
@@ -84,18 +84,20 @@ interface Props {
|
||||
richHistory: RichHistoryQuery[];
|
||||
firstTab: Tabs;
|
||||
deleteRichHistory: typeof deleteRichHistory;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function RichHistoryContainer(props: Props) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [height, setHeight] = useState(400);
|
||||
|
||||
/* To create sliding animation for rich history drawer */
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setVisible(true), 100);
|
||||
const timer = setTimeout(() => setVisible(true), 10);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const { richHistory, width, firstTab, activeDatasourceInstance, exploreId, deleteRichHistory } = props;
|
||||
const { richHistory, width, firstTab, activeDatasourceInstance, exploreId, deleteRichHistory, onClose } = props;
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
const drawerWidth = `${width + 31.5}px`;
|
||||
@@ -118,6 +120,9 @@ function RichHistoryContainer(props: Props) {
|
||||
maxHeight="100vh"
|
||||
maxWidth={drawerWidth}
|
||||
minWidth={drawerWidth}
|
||||
onResize={(e, dir, ref) => {
|
||||
setHeight(Number(ref.style.height.slice(0, -2)));
|
||||
}}
|
||||
>
|
||||
<RichHistory
|
||||
richHistory={richHistory}
|
||||
@@ -125,6 +130,8 @@ function RichHistoryContainer(props: Props) {
|
||||
activeDatasourceInstance={activeDatasourceInstance}
|
||||
exploreId={exploreId}
|
||||
deleteRichHistory={deleteRichHistory}
|
||||
onClose={onClose}
|
||||
height={height}
|
||||
/>
|
||||
</Resizable>
|
||||
);
|
||||
|
||||
@@ -30,15 +30,17 @@ interface Props {
|
||||
datasourceFilters: SelectableValue[] | null;
|
||||
retentionPeriod: number;
|
||||
exploreId: ExploreId;
|
||||
height: number;
|
||||
onChangeSortOrder: (sortOrder: SortOrder) => void;
|
||||
onSelectDatasourceFilters: (value: SelectableValue[] | null) => void;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => {
|
||||
const bgColor = theme.isLight ? theme.colors.gray5 : theme.colors.dark4;
|
||||
|
||||
/* 134px is based on the width of the Query history tabs bar, so the content is aligned to right side of the tab */
|
||||
const cardWidth = '100% - 134px';
|
||||
const sliderHeight = `${height - 200}px`;
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
@@ -61,9 +63,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
margin-right: ${theme.spacing.sm};
|
||||
.slider {
|
||||
bottom: 10px;
|
||||
height: 200px;
|
||||
height: ${sliderHeight};
|
||||
width: 127px;
|
||||
padding: ${theme.spacing.xs} 0;
|
||||
padding: ${theme.spacing.sm} 0;
|
||||
}
|
||||
`,
|
||||
slider: css`
|
||||
@@ -127,12 +129,13 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
activeDatasourceOnly,
|
||||
retentionPeriod,
|
||||
exploreId,
|
||||
height,
|
||||
} = props;
|
||||
|
||||
const [sliderRetentionFilter, setSliderRetentionFilter] = useState<[number, number]>([0, retentionPeriod]);
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
const styles = getStyles(theme, height);
|
||||
const listOfDsNamesWithQueries = uniqBy(queries, 'datasourceName').map(d => d.datasourceName);
|
||||
|
||||
/* Display only explore datasoources, that have saved queries */
|
||||
@@ -192,13 +195,14 @@ export function RichHistoryQueriesTab(props: Props) {
|
||||
isMulti={true}
|
||||
options={datasources}
|
||||
value={datasourceFilters}
|
||||
placeholder="Filter queries for specific datasources(s)"
|
||||
placeholder="Filter queries for specific data sources(s)"
|
||||
onChange={onSelectDatasourceFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.sort}>
|
||||
<Select
|
||||
value={sortOrderOptions.filter(order => order.value === sortOrder)}
|
||||
options={sortOrderOptions}
|
||||
placeholder="Sort queries by"
|
||||
onChange={e => onChangeSortOrder(e.value as SortOrder)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { css } from 'emotion';
|
||||
import { stylesFactory, useTheme, Forms } from '@grafana/ui';
|
||||
import { GrafanaTheme, AppEvents } from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CoreEvents } from 'app/types';
|
||||
|
||||
interface RichHistorySettingsProps {
|
||||
retentionPeriod: number;
|
||||
@@ -57,6 +58,19 @@ export function RichHistorySettings(props: RichHistorySettingsProps) {
|
||||
const styles = getStyles(theme);
|
||||
const selectedOption = retentionPeriodOptions.find(v => v.value === retentionPeriod);
|
||||
|
||||
const onDelete = () => {
|
||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to permanently delete your query history?',
|
||||
yesText: 'Delete',
|
||||
icon: 'fa-trash',
|
||||
onConfirm: () => {
|
||||
deleteRichHistory();
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Query history deleted']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Forms.Field
|
||||
@@ -78,10 +92,10 @@ export function RichHistorySettings(props: RichHistorySettingsProps) {
|
||||
<div className={styles.label}>Change the default active tab from “Query history” to “Starred”</div>
|
||||
</div>
|
||||
</Forms.Field>
|
||||
<Forms.Field label="Datasource behaviour" description=" " className="space-between">
|
||||
<Forms.Field label="Data source behaviour" description=" " className="space-between">
|
||||
<div className={styles.switch}>
|
||||
<Forms.Switch value={activeDatasourceOnly} onChange={toggleactiveDatasourceOnly}></Forms.Switch>
|
||||
<div className={styles.label}>Only show queries for datasource currently active in Explore</div>
|
||||
<div className={styles.label}>Only show queries for data source currently active in Explore</div>
|
||||
</div>
|
||||
</Forms.Field>
|
||||
<div
|
||||
@@ -98,13 +112,7 @@ export function RichHistorySettings(props: RichHistorySettingsProps) {
|
||||
>
|
||||
Delete all of your query history, permanently.
|
||||
</div>
|
||||
<Forms.Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
deleteRichHistory();
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Query history deleted']);
|
||||
}}
|
||||
>
|
||||
<Forms.Button variant="destructive" onClick={onDelete}>
|
||||
Clear query history
|
||||
</Forms.Button>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +116,7 @@ export function RichHistoryStarredTab(props: Props) {
|
||||
isMulti={true}
|
||||
options={exploreDatasources}
|
||||
value={datasourceFilters}
|
||||
placeholder="Filter queries for specific datasources(s)"
|
||||
placeholder="Filter queries for specific data sources(s)"
|
||||
onChange={onSelectDatasourceFilters}
|
||||
/>
|
||||
</div>
|
||||
@@ -124,6 +124,7 @@ export function RichHistoryStarredTab(props: Props) {
|
||||
<div className={styles.sort}>
|
||||
<Select
|
||||
options={sortOrderOptions}
|
||||
value={sortOrderOptions.filter(order => order.value === sortOrder)}
|
||||
placeholder="Sort queries by"
|
||||
onChange={e => onChangeSortOrder(e.value as SortOrder)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user