import { PureComponent } from 'react'; import * as React from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { AbsoluteTimeRange, Field, hasLogsContextSupport, hasLogsContextUiSupport, LoadingState, LogRowModel, RawTimeRange, EventBus, SplitOpen, DataFrame, SupplementaryQueryType, DataQueryResponse, LogRowContextOptions, DataSourceWithLogsContextSupport, DataSourceApi, hasToggleableQueryFiltersSupport, DataSourceWithQueryModificationSupport, hasQueryModificationSupport, } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Collapse } from '@grafana/ui'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { StoreState } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; import { getTimeZone } from '../../profile/state/selectors'; import { addResultsToCache, clearCache, loadSupplementaryQueryData, selectIsWaitingForData, setSupplementaryQueryEnabled, } from '../state/query'; import { updateTimeRange, loadMoreLogs } from '../state/time'; import { LiveTailControls } from '../useLiveTailControls'; import { getFieldLinksForExplore } from '../utils/links'; import { LiveLogsWithTheme } from './LiveLogs'; import { Logs } from './Logs'; import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition'; interface LogsContainerProps extends PropsFromRedux { width: number; exploreId: string; scanRange?: RawTimeRange; syncedTimes: boolean; loadingState: LoadingState; onClickFilterLabel: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel: (key: string, value: string, frame?: DataFrame) => void; onStartScanning: () => void; onStopScanning: () => void; eventBus: EventBus; splitOpenFn: SplitOpen; scrollElement?: HTMLDivElement; isFilterLabelActive: (key: string, value: string, refId?: string) => Promise; onClickFilterString: (value: string, refId?: string) => void; onClickFilterOutString: (value: string, refId?: string) => void; onPinLineCallback?: () => void; } type DataSourceInstance = | DataSourceApi | (DataSourceApi & DataSourceWithLogsContextSupport) | (DataSourceApi & DataSourceWithQueryModificationSupport); interface LogsContainerState { dsInstances: Record; } class LogsContainer extends PureComponent { state: LogsContainerState = { dsInstances: {}, }; componentDidMount() { this.updateDataSourceInstances(); } componentDidUpdate(prevProps: LogsContainerProps) { if (prevProps.logsQueries !== this.props.logsQueries) { this.updateDataSourceInstances(); } } private updateDataSourceInstances() { const { logsQueries, datasourceInstance } = this.props; if (!logsQueries || !datasourceInstance) { return; } const dsInstances: Record = {}; // Not in mixed mode. if (datasourceInstance.uid !== MIXED_DATASOURCE_NAME) { logsQueries.forEach(({ refId }) => { dsInstances[refId] = datasourceInstance; }); this.setState({ dsInstances }); return; } // Mixed mode. const dsPromises: Array> = []; for (const query of logsQueries) { if (!query.datasource) { continue; } const mustCheck = !dsInstances[query.refId] || dsInstances[query.refId].uid !== query.datasource.uid; if (mustCheck) { dsPromises.push( new Promise((resolve) => { getDataSourceSrv() .get(query.datasource) .then((ds) => { resolve({ ds, refId: query.refId }); }); }) ); } } if (!dsPromises.length) { return; } Promise.all(dsPromises).then((instances) => { instances.forEach(({ ds, refId }) => { dsInstances[refId] = ds; }); this.setState({ dsInstances }); }); } onChangeTime = (absoluteRange: AbsoluteTimeRange) => { const { exploreId, updateTimeRange } = this.props; updateTimeRange({ exploreId, absoluteRange }); }; loadMoreLogs = (absoluteRange: AbsoluteTimeRange) => { const { exploreId, loadMoreLogs } = this.props; loadMoreLogs({ exploreId, absoluteRange }); }; private getQuery( logsQueries: DataQuery[] | undefined, row: LogRowModel, datasourceInstance: DataSourceApi & DataSourceWithLogsContextSupport ) { // we need to find the query, and we need to be very sure that it's a query // from this datasource return (logsQueries ?? []).find( (q) => q.refId === row.dataFrame.refId && q.datasource != null && q.datasource.type === datasourceInstance.type ); } getLogRowContext = async ( row: LogRowModel, origRow: LogRowModel, options: LogRowContextOptions ): Promise => { const { logsQueries } = this.props; if (!origRow.dataFrame.refId || !this.state.dsInstances[origRow.dataFrame.refId]) { return Promise.resolve([]); } const ds = this.state.dsInstances[origRow.dataFrame.refId]; if (!hasLogsContextSupport(ds)) { return Promise.resolve([]); } const query = this.getQuery(logsQueries, origRow, ds); return query ? ds.getLogRowContext(row, options, query) : Promise.resolve([]); }; getLogRowContextQuery = async ( row: LogRowModel, options?: LogRowContextOptions, cacheFilters = true ): Promise => { const { logsQueries } = this.props; if (!row.dataFrame.refId || !this.state.dsInstances[row.dataFrame.refId]) { return Promise.resolve(null); } const ds = this.state.dsInstances[row.dataFrame.refId]; if (!hasLogsContextSupport(ds)) { return Promise.resolve(null); } const query = this.getQuery(logsQueries, row, ds); return query && ds.getLogRowContextQuery ? ds.getLogRowContextQuery(row, options, query, cacheFilters) : Promise.resolve(null); }; getLogRowContextUi = (row: LogRowModel, runContextQuery?: () => void): React.ReactNode => { const { logsQueries } = this.props; if (!row.dataFrame.refId || !this.state.dsInstances[row.dataFrame.refId]) { return <>; } const ds = this.state.dsInstances[row.dataFrame.refId]; if (!hasLogsContextSupport(ds)) { return <>; } const query = this.getQuery(logsQueries, row, ds); return query && hasLogsContextUiSupport(ds) && ds.getLogRowContextUi ? ( ds.getLogRowContextUi(row, runContextQuery, query) ) : ( <> ); }; showContextToggle = (row?: LogRowModel): boolean => { if (!row?.dataFrame.refId || !this.state.dsInstances[row.dataFrame.refId]) { return false; } return hasLogsContextSupport(this.state.dsInstances[row.dataFrame.refId]); }; getFieldLinks = (field: Field, rowIndex: number, dataFrame: DataFrame) => { const { splitOpenFn, range } = this.props; return getFieldLinksForExplore({ field, rowIndex, splitOpenFn, range, dataFrame }); }; logDetailsFilterAvailable = () => { return Object.values(this.state.dsInstances).some( (ds) => ds?.modifyQuery || hasQueryModificationSupport(ds) || hasToggleableQueryFiltersSupport(ds) ); }; filterValueAvailable = () => { return Object.values(this.state.dsInstances).some( (ds) => hasQueryModificationSupport(ds) && ds?.getSupportedQueryModifications().includes('ADD_STRING_FILTER') ); }; filterOutValueAvailable = () => { return Object.values(this.state.dsInstances).some( (ds) => hasQueryModificationSupport(ds) && ds?.getSupportedQueryModifications().includes('ADD_STRING_FILTER_OUT') ); }; addResultsToCache = () => { this.props.addResultsToCache(this.props.exploreId); }; clearCache = () => { this.props.clearCache(this.props.exploreId); }; loadLogsVolumeData = () => { this.props.loadSupplementaryQueryData(this.props.exploreId, SupplementaryQueryType.LogsVolume); }; onSetLogsVolumeEnabled = (enabled: boolean) => { this.props.setSupplementaryQueryEnabled(this.props.exploreId, enabled, SupplementaryQueryType.LogsVolume); }; render() { const { loading, loadingState, logRows, logsMeta, logsSeries, logsQueries, onClickFilterLabel, onClickFilterOutLabel, onStartScanning, onStopScanning, absoluteRange, timeZone, visibleRange, scanning, range, width, splitOpenFn, isLive, exploreId, logsVolume, scrollElement, onPinLineCallback, } = this.props; if (!logRows) { return null; } return ( <> {(controls) => ( )} ); } } function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) { const explore = state.explore; const item: ExploreItemState = explore.panes[exploreId]!; const { logsResult, scanning, datasourceInstance, isLive, isPaused, clearedAtIndex, range, absoluteRange, supplementaryQueries, } = item; const loading = selectIsWaitingForData(exploreId)(state); const panelState = item.panelsState; const timeZone = getTimeZone(state.user); const logsVolume = supplementaryQueries[SupplementaryQueryType.LogsVolume]; return { loading, logRows: logsResult?.rows, logsMeta: logsResult?.meta, logsSeries: logsResult?.series, logsQueries: logsResult?.queries, visibleRange: logsResult?.visibleRange, scanning, timeZone, datasourceInstance, isLive, isPaused, clearedAtIndex, range, absoluteRange, logsVolume, panelState, logsFrames: item.queryResponse.logsFrames, }; } const mapDispatchToProps = { updateTimeRange, loadMoreLogs, addResultsToCache, clearCache, loadSupplementaryQueryData, setSupplementaryQueryEnabled, }; const connector = connect(mapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; export default connector(LogsContainer);