1import React, { useCallback, useMemo, useRef, useLayoutEffect, useState } from 'react';
2import { css } from '@emotion/css';
3import { LogRows, CustomScrollbar, LogLabels, useStyles2, usePanelContext } from '@grafana/ui';
4import {
5  PanelProps,
6  Field,
7  Labels,
8  GrafanaTheme2,
9  LogsSortOrder,
10  LogRowModel,
11  DataHoverClearEvent,
12  DataHoverEvent,
13} from '@grafana/data';
14import { Options } from './types';
15import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
16import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
17import { COMMON_LABELS } from '../../../core/logs_model';
18
19interface LogsPanelProps extends PanelProps<Options> {}
20
21export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
22  data,
23  timeZone,
24  options: {
25    showLabels,
26    showTime,
27    wrapLogMessage,
28    showCommonLabels,
29    prettifyLogMessage,
30    sortOrder,
31    dedupStrategy,
32    enableLogDetails,
33  },
34  title,
35}) => {
36  const isAscending = sortOrder === LogsSortOrder.Ascending;
37  const style = useStyles2(getStyles(title, isAscending));
38  const [scrollTop, setScrollTop] = useState(0);
39  const logsContainerRef = useRef<HTMLDivElement>(null);
40
41  const { eventBus } = usePanelContext();
42  const onLogRowHover = useCallback(
43    (row?: LogRowModel) => {
44      if (!row) {
45        eventBus.publish(new DataHoverClearEvent());
46      } else {
47        eventBus.publish(
48          new DataHoverEvent({
49            point: {
50              time: row.timeEpochMs,
51            },
52          })
53        );
54      }
55    },
56    [eventBus]
57  );
58
59  // Important to memoize stuff here, as panel rerenders a lot for example when resizing.
60  const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
61    const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs) : null;
62    const logRows = newResults?.rows || [];
63    const commonLabels = newResults?.meta?.find((m) => m.label === COMMON_LABELS);
64    const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
65    return [logRows, deduplicatedRows, commonLabels];
66  }, [data, dedupStrategy]);
67
68  useLayoutEffect(() => {
69    if (isAscending && logsContainerRef.current) {
70      setScrollTop(logsContainerRef.current.offsetHeight);
71    } else {
72      setScrollTop(0);
73    }
74  }, [isAscending, logRows]);
75
76  const getFieldLinks = useCallback(
77    (field: Field, rowIndex: number) => {
78      return getFieldLinksForExplore({ field, rowIndex, range: data.timeRange });
79    },
80    [data]
81  );
82
83  if (!data) {
84    return (
85      <div className="panel-empty">
86        <p>No data found in response</p>
87      </div>
88    );
89  }
90
91  const renderCommonLabels = () => (
92    <div className={style.labelContainer}>
93      <span className={style.label}>Common labels:</span>
94      <LogLabels labels={commonLabels ? (commonLabels.value as Labels) : { labels: '(no common labels)' }} />
95    </div>
96  );
97
98  return (
99    <CustomScrollbar autoHide scrollTop={scrollTop}>
100      <div className={style.container} ref={logsContainerRef}>
101        {showCommonLabels && !isAscending && renderCommonLabels()}
102        <LogRows
103          logRows={logRows}
104          deduplicatedRows={deduplicatedRows}
105          dedupStrategy={dedupStrategy}
106          showLabels={showLabels}
107          showTime={showTime}
108          wrapLogMessage={wrapLogMessage}
109          prettifyLogMessage={prettifyLogMessage}
110          timeZone={timeZone}
111          getFieldLinks={getFieldLinks}
112          logsSortOrder={sortOrder}
113          enableLogDetails={enableLogDetails}
114          previewLimit={isAscending ? logRows.length : undefined}
115          onLogRowHover={onLogRowHover}
116        />
117        {showCommonLabels && isAscending && renderCommonLabels()}
118      </div>
119    </CustomScrollbar>
120  );
121};
122
123const getStyles = (title: string, isAscending: boolean) => (theme: GrafanaTheme2) => ({
124  container: css`
125    margin-bottom: ${theme.spacing(1.5)};
126    //We can remove this hot-fix when we fix panel menu with no title overflowing top of all panels
127    margin-top: ${theme.spacing(!title ? 2.5 : 0)};
128  `,
129  labelContainer: css`
130    margin: ${isAscending ? theme.spacing(0.5, 0, 0.5, 0) : theme.spacing(0, 0, 0.5, 0.5)};
131    display: flex;
132    align-items: center;
133  `,
134  label: css`
135    margin-right: ${theme.spacing(0.5)};
136    font-size: ${theme.typography.bodySmall.fontSize};
137    font-weight: ${theme.typography.fontWeightMedium};
138  `,
139});
140