mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
* Load Rich History when the container is opened * Store rich history for each pane separately * Do not update currently opened query history when an item is added It's impossible to figure out if the item should be added or not, because filters are applied in the backend. We don't want to replicate that filtering logic in frontend. One way to make it work could be by refreshing both panes. * Test starring and deleting query history items when both panes are open * Remove e2e dependency on ExploreId * Fix unit test * Assert exact queries * Simplify test * Fix e2e tests * Fix toolbar a11y * Reload the history after an item is added * Fix unit test * Remove references to Explore from generic PageToolbar component * Update test name * Fix test assertion * Add issue item to TODO * Improve test assertion * Simplify test setup * Move query history settings to persistence layer * Fix test import * Fix unit test * Fix unit test * Test local storage settings API * Code formatting * Fix linting errors * Add an integration test * Add missing aria role * Fix a11y issues * Fix a11y issues * Use divs instead of ul/li Otherwis,e pa11y-ci reports the error below claiming there are no children with role=tab: Certain ARIA roles must contain particular children (https://dequeuniversity.com/rules/axe/4.3/aria-required-children?application=axeAPI) (#reactRoot > div > main > div:nth-child(3) > div > div:nth-child(1) > div > div:nth-child(1) > div > div > nav > div:nth-child(2) > ul) <ul class="css-af3vye" role="tablist"><li class="css-1ciwanz"><a href...</ul> * Clean up settings tab * Remove redundant aria label * Remove redundant container * Clean up test assertions * Move filtering to persistence layer * Move filtering to persistence layer * Simplify applying filters * Split applying filters and reloading the history * Debounce updating filters * Update tests * Fix waiting for debounced results * Clear results when switching tabs * Improve test coverage * Update docs * Revert extra handling for uid (will be added when we introduce remote storage) * Fix betterer conflicts * Fix imports * Fix imports * Simplify test setup * Simplify assertion * Improve readability * Remove unnecessary casting * Mock backend in integration tests
250 lines
8.1 KiB
TypeScript
250 lines
8.1 KiB
TypeScript
import { css } from '@emotion/css';
|
|
import React, { useEffect } from 'react';
|
|
|
|
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
|
import { FilterInput, MultiSelect, RangeSlider, Select, stylesFactory, useTheme } from '@grafana/ui';
|
|
import {
|
|
createDatasourcesList,
|
|
mapNumbertoTimeInSlider,
|
|
mapQueriesToHeadings,
|
|
SortOrder,
|
|
RichHistorySearchFilters,
|
|
RichHistorySettings,
|
|
} from 'app/core/utils/richHistory';
|
|
import { ExploreId, RichHistoryQuery } from 'app/types/explore';
|
|
|
|
import { sortOrderOptions } from './RichHistory';
|
|
import RichHistoryCard from './RichHistoryCard';
|
|
|
|
export interface Props {
|
|
queries: RichHistoryQuery[];
|
|
activeDatasourceInstance?: string;
|
|
updateFilters: (filtersToUpdate?: Partial<RichHistorySearchFilters>) => void;
|
|
clearRichHistoryResults: () => void;
|
|
richHistorySettings: RichHistorySettings;
|
|
richHistorySearchFilters?: RichHistorySearchFilters;
|
|
exploreId: ExploreId;
|
|
height: number;
|
|
}
|
|
|
|
const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => {
|
|
const bgColor = theme.isLight ? theme.palette.gray5 : theme.palette.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 - 180}px`;
|
|
return {
|
|
container: css`
|
|
display: flex;
|
|
.label-slider {
|
|
font-size: ${theme.typography.size.sm};
|
|
&:last-of-type {
|
|
margin-top: ${theme.spacing.lg};
|
|
}
|
|
&:first-of-type {
|
|
font-weight: ${theme.typography.weight.semibold};
|
|
margin-bottom: ${theme.spacing.md};
|
|
}
|
|
}
|
|
`,
|
|
containerContent: css`
|
|
width: calc(${cardWidth});
|
|
`,
|
|
containerSlider: css`
|
|
width: 129px;
|
|
margin-right: ${theme.spacing.sm};
|
|
.slider {
|
|
bottom: 10px;
|
|
height: ${sliderHeight};
|
|
width: 129px;
|
|
padding: ${theme.spacing.sm} 0;
|
|
}
|
|
`,
|
|
slider: css`
|
|
position: fixed;
|
|
`,
|
|
selectors: css`
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
`,
|
|
filterInput: css`
|
|
margin-bottom: ${theme.spacing.sm};
|
|
`,
|
|
multiselect: css`
|
|
width: 100%;
|
|
margin-bottom: ${theme.spacing.sm};
|
|
.gf-form-select-box__multi-value {
|
|
background-color: ${bgColor};
|
|
padding: ${theme.spacing.xxs} ${theme.spacing.xs} ${theme.spacing.xxs} ${theme.spacing.sm};
|
|
border-radius: ${theme.border.radius.sm};
|
|
}
|
|
`,
|
|
sort: css`
|
|
width: 170px;
|
|
`,
|
|
sessionName: css`
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: flex-start;
|
|
margin-top: ${theme.spacing.lg};
|
|
h4 {
|
|
margin: 0 10px 0 0;
|
|
}
|
|
`,
|
|
heading: css`
|
|
font-size: ${theme.typography.heading.h4};
|
|
margin: ${theme.spacing.md} ${theme.spacing.xxs} ${theme.spacing.sm} ${theme.spacing.xxs};
|
|
`,
|
|
footer: css`
|
|
height: 60px;
|
|
margin: ${theme.spacing.lg} auto;
|
|
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};
|
|
}
|
|
`,
|
|
queries: css`
|
|
font-size: ${theme.typography.size.sm};
|
|
font-weight: ${theme.typography.weight.regular};
|
|
margin-left: ${theme.spacing.xs};
|
|
`,
|
|
};
|
|
});
|
|
|
|
export function RichHistoryQueriesTab(props: Props) {
|
|
const {
|
|
queries,
|
|
richHistorySearchFilters,
|
|
updateFilters,
|
|
clearRichHistoryResults,
|
|
richHistorySettings,
|
|
exploreId,
|
|
height,
|
|
activeDatasourceInstance,
|
|
} = props;
|
|
|
|
const theme = useTheme();
|
|
const styles = getStyles(theme, height);
|
|
|
|
const listOfDatasources = createDatasourcesList();
|
|
|
|
useEffect(() => {
|
|
const datasourceFilters =
|
|
richHistorySettings.activeDatasourceOnly && activeDatasourceInstance
|
|
? [activeDatasourceInstance]
|
|
: richHistorySettings.lastUsedDatasourceFilters;
|
|
const filters: RichHistorySearchFilters = {
|
|
search: '',
|
|
sortOrder: SortOrder.Descending,
|
|
datasourceFilters,
|
|
from: 0,
|
|
to: richHistorySettings.retentionPeriod,
|
|
starred: false,
|
|
};
|
|
updateFilters(filters);
|
|
|
|
return () => {
|
|
clearRichHistoryResults();
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
if (!richHistorySearchFilters) {
|
|
return <span>Loading...</span>;
|
|
}
|
|
|
|
/* mappedQueriesToHeadings is an object where query headings (stringified dates/data sources)
|
|
* are keys and arrays with queries that belong to that headings are values.
|
|
*/
|
|
const mappedQueriesToHeadings = mapQueriesToHeadings(queries, richHistorySearchFilters.sortOrder);
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<div className={styles.containerSlider}>
|
|
<div className={styles.slider}>
|
|
<div className="label-slider">Filter history</div>
|
|
<div className="label-slider">{mapNumbertoTimeInSlider(richHistorySearchFilters.from)}</div>
|
|
<div className="slider">
|
|
<RangeSlider
|
|
tooltipAlwaysVisible={false}
|
|
min={0}
|
|
max={richHistorySettings.retentionPeriod}
|
|
value={[richHistorySearchFilters.from, richHistorySearchFilters.to]}
|
|
orientation="vertical"
|
|
formatTooltipResult={mapNumbertoTimeInSlider}
|
|
reverse={true}
|
|
onAfterChange={(value) => {
|
|
updateFilters({ from: value![0], to: value![1] });
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="label-slider">{mapNumbertoTimeInSlider(richHistorySearchFilters.to)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.containerContent}>
|
|
<div className={styles.selectors}>
|
|
{!richHistorySettings.activeDatasourceOnly && (
|
|
<MultiSelect
|
|
className={styles.multiselect}
|
|
menuShouldPortal
|
|
options={listOfDatasources.map((ds) => {
|
|
return { value: ds.name, label: ds.name };
|
|
})}
|
|
value={richHistorySearchFilters.datasourceFilters}
|
|
placeholder="Filter queries for data sources(s)"
|
|
aria-label="Filter queries for data sources(s)"
|
|
onChange={(options: SelectableValue[]) => {
|
|
updateFilters({ datasourceFilters: options.map((option) => option.value) });
|
|
}}
|
|
/>
|
|
)}
|
|
<div className={styles.filterInput}>
|
|
<FilterInput
|
|
placeholder="Search queries"
|
|
value={richHistorySearchFilters.search}
|
|
onChange={(search: string) => updateFilters({ search })}
|
|
/>
|
|
</div>
|
|
<div aria-label="Sort queries" className={styles.sort}>
|
|
<Select
|
|
menuShouldPortal
|
|
value={sortOrderOptions.filter((order) => order.value === richHistorySearchFilters.sortOrder)}
|
|
options={sortOrderOptions}
|
|
placeholder="Sort queries by"
|
|
onChange={(e: SelectableValue<SortOrder>) => updateFilters({ sortOrder: e.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{Object.keys(mappedQueriesToHeadings).map((heading) => {
|
|
return (
|
|
<div key={heading}>
|
|
<div className={styles.heading}>
|
|
{heading} <span className={styles.queries}>{mappedQueriesToHeadings[heading].length} queries</span>
|
|
</div>
|
|
{mappedQueriesToHeadings[heading].map((q: RichHistoryQuery) => {
|
|
const idx = listOfDatasources.findIndex((d) => d.name === q.datasourceName);
|
|
return (
|
|
<RichHistoryCard
|
|
query={q}
|
|
key={q.id}
|
|
exploreId={exploreId}
|
|
dsImg={idx === -1 ? 'public/img/icn-datasource.svg' : listOfDatasources[idx].imgUrl}
|
|
isRemoved={idx === -1}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
})}
|
|
<div className={styles.footer}>The history is local to your browser and is not shared with others.</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|