123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- import { css, cx } from '@emotion/css';
- import memoizeOne from 'memoize-one';
- import React, { createRef } from 'react';
- import { connect, ConnectedProps } from 'react-redux';
- import AutoSizer from 'react-virtualized-auto-sizer';
- import { compose } from 'redux';
- import { Unsubscribable } from 'rxjs';
- import { AbsoluteTimeRange, DataQuery, GrafanaTheme2, LoadingState, RawTimeRange } from '@grafana/data';
- import { selectors } from '@grafana/e2e-selectors';
- import { Collapse, CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2, PanelContainer } from '@grafana/ui';
- import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types';
- import appEvents from 'app/core/app_events';
- import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider';
- import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils';
- import { StoreState } from 'app/types';
- import { AbsoluteTimeEvent } from 'app/types/events';
- import { ExploreGraphStyle, ExploreId, ExploreItemState } from 'app/types/explore';
- import { getTimeZone } from '../profile/state/selectors';
- import { ExploreGraph } from './ExploreGraph';
- import { ExploreGraphLabel } from './ExploreGraphLabel';
- import ExploreQueryInspector from './ExploreQueryInspector';
- import { ExploreToolbar } from './ExploreToolbar';
- import LogsContainer from './LogsContainer';
- import { LogsVolumePanel } from './LogsVolumePanel';
- import { NoData } from './NoData';
- import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
- import { NodeGraphContainer } from './NodeGraphContainer';
- import { QueryRows } from './QueryRows';
- import { ResponseErrorContainer } from './ResponseErrorContainer';
- import RichHistoryContainer from './RichHistory/RichHistoryContainer';
- import { SecondaryActions } from './SecondaryActions';
- import TableContainer from './TableContainer';
- import { TraceViewContainer } from './TraceView/TraceViewContainer';
- import { changeSize, changeGraphStyle } from './state/explorePane';
- import { splitOpen } from './state/main';
- import { addQueryRow, loadLogsVolumeData, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query';
- import { makeAbsoluteTime, updateTimeRange } from './state/time';
- const getStyles = (theme: GrafanaTheme2) => {
- return {
- exploreMain: css`
- label: exploreMain;
- // Is needed for some transition animations to work.
- position: relative;
- margin-top: 21px;
- `,
- button: css`
- label: button;
- margin: 1em 4px 0 0;
- `,
- queryContainer: css`
- label: queryContainer;
- // Need to override normal css class and don't want to count on ordering of the classes in html.
- height: auto !important;
- flex: unset !important;
- display: unset !important;
- padding: ${theme.spacing(1)};
- `,
- exploreContainer: css`
- display: flex;
- flex: 1 1 auto;
- flex-direction: column;
- padding: ${theme.spacing(2)};
- padding-top: 0;
- `,
- };
- };
- export interface ExploreProps extends Themeable2 {
- exploreId: ExploreId;
- theme: GrafanaTheme2;
- }
- enum ExploreDrawer {
- RichHistory,
- QueryInspector,
- }
- interface ExploreState {
- openDrawer?: ExploreDrawer;
- }
- export type Props = ExploreProps & ConnectedProps<typeof connector>;
- /**
- * Explore provides an area for quick query iteration for a given datasource.
- * Once a datasource is selected it populates the query section at the top.
- * When queries are run, their results are being displayed in the main section.
- * The datasource determines what kind of query editor it brings, and what kind
- * of results viewers it supports. The state is managed entirely in Redux.
- *
- * SPLIT VIEW
- *
- * Explore can have two Explore areas side-by-side. This is handled in `Wrapper.tsx`.
- * Since there can be multiple Explores (e.g., left and right) each action needs
- * the `exploreId` as first parameter so that the reducer knows which Explore state
- * is affected.
- *
- * DATASOURCE REQUESTS
- *
- * A click on Run Query creates transactions for all DataQueries for all expanded
- * result viewers. New runs are discarding previous runs. Upon completion a transaction
- * saves the result. The result viewers construct their data from the currently existing
- * transactions.
- *
- * The result viewers determine some of the query options sent to the datasource, e.g.,
- * `format`, to indicate eventual transformations by the datasources' result transformers.
- */
- export class Explore extends React.PureComponent<Props, ExploreState> {
- scrollElement: HTMLDivElement | undefined;
- absoluteTimeUnsubsciber: Unsubscribable | undefined;
- topOfViewRef = createRef<HTMLDivElement>();
- constructor(props: Props) {
- super(props);
- this.state = {
- openDrawer: undefined,
- };
- }
- componentDidMount() {
- this.absoluteTimeUnsubsciber = appEvents.subscribe(AbsoluteTimeEvent, this.onMakeAbsoluteTime);
- }
- componentWillUnmount() {
- this.absoluteTimeUnsubsciber?.unsubscribe();
- }
- onChangeTime = (rawRange: RawTimeRange) => {
- const { updateTimeRange, exploreId } = this.props;
- updateTimeRange({ exploreId, rawRange });
- };
- // Use this in help pages to set page to a single query
- onClickExample = (query: DataQuery) => {
- this.props.setQueries(this.props.exploreId, [query]);
- };
- onCellFilterAdded = (filter: FilterItem) => {
- const { value, key, operator } = filter;
- if (operator === FILTER_FOR_OPERATOR) {
- this.onClickFilterLabel(key, value);
- }
- if (operator === FILTER_OUT_OPERATOR) {
- this.onClickFilterOutLabel(key, value);
- }
- };
- onClickFilterLabel = (key: string, value: string) => {
- this.onModifyQueries({ type: 'ADD_FILTER', key, value });
- };
- onClickFilterOutLabel = (key: string, value: string) => {
- this.onModifyQueries({ type: 'ADD_FILTER_OUT', key, value });
- };
- onClickAddQueryRowButton = () => {
- const { exploreId, queryKeys } = this.props;
- this.props.addQueryRow(exploreId, queryKeys.length);
- };
- onMakeAbsoluteTime = () => {
- const { makeAbsoluteTime } = this.props;
- makeAbsoluteTime();
- };
- onModifyQueries = (action: any, index?: number) => {
- const { datasourceInstance } = this.props;
- if (datasourceInstance?.modifyQuery) {
- const modifier = (queries: DataQuery, modification: any) =>
- datasourceInstance.modifyQuery!(queries, modification);
- this.props.modifyQueries(this.props.exploreId, action, modifier, index);
- }
- };
- onResize = (size: { height: number; width: number }) => {
- this.props.changeSize(this.props.exploreId, size);
- };
- onStartScanning = () => {
- // Scanner will trigger a query
- this.props.scanStart(this.props.exploreId);
- };
- onStopScanning = () => {
- this.props.scanStopAction({ exploreId: this.props.exploreId });
- };
- onUpdateTimeRange = (absoluteRange: AbsoluteTimeRange) => {
- const { exploreId, updateTimeRange } = this.props;
- updateTimeRange({ exploreId, absoluteRange });
- };
- onChangeGraphStyle = (graphStyle: ExploreGraphStyle) => {
- const { exploreId, changeGraphStyle } = this.props;
- changeGraphStyle(exploreId, graphStyle);
- };
- toggleShowRichHistory = () => {
- this.setState((state) => {
- return {
- openDrawer: state.openDrawer === ExploreDrawer.RichHistory ? undefined : ExploreDrawer.RichHistory,
- };
- });
- };
- toggleShowQueryInspector = () => {
- this.setState((state) => {
- return {
- openDrawer: state.openDrawer === ExploreDrawer.QueryInspector ? undefined : ExploreDrawer.QueryInspector,
- };
- });
- };
- renderEmptyState(exploreContainerStyles: string) {
- return (
- <div className={cx(exploreContainerStyles)}>
- <NoDataSourceCallToAction />
- </div>
- );
- }
- renderNoData() {
- return <NoData />;
- }
- renderGraphPanel(width: number) {
- const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading, theme, graphStyle } = this.props;
- const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
- const label = <ExploreGraphLabel graphStyle={graphStyle} onChangeGraphStyle={this.onChangeGraphStyle} />;
- return (
- <Collapse label={label} loading={loading} isOpen>
- <ExploreGraph
- graphStyle={graphStyle}
- data={graphResult!}
- height={400}
- width={width - spacing}
- absoluteRange={absoluteRange}
- onChangeTime={this.onUpdateTimeRange}
- timeZone={timeZone}
- annotations={queryResponse.annotations}
- splitOpenFn={splitOpen}
- loadingState={queryResponse.state}
- />
- </Collapse>
- );
- }
- renderLogsVolume(width: number) {
- const { logsVolumeData, exploreId, loadLogsVolumeData, absoluteRange, timeZone, splitOpen } = this.props;
- return (
- <LogsVolumePanel
- absoluteRange={absoluteRange}
- width={width}
- logsVolumeData={logsVolumeData}
- onUpdateTimeRange={this.onUpdateTimeRange}
- timeZone={timeZone}
- splitOpen={splitOpen}
- onLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
- />
- );
- }
- renderTablePanel(width: number) {
- const { exploreId, datasourceInstance, timeZone } = this.props;
- return (
- <TableContainer
- ariaLabel={selectors.pages.Explore.General.table}
- width={width}
- exploreId={exploreId}
- onCellFilterAdded={datasourceInstance?.modifyQuery ? this.onCellFilterAdded : undefined}
- timeZone={timeZone}
- />
- );
- }
- renderLogsPanel(width: number) {
- const { exploreId, syncedTimes, theme, queryResponse } = this.props;
- const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
- return (
- <LogsContainer
- exploreId={exploreId}
- loadingState={queryResponse.state}
- syncedTimes={syncedTimes}
- width={width - spacing}
- onClickFilterLabel={this.onClickFilterLabel}
- onClickFilterOutLabel={this.onClickFilterOutLabel}
- onStartScanning={this.onStartScanning}
- onStopScanning={this.onStopScanning}
- />
- );
- }
- renderNodeGraphPanel() {
- const { exploreId, showTrace, queryResponse, datasourceInstance } = this.props;
- const datasourceType = datasourceInstance ? datasourceInstance?.type : 'unknown';
- return (
- <NodeGraphContainer
- dataFrames={this.memoizedGetNodeGraphDataFrames(queryResponse.series)}
- exploreId={exploreId}
- withTraceView={showTrace}
- datasourceType={datasourceType}
- />
- );
- }
- memoizedGetNodeGraphDataFrames = memoizeOne(getNodeGraphDataFrames);
- renderTraceViewPanel() {
- const { queryResponse, splitOpen, exploreId } = this.props;
- const dataFrames = queryResponse.series.filter((series) => series.meta?.preferredVisualisationType === 'trace');
- return (
- // If there is no data (like 404) we show a separate error so no need to show anything here
- dataFrames.length && (
- <TraceViewContainer
- exploreId={exploreId}
- dataFrames={dataFrames}
- splitOpenFn={splitOpen}
- scrollElement={this.scrollElement}
- queryResponse={queryResponse}
- topOfViewRef={this.topOfViewRef}
- />
- )
- );
- }
- render() {
- const {
- datasourceInstance,
- datasourceMissing,
- exploreId,
- graphResult,
- queryResponse,
- isLive,
- theme,
- showMetrics,
- showTable,
- showLogs,
- showTrace,
- showNodeGraph,
- timeZone,
- } = this.props;
- const { openDrawer } = this.state;
- const styles = getStyles(theme);
- const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
- const showRichHistory = openDrawer === ExploreDrawer.RichHistory;
- const richHistoryRowButtonHidden = !supportedFeatures().queryHistoryAvailable;
- const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector;
- const showNoData =
- queryResponse.state === LoadingState.Done &&
- [
- queryResponse.logsFrames,
- queryResponse.graphFrames,
- queryResponse.nodeGraphFrames,
- queryResponse.tableFrames,
- queryResponse.traceFrames,
- ].every((e) => e.length === 0);
- return (
- <CustomScrollbar
- testId={selectors.pages.Explore.General.scrollView}
- autoHeightMin={'100%'}
- scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)}
- >
- <ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} topOfViewRef={this.topOfViewRef} />
- {datasourceMissing ? this.renderEmptyState(styles.exploreContainer) : null}
- {datasourceInstance && (
- <div className={cx(styles.exploreContainer)}>
- <PanelContainer className={styles.queryContainer}>
- <QueryRows exploreId={exploreId} />
- <SecondaryActions
- addQueryRowButtonDisabled={isLive}
- // We cannot show multiple traces at the same time right now so we do not show add query button.
- //TODO:unification
- addQueryRowButtonHidden={false}
- richHistoryRowButtonHidden={richHistoryRowButtonHidden}
- richHistoryButtonActive={showRichHistory}
- queryInspectorButtonActive={showQueryInspector}
- onClickAddQueryRowButton={this.onClickAddQueryRowButton}
- onClickRichHistoryButton={this.toggleShowRichHistory}
- onClickQueryInspectorButton={this.toggleShowQueryInspector}
- />
- <ResponseErrorContainer exploreId={exploreId} />
- </PanelContainer>
- <AutoSizer onResize={this.onResize} disableHeight>
- {({ width }) => {
- if (width === 0) {
- return null;
- }
- return (
- <main className={cx(styles.exploreMain)} style={{ width }}>
- <ErrorBoundaryAlert>
- {showPanels && (
- <>
- {showMetrics && graphResult && (
- <ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
- )}
- {<ErrorBoundaryAlert>{this.renderLogsVolume(width)}</ErrorBoundaryAlert>}
- {showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
- {showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
- {showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
- {showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>}
- {showNoData && <ErrorBoundaryAlert>{this.renderNoData()}</ErrorBoundaryAlert>}
- </>
- )}
- {showRichHistory && (
- <RichHistoryContainer
- width={width}
- exploreId={exploreId}
- onClose={this.toggleShowRichHistory}
- />
- )}
- {showQueryInspector && (
- <ExploreQueryInspector
- exploreId={exploreId}
- width={width}
- onClose={this.toggleShowQueryInspector}
- timeZone={timeZone}
- />
- )}
- </ErrorBoundaryAlert>
- </main>
- );
- }}
- </AutoSizer>
- </div>
- )}
- </CustomScrollbar>
- );
- }
- }
- function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
- const explore = state.explore;
- const { syncedTimes } = explore;
- const item: ExploreItemState = explore[exploreId]!;
- const timeZone = getTimeZone(state.user);
- const {
- datasourceInstance,
- datasourceMissing,
- queryKeys,
- isLive,
- graphResult,
- logsVolumeData,
- logsResult,
- showLogs,
- showMetrics,
- showTable,
- showTrace,
- absoluteRange,
- queryResponse,
- showNodeGraph,
- loading,
- graphStyle,
- } = item;
- return {
- datasourceInstance,
- datasourceMissing,
- queryKeys,
- isLive,
- graphResult,
- logsVolumeData,
- logsResult: logsResult ?? undefined,
- absoluteRange,
- queryResponse,
- syncedTimes,
- timeZone,
- showLogs,
- showMetrics,
- showTable,
- showTrace,
- showNodeGraph,
- loading,
- graphStyle,
- };
- }
- const mapDispatchToProps = {
- changeSize,
- changeGraphStyle,
- modifyQueries,
- scanStart,
- scanStopAction,
- setQueries,
- updateTimeRange,
- makeAbsoluteTime,
- loadLogsVolumeData,
- addQueryRow,
- splitOpen,
- };
- const connector = connect(mapStateToProps, mapDispatchToProps);
- export default compose(connector, withTheme2)(Explore) as React.ComponentType<{ exploreId: ExploreId }>;
|