Commit 54469006 by Ivana Huckova Committed by GitHub

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
parent e38e08df
......@@ -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)}
/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment