From 5a4465a3824efc16e8698df2651a85a99c3826ee Mon Sep 17 00:00:00 2001
From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Date: Tue, 3 Dec 2019 13:02:44 +0100
Subject: [PATCH] Explore: Log message line wrapping options for logs (#20360)

---
 .../grafana-ui/src/components/Logs/LogRow.tsx |  4 +
 .../src/components/Logs/LogRowMessage.tsx     |  8 +-
 .../src/components/Logs/LogRows.test.tsx      |  4 +
 .../src/components/Logs/LogRows.tsx           | 79 ++++++++++---------
 .../src/components/Logs/getLogRowStyles.ts    |  5 ++
 public/app/features/explore/Logs.tsx          | 15 +++-
 public/app/plugins/panel/logs/LogsPanel.tsx   |  3 +-
 .../plugins/panel/logs/LogsPanelEditor.tsx    | 15 +++-
 public/app/plugins/panel/logs/types.ts        |  2 +
 9 files changed, 95 insertions(+), 40 deletions(-)

diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx
index 1451f9a86e6..771ff78534a 100644
--- a/packages/grafana-ui/src/components/Logs/LogRow.tsx
+++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx
@@ -22,6 +22,7 @@ interface Props extends Themeable {
   row: LogRowModel;
   showDuplicates: boolean;
   showTime: boolean;
+  wrapLogMessage: boolean;
   timeZone: TimeZone;
   allowDetails?: boolean;
   getRows: () => LogRowModel[];
@@ -93,6 +94,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
       showDuplicates,
       timeZone,
       showTime,
+      wrapLogMessage,
       theme,
       getFieldLinks,
     } = this.props;
@@ -103,6 +105,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
     const showDetailsClassName = showDetails
       ? cx(['fa fa-chevron-down', styles.topVerticalAlign])
       : cx(['fa fa-chevron-right', styles.topVerticalAlign]);
+
     return (
       <div className={style.logsRow}>
         {showDuplicates && (
@@ -141,6 +144,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
               updateLimit={updateLimit}
               context={context}
               showContext={showContext}
+              wrapLogMessage={wrapLogMessage}
               onToggleContext={this.toggleContext}
             />
           </div>
diff --git a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx
index 47402172fb5..bda60e6a0b9 100644
--- a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx
+++ b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx
@@ -21,6 +21,7 @@ interface Props extends Themeable {
   row: LogRowModel;
   hasMoreContextRows?: HasMoreContextRows;
   showContext: boolean;
+  wrapLogMessage: boolean;
   errors?: LogRowContextQueryErrors;
   context?: LogRowContextRows;
   highlighterExpressions?: string[];
@@ -57,6 +58,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
       label: whiteSpacePreWrap;
       white-space: pre-wrap;
     `,
+    horizontalScroll: css`
+      label: verticalScroll;
+      white-space: nowrap;
+    `,
   };
 });
 
@@ -76,6 +81,7 @@ class UnThemedLogRowMessage extends PureComponent<Props, State> {
       updateLimit,
       context,
       showContext,
+      wrapLogMessage,
       onToggleContext,
     } = this.props;
     const {} = this.state;
@@ -91,7 +97,7 @@ class UnThemedLogRowMessage extends PureComponent<Props, State> {
     const styles = getStyles(theme);
     return (
       <div className={style.logsRowMessage}>
-        <div className={styles.positionRelative}>
+        <div className={cx(styles.positionRelative, { [styles.horizontalScroll]: !wrapLogMessage })}>
           {showContext && context && (
             <LogRowContext
               row={row}
diff --git a/packages/grafana-ui/src/components/Logs/LogRows.test.tsx b/packages/grafana-ui/src/components/Logs/LogRows.test.tsx
index e88eb2b78d0..fe7dd4c515a 100644
--- a/packages/grafana-ui/src/components/Logs/LogRows.test.tsx
+++ b/packages/grafana-ui/src/components/Logs/LogRows.test.tsx
@@ -14,6 +14,7 @@ describe('LogRows', () => {
         dedupStrategy={LogsDedupStrategy.none}
         highlighterExpressions={[]}
         showTime={false}
+        wrapLogMessage={true}
         timeZone={'utc'}
       />
     );
@@ -33,6 +34,7 @@ describe('LogRows', () => {
         dedupStrategy={LogsDedupStrategy.none}
         highlighterExpressions={[]}
         showTime={false}
+        wrapLogMessage={true}
         timeZone={'utc'}
         previewLimit={1}
       />
@@ -61,6 +63,7 @@ describe('LogRows', () => {
         dedupStrategy={LogsDedupStrategy.none}
         highlighterExpressions={[]}
         showTime={false}
+        wrapLogMessage={true}
         timeZone={'utc'}
       />
     );
@@ -79,6 +82,7 @@ describe('LogRows', () => {
         dedupStrategy={LogsDedupStrategy.none}
         highlighterExpressions={[]}
         showTime={false}
+        wrapLogMessage={true}
         timeZone={'utc'}
       />
     );
diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx
index 323f98135f6..c7c55112049 100644
--- a/packages/grafana-ui/src/components/Logs/LogRows.tsx
+++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx
@@ -18,6 +18,7 @@ export interface Props extends Themeable {
   dedupStrategy: LogsDedupStrategy;
   highlighterExpressions?: string[];
   showTime: boolean;
+  wrapLogMessage: boolean;
   timeZone: TimeZone;
   rowLimit?: number;
   allowDetails?: boolean;
@@ -71,6 +72,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
     const {
       dedupStrategy,
       showTime,
+      wrapLogMessage,
       logRows,
       deduplicatedRows,
       highlighterExpressions,
@@ -84,12 +86,14 @@ class UnThemedLogRows extends PureComponent<Props, State> {
       getFieldLinks,
     } = this.props;
     const { renderAll } = this.state;
+    const { logsRows, logsRowsHorizontalScroll } = getLogRowStyles(theme);
     const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows;
     const hasData = logRows && logRows.length > 0;
     const dedupCount = dedupedRows
       ? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
       : 0;
     const showDuplicates = dedupStrategy !== LogsDedupStrategy.none && dedupCount > 0;
+    const horizontalScrollWindow = wrapLogMessage ? '' : logsRowsHorizontalScroll;
 
     // Staged rendering
     const processedRows = dedupedRows ? dedupedRows : [];
@@ -100,45 +104,48 @@ class UnThemedLogRows extends PureComponent<Props, State> {
     // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
     const getRows = this.makeGetRows(processedRows);
     const getRowContext = this.props.getRowContext ? this.props.getRowContext : () => Promise.resolve([]);
-    const { logsRows } = getLogRowStyles(theme);
 
     return (
       <div className={logsRows}>
-        {hasData &&
-          firstRows.map((row, index) => (
-            <LogRow
-              key={row.uid}
-              getRows={getRows}
-              getRowContext={getRowContext}
-              highlighterExpressions={highlighterExpressions}
-              row={row}
-              showDuplicates={showDuplicates}
-              showTime={showTime}
-              timeZone={timeZone}
-              allowDetails={allowDetails}
-              onClickFilterLabel={onClickFilterLabel}
-              onClickFilterOutLabel={onClickFilterOutLabel}
-              getFieldLinks={getFieldLinks}
-            />
-          ))}
-        {hasData &&
-          renderAll &&
-          lastRows.map((row, index) => (
-            <LogRow
-              key={row.uid}
-              getRows={getRows}
-              getRowContext={getRowContext}
-              row={row}
-              showDuplicates={showDuplicates}
-              showTime={showTime}
-              timeZone={timeZone}
-              allowDetails={allowDetails}
-              onClickFilterLabel={onClickFilterLabel}
-              onClickFilterOutLabel={onClickFilterOutLabel}
-              getFieldLinks={getFieldLinks}
-            />
-          ))}
-        {hasData && !renderAll && <span>Rendering {rowCount - previewLimit!} rows...</span>}
+        <div className={horizontalScrollWindow}>
+          {hasData &&
+            firstRows.map((row, index) => (
+              <LogRow
+                key={row.uid}
+                getRows={getRows}
+                getRowContext={getRowContext}
+                highlighterExpressions={highlighterExpressions}
+                row={row}
+                showDuplicates={showDuplicates}
+                showTime={showTime}
+                wrapLogMessage={wrapLogMessage}
+                timeZone={timeZone}
+                allowDetails={allowDetails}
+                onClickFilterLabel={onClickFilterLabel}
+                onClickFilterOutLabel={onClickFilterOutLabel}
+                getFieldLinks={getFieldLinks}
+              />
+            ))}
+          {hasData &&
+            renderAll &&
+            lastRows.map((row, index) => (
+              <LogRow
+                key={row.uid}
+                getRows={getRows}
+                getRowContext={getRowContext}
+                row={row}
+                showDuplicates={showDuplicates}
+                showTime={showTime}
+                wrapLogMessage={wrapLogMessage}
+                timeZone={timeZone}
+                allowDetails={allowDetails}
+                onClickFilterLabel={onClickFilterLabel}
+                onClickFilterOutLabel={onClickFilterOutLabel}
+                getFieldLinks={getFieldLinks}
+              />
+            ))}
+          {hasData && !renderAll && <span>Rendering {rowCount - previewLimit!} rows...</span>}
+        </div>
       </div>
     );
   }
diff --git a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts
index b117ed28477..2463476b622 100644
--- a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts
+++ b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts
@@ -62,6 +62,10 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
       table-layout: fixed;
       width: 100%;
     `,
+    logsRowsHorizontalScroll: css`
+      label: logs-rows__horizontal-scroll;
+      overflow-y: scroll;
+    `,
     context: context,
     logsRow: css`
       label: logs-row;
@@ -130,6 +134,7 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
       display: table-cell;
       white-space: nowrap;
       width: 12.5em;
+      padding-right: 1em;
     `,
     logsRowMessage: css`
       label: logs-row__message;
diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx
index fbd8f6d0810..764f9766e6d 100644
--- a/public/app/features/explore/Logs.tsx
+++ b/public/app/features/explore/Logs.tsx
@@ -57,11 +57,13 @@ interface Props {
 
 interface State {
   showTime: boolean;
+  wrapLogMessage: boolean;
 }
 
 export class Logs extends PureComponent<Props, State> {
   state = {
     showTime: true,
+    wrapLogMessage: true,
   };
 
   onChangeDedup = (dedup: LogsDedupStrategy) => {
@@ -81,6 +83,15 @@ export class Logs extends PureComponent<Props, State> {
     }
   };
 
+  onChangewrapLogMessage = (event?: React.SyntheticEvent) => {
+    const target = event && (event.target as HTMLInputElement);
+    if (target) {
+      this.setState({
+        wrapLogMessage: target.checked,
+      });
+    }
+  };
+
   onToggleLogLevel = (hiddenRawLevels: string[]) => {
     const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map(level => LogLevel[level as LogLevel]);
     this.props.onToggleLogLevel(hiddenLogLevels);
@@ -123,7 +134,7 @@ export class Logs extends PureComponent<Props, State> {
       return null;
     }
 
-    const { showTime } = this.state;
+    const { showTime, wrapLogMessage } = this.state;
     const { dedupStrategy } = this.props;
     const hasData = logRows && logRows.length > 0;
     const dedupCount = dedupedRows
@@ -164,6 +175,7 @@ export class Logs extends PureComponent<Props, State> {
         <div className="logs-panel-options">
           <div className="logs-panel-controls">
             <Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
+            <Switch label="Wrap lines" checked={wrapLogMessage} onChange={this.onChangewrapLogMessage} transparent />
             <ToggleButtonGroup label="Dedup" transparent={true}>
               {Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
                 <ToggleButton
@@ -202,6 +214,7 @@ export class Logs extends PureComponent<Props, State> {
           onClickFilterLabel={onClickFilterLabel}
           onClickFilterOutLabel={onClickFilterOutLabel}
           showTime={showTime}
+          wrapLogMessage={wrapLogMessage}
           timeZone={timeZone}
           getFieldLinks={getFieldLinks}
         />
diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx
index bed64edde61..d9198ac4bc3 100644
--- a/public/app/plugins/panel/logs/LogsPanel.tsx
+++ b/public/app/plugins/panel/logs/LogsPanel.tsx
@@ -10,7 +10,7 @@ interface LogsPanelProps extends PanelProps<Options> {}
 export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
   data,
   timeZone,
-  options: { showTime, sortOrder },
+  options: { showTime, wrapLogMessage, sortOrder },
   width,
 }) => {
   if (!data) {
@@ -31,6 +31,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
         dedupStrategy={LogsDedupStrategy.none}
         highlighterExpressions={[]}
         showTime={showTime}
+        wrapLogMessage={wrapLogMessage}
         timeZone={timeZone}
         allowDetails={true}
       />
diff --git a/public/app/plugins/panel/logs/LogsPanelEditor.tsx b/public/app/plugins/panel/logs/LogsPanelEditor.tsx
index 83cc3a8485e..c27632e1cd6 100644
--- a/public/app/plugins/panel/logs/LogsPanelEditor.tsx
+++ b/public/app/plugins/panel/logs/LogsPanelEditor.tsx
@@ -20,13 +20,20 @@ export class LogsPanelEditor extends PureComponent<PanelEditorProps<Options>> {
     onOptionsChange({ ...options, showTime: !showTime });
   };
 
+  onTogglewrapLogMessage = () => {
+    const { options, onOptionsChange } = this.props;
+    const { wrapLogMessage } = options;
+
+    onOptionsChange({ ...options, wrapLogMessage: !wrapLogMessage });
+  };
+
   onShowValuesChange = (item: SelectableValue<SortOrder>) => {
     const { options, onOptionsChange } = this.props;
     onOptionsChange({ ...options, sortOrder: item.value });
   };
 
   render() {
-    const { showTime, sortOrder } = this.props.options;
+    const { showTime, wrapLogMessage, sortOrder } = this.props.options;
     const value = sortOrderOptions.filter(option => option.value === sortOrder)[0];
 
     return (
@@ -34,6 +41,12 @@ export class LogsPanelEditor extends PureComponent<PanelEditorProps<Options>> {
         <PanelOptionsGrid>
           <PanelOptionsGroup title="Columns">
             <Switch label="Time" labelClass="width-10" checked={showTime} onChange={this.onToggleTime} />
+            <Switch
+              label="Wrap lines"
+              labelClass="width-10"
+              checked={wrapLogMessage}
+              onChange={this.onTogglewrapLogMessage}
+            />
             <div className="gf-form">
               <FormLabel>Order</FormLabel>
               <Select options={sortOrderOptions} value={value} onChange={this.onShowValuesChange} />
diff --git a/public/app/plugins/panel/logs/types.ts b/public/app/plugins/panel/logs/types.ts
index a8cb953bd2f..46859ddce08 100644
--- a/public/app/plugins/panel/logs/types.ts
+++ b/public/app/plugins/panel/logs/types.ts
@@ -2,10 +2,12 @@ import { SortOrder } from 'app/core/utils/explore';
 
 export interface Options {
   showTime: boolean;
+  wrapLogMessage: boolean;
   sortOrder: SortOrder;
 }
 
 export const defaults: Options = {
   showTime: true,
+  wrapLogMessage: true,
   sortOrder: SortOrder.Descending,
 };