Submit
Path:
~
/
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
explore
/
Logs
/
File Content:
Logs.tsx
import { css, cx } from '@emotion/css'; import { capitalize } from 'lodash'; import memoizeOne from 'memoize-one'; import React, { createRef, PureComponent } from 'react'; import { AbsoluteTimeRange, CoreApp, DataFrame, DataHoverClearEvent, DataHoverEvent, DataQueryResponse, EventBus, ExploreLogsPanelState, ExplorePanelsState, Field, GrafanaTheme2, LinkModel, LoadingState, LogLevel, LogRowContextOptions, LogRowModel, LogsDedupDescription, LogsDedupStrategy, LogsMetaItem, LogsSortOrder, rangeUtil, RawTimeRange, serializeStateToUrlParam, SplitOpen, TimeRange, urlUtil, } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { Button, InlineField, InlineFieldRow, InlineSwitch, PanelChrome, RadioButtonGroup, Themeable2, withTheme2, } from '@grafana/ui'; import store from 'app/core/store'; import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll'; import { getLogLevelFromKey } from 'app/features/logs/utils'; import { dispatch, getState } from 'app/store/store'; import { ExploreItemState } from '../../../types'; import { LogRows } from '../../logs/components/LogRows'; import { LogRowContextModal } from '../../logs/components/log-context/LogRowContextModal'; import { dedupLogRows, filterLogLevels } from '../../logs/logsModel'; import { getUrlStateFromPaneState } from '../hooks/useStateSync'; import { changePanelState } from '../state/explorePane'; import { LogsFeedback } from './LogsFeedback'; import { LogsMetaRow } from './LogsMetaRow'; import LogsNavigation from './LogsNavigation'; import { getLogsTableHeight, LogsTableWrap } from './LogsTableWrap'; import { LogsVolumePanelList } from './LogsVolumePanelList'; import { SETTINGS_KEYS, visualisationTypeKey } from './utils/logs'; interface Props extends Themeable2 { width: number; splitOpen: SplitOpen; logRows: LogRowModel[]; logsMeta?: LogsMetaItem[]; logsSeries?: DataFrame[]; logsQueries?: DataQuery[]; visibleRange?: AbsoluteTimeRange; theme: GrafanaTheme2; loading: boolean; loadingState: LoadingState; absoluteRange: AbsoluteTimeRange; timeZone: TimeZone; scanning?: boolean; scanRange?: RawTimeRange; exploreId: string; datasourceType?: string; logsVolumeEnabled: boolean; logsVolumeData: DataQueryResponse | undefined; onSetLogsVolumeEnabled: (enabled: boolean) => void; loadLogsVolumeData: () => void; showContextToggle?: (row: LogRowModel) => boolean; onChangeTime: (range: AbsoluteTimeRange) => void; onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void; onStartScanning?: () => void; onStopScanning?: () => void; getRowContext?: (row: LogRowModel, origRow: LogRowModel, options: LogRowContextOptions) => Promise<any>; getRowContextQuery?: ( row: LogRowModel, options?: LogRowContextOptions, cacheFilters?: boolean ) => Promise<DataQuery | null>; getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode; getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>; addResultsToCache: () => void; clearCache: () => void; eventBus: EventBus; panelState?: ExplorePanelsState; scrollElement?: HTMLDivElement; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>; logsFrames?: DataFrame[]; range: TimeRange; onClickFilterValue?: (value: string, refId?: string) => void; onClickFilterOutValue?: (value: string, refId?: string) => void; loadMoreLogs?(range: AbsoluteTimeRange): void; } export type LogsVisualisationType = 'table' | 'logs'; interface State { showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; prettifyLogMessage: boolean; dedupStrategy: LogsDedupStrategy; hiddenLogLevels: LogLevel[]; logsSortOrder: LogsSortOrder; isFlipping: boolean; displayedFields: string[]; forceEscape: boolean; contextOpen: boolean; contextRow?: LogRowModel; tableFrame?: DataFrame; visualisationType?: LogsVisualisationType; logsContainer?: HTMLDivElement; } // we need to define the order of these explicitly const DEDUP_OPTIONS = [ LogsDedupStrategy.none, LogsDedupStrategy.exact, LogsDedupStrategy.numbers, LogsDedupStrategy.signature, ]; const getDefaultVisualisationType = (): LogsVisualisationType => { const visualisationType = store.get(visualisationTypeKey); if (visualisationType === 'table') { return 'table'; } return 'logs'; }; class UnthemedLogs extends PureComponent<Props, State> { flipOrderTimer?: number; cancelFlippingTimer?: number; topLogsRef = createRef<HTMLDivElement>(); logsVolumeEventBus: EventBus; state: State = { showLabels: store.getBool(SETTINGS_KEYS.showLabels, false), showTime: store.getBool(SETTINGS_KEYS.showTime, true), wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true), prettifyLogMessage: store.getBool(SETTINGS_KEYS.prettifyLogMessage, false), dedupStrategy: LogsDedupStrategy.none, hiddenLogLevels: [], logsSortOrder: store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending, isFlipping: false, displayedFields: [], forceEscape: false, contextOpen: false, contextRow: undefined, tableFrame: undefined, visualisationType: this.props.panelState?.logs?.visualisationType ?? getDefaultVisualisationType(), logsContainer: undefined, }; constructor(props: Props) { super(props); this.logsVolumeEventBus = props.eventBus.newScopedBus('logsvolume', { onlyLocal: false }); } componentWillUnmount() { if (this.flipOrderTimer) { window.clearTimeout(this.flipOrderTimer); } if (this.cancelFlippingTimer) { window.clearTimeout(this.cancelFlippingTimer); } // If we're unmounting logs (e.g. switching to another datasource), we need to remove the table specific panel state, otherwise it will persist in the explore url if ( this.props?.panelState?.logs?.columns || this.props?.panelState?.logs?.refId || this.props?.panelState?.logs?.labelFieldName ) { dispatch( changePanelState(this.props.exploreId, 'logs', { ...this.props.panelState?.logs, columns: undefined, visualisationType: this.state.visualisationType, labelFieldName: undefined, refId: undefined, }) ); } } updatePanelState = (logsPanelState: Partial<ExploreLogsPanelState>) => { const state: ExploreItemState | undefined = getState().explore.panes[this.props.exploreId]; if (state?.panelsState) { dispatch( changePanelState(this.props.exploreId, 'logs', { ...state.panelsState.logs, columns: logsPanelState.columns ?? this.props.panelState?.logs?.columns, visualisationType: logsPanelState.visualisationType ?? this.state.visualisationType, labelFieldName: logsPanelState.labelFieldName, refId: logsPanelState.refId ?? this.props.panelState?.logs?.refId, }) ); } }; componentDidUpdate(prevProps: Readonly<Props>): void { if (this.props.loading && !prevProps.loading && this.props.panelState?.logs?.id) { // loading stopped, so we need to remove any permalinked log lines delete this.props.panelState.logs.id; dispatch( changePanelState(this.props.exploreId, 'logs', { ...this.props.panelState, }) ); } if (this.props.panelState?.logs?.visualisationType !== prevProps.panelState?.logs?.visualisationType) { const visualisationType = this.props.panelState?.logs?.visualisationType ?? getDefaultVisualisationType(); this.setState({ visualisationType: visualisationType, }); store.set(visualisationTypeKey, visualisationType); } } onLogRowHover = (row?: LogRowModel) => { if (!row) { this.props.eventBus.publish(new DataHoverClearEvent()); } else { this.props.eventBus.publish( new DataHoverEvent({ point: { time: row.timeEpochMs, }, }) ); } }; onLogsContainerRef = (node: HTMLDivElement) => { this.setState({ logsContainer: node }); }; onChangeLogsSortOrder = () => { this.setState({ isFlipping: true }); // we are using setTimeout here to make sure that disabled button is rendered before the rendering of reordered logs this.flipOrderTimer = window.setTimeout(() => { this.setState((prevState) => { const newSortOrder = prevState.logsSortOrder === LogsSortOrder.Descending ? LogsSortOrder.Ascending : LogsSortOrder.Descending; store.set(SETTINGS_KEYS.logsSortOrder, newSortOrder); return { logsSortOrder: newSortOrder }; }); }, 0); this.cancelFlippingTimer = window.setTimeout(() => this.setState({ isFlipping: false }), 1000); }; onEscapeNewlines = () => { this.setState((prevState) => ({ forceEscape: !prevState.forceEscape, })); }; onChangeVisualisation = (visualisation: LogsVisualisationType) => { this.setState(() => ({ visualisationType: visualisation, })); const payload = { ...this.props.panelState?.logs, visualisationType: visualisation, }; this.updatePanelState(payload); reportInteraction('grafana_explore_logs_visualisation_changed', { newVisualizationType: visualisation, datasourceType: this.props.datasourceType ?? 'unknown', }); }; onChangeDedup = (dedupStrategy: LogsDedupStrategy) => { reportInteraction('grafana_explore_logs_deduplication_clicked', { deduplicationType: dedupStrategy, datasourceType: this.props.datasourceType, }); this.setState({ dedupStrategy }); }; onChangeLabels = (event: React.ChangeEvent<HTMLInputElement>) => { const { target } = event; if (target) { const showLabels = target.checked; this.setState({ showLabels, }); store.set(SETTINGS_KEYS.showLabels, showLabels); } }; onChangeTime = (event: React.ChangeEvent<HTMLInputElement>) => { const { target } = event; if (target) { const showTime = target.checked; this.setState({ showTime, }); store.set(SETTINGS_KEYS.showTime, showTime); } }; onChangeWrapLogMessage = (event: React.ChangeEvent<HTMLInputElement>) => { const { target } = event; if (target) { const wrapLogMessage = target.checked; this.setState({ wrapLogMessage, }); store.set(SETTINGS_KEYS.wrapLogMessage, wrapLogMessage); } }; onChangePrettifyLogMessage = (event: React.ChangeEvent<HTMLInputElement>) => { const { target } = event; if (target) { const prettifyLogMessage = target.checked; this.setState({ prettifyLogMessage, }); store.set(SETTINGS_KEYS.prettifyLogMessage, prettifyLogMessage); } }; onToggleLogLevel = (hiddenRawLevels: string[]) => { const hiddenLogLevels = hiddenRawLevels.map((level) => getLogLevelFromKey(level)); this.setState({ hiddenLogLevels }); }; onToggleLogsVolumeCollapse = (collapsed: boolean) => { this.props.onSetLogsVolumeEnabled(!collapsed); reportInteraction('grafana_explore_logs_histogram_toggle_clicked', { datasourceType: this.props.datasourceType, type: !collapsed ? 'open' : 'close', }); }; onClickScan = (event: React.SyntheticEvent) => { event.preventDefault(); if (this.props.onStartScanning) { this.props.onStartScanning(); reportInteraction('grafana_explore_logs_scanning_button_clicked', { type: 'start', datasourceType: this.props.datasourceType, }); } }; onClickStopScan = (event: React.SyntheticEvent) => { event.preventDefault(); if (this.props.onStopScanning) { this.props.onStopScanning(); } }; showField = (key: string) => { const index = this.state.displayedFields.indexOf(key); if (index === -1) { this.setState((state) => { return { displayedFields: state.displayedFields.concat(key), }; }); } }; hideField = (key: string) => { const index = this.state.displayedFields.indexOf(key); if (index > -1) { this.setState((state) => { return { displayedFields: state.displayedFields.filter((k) => key !== k), }; }); } }; clearDetectedFields = () => { this.setState((state) => { return { displayedFields: [], }; }); }; onCloseContext = () => { this.setState({ contextOpen: false, contextRow: undefined, }); }; onOpenContext = (row: LogRowModel, onClose: () => void) => { // we are setting the `contextOpen` open state and passing it down to the `LogRow` in order to highlight the row when a LogContext is open this.setState({ contextOpen: true, contextRow: row, }); reportInteraction('grafana_explore_logs_log_context_opened', { datasourceType: row.datasourceType, logRowUid: row.uid, }); this.onCloseContext = () => { this.setState({ contextOpen: false, contextRow: undefined, }); reportInteraction('grafana_explore_logs_log_context_closed', { datasourceType: row.datasourceType, logRowUid: row.uid, }); onClose(); }; }; getPermalinkRange(row: LogRowModel) { const range = { from: new Date(this.props.absoluteRange.from).toISOString(), to: new Date(this.props.absoluteRange.to).toISOString(), }; if (!config.featureToggles.logsInfiniteScrolling) { return range; } // With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust // Look for the previous sibling log, and use its timestamp const allLogs = this.props.logRows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId); const prevLog = allLogs[allLogs.indexOf(row) - 1]; if (row.timeEpochMs > this.props.absoluteRange.to && !prevLog) { // Because there's no sibling and the current `to` is oldest than the log, we have no reference we can use for the interval // This only happens when you scroll into the future and you want to share the first log of the list return { from: new Date(this.props.absoluteRange.from).toISOString(), // Slide 1ms otherwise it's very likely to be omitted in the results to: new Date(row.timeEpochMs + 1).toISOString(), }; } return { from: new Date(this.props.absoluteRange.from).toISOString(), to: new Date(prevLog ? prevLog.timeEpochMs : this.props.absoluteRange.to).toISOString(), }; } onPermalinkClick = async (row: LogRowModel) => { // this is an extra check, to be sure that we are not // creating permalinks for logs without an id-field. // normally it should never happen, because we do not // display the permalink button in such cases. if (row.rowId === undefined) { return; } // get explore state, add log-row-id and make timerange absolute const urlState = getUrlStateFromPaneState(getState().explore.panes[this.props.exploreId]!); urlState.panelsState = { ...this.props.panelState, logs: { id: row.uid, visualisationType: this.state.visualisationType ?? getDefaultVisualisationType() }, }; urlState.range = this.getPermalinkRange(row); // append changed urlState to baseUrl const serializedState = serializeStateToUrlParam(urlState); const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)![0]; const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState }); await createAndCopyShortLink(url); reportInteraction('grafana_explore_logs_permalink_clicked', { datasourceType: row.datasourceType ?? 'unknown', logRowUid: row.uid, logRowLevel: row.logLevel, }); }; scrollIntoView = (element: HTMLElement) => { if (config.featureToggles.logsInfiniteScrolling) { if (this.state.logsContainer) { this.topLogsRef.current?.scrollIntoView(); this.state.logsContainer.scroll({ behavior: 'smooth', top: this.state.logsContainer.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, }); } return; } const { scrollElement } = this.props; if (scrollElement) { scrollElement.scroll({ behavior: 'smooth', top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, }); } }; checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => { return !!logRows.some((r) => r.hasUnescapedContent); }); dedupRows = memoizeOne((logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => { const dedupedRows = dedupLogRows(logRows, dedupStrategy); const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0); return { dedupedRows, dedupCount }; }); filterRows = memoizeOne((logRows: LogRowModel[], hiddenLogLevels: LogLevel[]) => { return filterLogLevels(logRows, new Set(hiddenLogLevels)); }); createNavigationRange = memoizeOne((logRows: LogRowModel[]): { from: number; to: number } | undefined => { if (!logRows || logRows.length === 0) { return undefined; } const firstTimeStamp = logRows[0].timeEpochMs; const lastTimeStamp = logRows[logRows.length - 1].timeEpochMs; if (lastTimeStamp < firstTimeStamp) { return { from: lastTimeStamp, to: firstTimeStamp }; } return { from: firstTimeStamp, to: lastTimeStamp }; }); scrollToTopLogs = () => { if (config.featureToggles.logsInfiniteScrolling) { if (this.state.logsContainer) { this.state.logsContainer.scroll({ behavior: 'auto', top: 0, }); } } this.topLogsRef.current?.scrollIntoView(); }; render() { const { width, splitOpen, logRows, logsMeta, logsVolumeEnabled, logsVolumeData, loadLogsVolumeData, loading = false, onClickFilterLabel, onClickFilterOutLabel, timeZone, scanning, scanRange, showContextToggle, absoluteRange, onChangeTime, getFieldLinks, theme, logsQueries, clearCache, addResultsToCache, exploreId, getRowContext, getLogRowContextUi, getRowContextQuery, loadMoreLogs, } = this.props; const { showLabels, showTime, wrapLogMessage, prettifyLogMessage, dedupStrategy, hiddenLogLevels, logsSortOrder, isFlipping, displayedFields, forceEscape, contextOpen, contextRow, } = this.state; const tableHeight = getLogsTableHeight(); const styles = getStyles(theme, wrapLogMessage, tableHeight); const hasData = logRows && logRows.length > 0; const hasUnescapedContent = this.checkUnescapedContent(logRows); const filteredLogs = this.filterRows(logRows, hiddenLogLevels); const { dedupedRows, dedupCount } = this.dedupRows(filteredLogs, dedupStrategy); const navigationRange = this.createNavigationRange(logRows); const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; return ( <> {getRowContext && contextRow && ( <LogRowContextModal open={contextOpen} row={contextRow} onClose={this.onCloseContext} getRowContext={(row, options) => getRowContext(row, contextRow, options)} getRowContextQuery={getRowContextQuery} getLogRowContextUi={getLogRowContextUi} logsSortOrder={logsSortOrder} timeZone={timeZone} /> )} <PanelChrome title="Logs volume" collapsible collapsed={!logsVolumeEnabled} onToggleCollapse={this.onToggleLogsVolumeCollapse} > {logsVolumeEnabled && ( <LogsVolumePanelList absoluteRange={absoluteRange} width={width} logsVolumeData={logsVolumeData} onUpdateTimeRange={onChangeTime} timeZone={timeZone} splitOpen={splitOpen} onLoadLogsVolume={loadLogsVolumeData} onHiddenSeriesChanged={this.onToggleLogLevel} eventBus={this.logsVolumeEventBus} onClose={() => this.onToggleLogsVolumeCollapse(true)} /> )} </PanelChrome> <PanelChrome titleItems={[ config.featureToggles.logsExploreTableVisualisation ? ( this.state.visualisationType === 'logs' ? null : ( <PanelChrome.TitleItem title="Feedback" key="A"> <LogsFeedback feedbackUrl="https://forms.gle/5YyKdRQJ5hzq4c289" /> </PanelChrome.TitleItem> ) ) : null, ]} title={'Logs'} actions={ <> {config.featureToggles.logsExploreTableVisualisation && ( <div className={styles.visualisationType}> <RadioButtonGroup className={styles.visualisationTypeRadio} options={[ { label: 'Logs', value: 'logs', description: 'Show results in logs visualisation', }, { label: 'Table', value: 'table', description: 'Show results in table visualisation', }, ]} size="sm" value={this.state.visualisationType} onChange={this.onChangeVisualisation} /> </div> )} </> } loadingState={loading ? LoadingState.Loading : LoadingState.Done} > <div className={styles.stickyNavigation}> {this.state.visualisationType !== 'table' && ( <div className={styles.logOptions}> <InlineFieldRow> <InlineField label="Time" className={styles.horizontalInlineLabel} transparent> <InlineSwitch value={showTime} onChange={this.onChangeTime} className={styles.horizontalInlineSwitch} transparent id={`show-time_${exploreId}`} /> </InlineField> <InlineField label="Unique labels" className={styles.horizontalInlineLabel} transparent> <InlineSwitch value={showLabels} onChange={this.onChangeLabels} className={styles.horizontalInlineSwitch} transparent id={`unique-labels_${exploreId}`} /> </InlineField> <InlineField label="Wrap lines" className={styles.horizontalInlineLabel} transparent> <InlineSwitch value={wrapLogMessage} onChange={this.onChangeWrapLogMessage} className={styles.horizontalInlineSwitch} transparent id={`wrap-lines_${exploreId}`} /> </InlineField> <InlineField label="Prettify JSON" className={styles.horizontalInlineLabel} transparent> <InlineSwitch value={prettifyLogMessage} onChange={this.onChangePrettifyLogMessage} className={styles.horizontalInlineSwitch} transparent id={`prettify_${exploreId}`} /> </InlineField> <InlineField label="Deduplication" className={styles.horizontalInlineLabel} transparent> <RadioButtonGroup options={DEDUP_OPTIONS.map((dedupType) => ({ label: capitalize(dedupType), value: dedupType, description: LogsDedupDescription[dedupType], }))} value={dedupStrategy} onChange={this.onChangeDedup} className={styles.radioButtons} /> </InlineField> </InlineFieldRow> <div> <InlineField label="Display results" className={styles.horizontalInlineLabel} transparent disabled={isFlipping || loading} > <RadioButtonGroup options={[ { label: 'Newest first', value: LogsSortOrder.Descending, description: 'Show results newest to oldest', }, { label: 'Oldest first', value: LogsSortOrder.Ascending, description: 'Show results oldest to newest', }, ]} value={logsSortOrder} onChange={this.onChangeLogsSortOrder} className={styles.radioButtons} /> </InlineField> </div> </div> )} <div ref={this.topLogsRef} /> <LogsMetaRow logRows={logRows} meta={logsMeta || []} dedupStrategy={dedupStrategy} dedupCount={dedupCount} hasUnescapedContent={hasUnescapedContent} forceEscape={forceEscape} displayedFields={displayedFields} onEscapeNewlines={this.onEscapeNewlines} clearDetectedFields={this.clearDetectedFields} /> </div> <div className={cx(styles.logsSection, this.state.visualisationType === 'table' ? styles.logsTable : undefined)} > {this.state.visualisationType === 'table' && hasData && ( <div className={styles.logRows} data-testid="logRowsTable"> {/* Width should be full width minus logs navigation and padding */} <LogsTableWrap logsSortOrder={this.state.logsSortOrder} range={this.props.range} splitOpen={this.props.splitOpen} timeZone={timeZone} width={width - 80} logsFrames={this.props.logsFrames ?? []} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} panelState={this.props.panelState?.logs} theme={theme} updatePanelState={this.updatePanelState} datasourceType={this.props.datasourceType} /> </div> )} {this.state.visualisationType === 'logs' && hasData && ( <div className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows} data-testid="logRows" ref={this.onLogsContainerRef} > <InfiniteScroll loading={loading} loadMoreLogs={loadMoreLogs} range={this.props.range} timeZone={timeZone} rows={logRows} scrollElement={this.state.logsContainer} sortOrder={logsSortOrder} > <LogRows logRows={logRows} deduplicatedRows={dedupedRows} dedupStrategy={dedupStrategy} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} showContextToggle={showContextToggle} getRowContextQuery={getRowContextQuery} showLabels={showLabels} showTime={showTime} enableLogDetails={true} forceEscape={forceEscape} wrapLogMessage={wrapLogMessage} prettifyLogMessage={prettifyLogMessage} timeZone={timeZone} getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} displayedFields={displayedFields} onClickShowField={this.showField} onClickHideField={this.hideField} app={CoreApp.Explore} onLogRowHover={this.onLogRowHover} onOpenContext={this.onOpenContext} onPermalinkClick={this.onPermalinkClick} permalinkedRowId={this.props.panelState?.logs?.id} scrollIntoView={this.scrollIntoView} isFilterLabelActive={this.props.isFilterLabelActive} containerRendered={!!this.state.logsContainer} onClickFilterValue={this.props.onClickFilterValue} onClickFilterOutValue={this.props.onClickFilterOutValue} /> </InfiniteScroll> </div> )} {!loading && !hasData && !scanning && ( <div className={styles.logRows}> <div className={styles.noData}> No logs found. <Button size="sm" variant="secondary" onClick={this.onClickScan}> Scan for older logs </Button> </div> </div> )} {scanning && ( <div className={styles.logRows}> <div className={styles.noData}> <span>{scanText}</span> <Button size="sm" variant="secondary" onClick={this.onClickStopScan}> Stop scan </Button> </div> </div> )} <LogsNavigation logsSortOrder={logsSortOrder} visibleRange={navigationRange ?? absoluteRange} absoluteRange={absoluteRange} timeZone={timeZone} onChangeTime={onChangeTime} loading={loading} queries={logsQueries ?? []} scrollToTopLogs={this.scrollToTopLogs} addResultsToCache={addResultsToCache} clearCache={clearCache} /> </div> </PanelChrome> </> ); } } export const Logs = withTheme2(UnthemedLogs); const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: number) => { return { noData: css({ '& > *': { marginLeft: '0.5em', }, }), logOptions: css({ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', backgroundColor: theme.colors.background.primary, padding: `${theme.spacing(1)} ${theme.spacing(2)}`, borderRadius: theme.shape.radius.default, margin: `${theme.spacing(0, 0, 1)}`, border: `1px solid ${theme.colors.border.medium}`, }), headerButton: css({ margin: `${theme.spacing(0.5, 0, 0, 1)}`, }), horizontalInlineLabel: css({ '& > label': { marginRight: '0', }, }), horizontalInlineSwitch: css({ padding: `0 ${theme.spacing(1)} 0 0`, }), radioButtons: css({ margin: '0', }), logsSection: css({ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', }), logsTable: css({ maxHeight: `${tableHeight}px`, }), scrollableLogRows: css({ overflowY: 'scroll', width: '100%', maxHeight: '75vh', }), logRows: css({ overflowX: `${wrapLogMessage ? 'unset' : 'scroll'}`, overflowY: 'visible', width: '100%', }), visualisationType: css({ display: 'flex', flex: '1', justifyContent: 'space-between', }), visualisationTypeRadio: css({ margin: `0 0 0 ${theme.spacing(1)}`, }), stickyNavigation: css({ overflow: 'visible', ...(config.featureToggles.logsInfiniteScrolling && { marginBottom: '0px' }), }), }; };
Edit
Rename
Chmod
Delete
FILE
FOLDER
INFO
Name
Size
Permission
Action
utils
---
0755
LiveLogs.test.tsx
3336 bytes
0644
LiveLogs.tsx
6480 bytes
0644
Logs.test.tsx
15642 bytes
0644
Logs.tsx
33158 bytes
0644
LogsColumnSearch.tsx
723 bytes
0644
LogsContainer.tsx
12999 bytes
0644
LogsFeedback.tsx
880 bytes
0644
LogsMetaRow.test.tsx
8416 bytes
0644
LogsMetaRow.tsx
6298 bytes
0644
LogsNavigation.test.tsx
6481 bytes
0644
LogsNavigation.tsx
8205 bytes
0644
LogsNavigationPages.test.tsx
1946 bytes
0644
LogsNavigationPages.tsx
3616 bytes
0644
LogsSamplePanel.test.tsx
6694 bytes
0644
LogsSamplePanel.tsx
4578 bytes
0644
LogsTable.test.tsx
9592 bytes
0644
LogsTable.tsx
8078 bytes
0644
LogsTableActiveFields.tsx
3548 bytes
0644
LogsTableAvailableFields.tsx
1986 bytes
0644
LogsTableEmptyFields.tsx
523 bytes
0644
LogsTableMultiSelect.tsx
2624 bytes
0644
LogsTableNavField.tsx
2202 bytes
0644
LogsTableWrap.test.tsx
5832 bytes
0644
LogsTableWrap.tsx
19976 bytes
0644
LogsVolumePanel.test.tsx
1526 bytes
0644
LogsVolumePanel.tsx
3437 bytes
0644
LogsVolumePanelList.test.tsx
2866 bytes
0644
LogsVolumePanelList.tsx
5766 bytes
0644
PopoverMenu.test.tsx
2774 bytes
0644
PopoverMenu.tsx
2640 bytes
0644
N4ST4R_ID | Naxtarrr