Forráskód Böngészése

Explore: live tail UI fixes and improvements (#19187)

Andrej Ocenas 6 éve
szülő
commit
bf24cbba76

+ 13 - 1
public/app/features/explore/Explore.tsx

@@ -1,6 +1,7 @@
 // Libraries
 import React, { ComponentClass } from 'react';
 import { hot } from 'react-hot-loader';
+import { css } from 'emotion';
 // @ts-ignore
 import { connect } from 'react-redux';
 import { AutoSizer } from 'react-virtualized';
@@ -52,6 +53,16 @@ import { ErrorContainer } from './ErrorContainer';
 import { scanStopAction } from './state/actionTypes';
 import { ExploreGraphPanel } from './ExploreGraphPanel';
 
+const getStyles = memoizeOne(() => {
+  return {
+    logsMain: css`
+      label: logsMain;
+      // Is needed for some transition animations to work.
+      position: relative;
+    `,
+  };
+});
+
 interface ExploreProps {
   StartPage?: ComponentClass<ExploreStartPageProps>;
   changeSize: typeof changeSize;
@@ -257,6 +268,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
       queryResponse,
     } = this.props;
     const exploreClass = split ? 'explore explore-split' : 'explore';
+    const styles = getStyles();
 
     return (
       <div className={exploreClass} ref={this.getRef}>
@@ -284,7 +296,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
                 }
 
                 return (
-                  <main className="m-t-2" style={{ width }}>
+                  <main className={`m-t-2 ${styles.logsMain}`} style={{ width }}>
                     <ErrorBoundaryAlert>
                       {showingStartPage && (
                         <div className="grafana-info-box grafana-info-box--max-lg">

+ 21 - 8
public/app/features/explore/ExploreToolbar.tsx

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
 import { hot } from 'react-hot-loader';
 import memoizeOne from 'memoize-one';
 import classNames from 'classnames';
+import { css } from 'emotion';
 
 import { ExploreId, ExploreItemState, ExploreMode } from 'app/types/explore';
 import {
@@ -39,6 +40,14 @@ import { LiveTailButton } from './LiveTailButton';
 import { ResponsiveButton } from './ResponsiveButton';
 import { RunButton } from './RunButton';
 
+const getStyles = memoizeOne(() => {
+  return {
+    liveTailButtons: css`
+      margin-left: 10px;
+    `,
+  };
+});
+
 interface OwnProps {
   exploreId: ExploreId;
   onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
@@ -132,6 +141,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
 
   stopLive = () => {
     const { exploreId } = this.props;
+    this.pauseLive();
     // TODO referencing this from perspective of refresh picker when there is designated button for it now is not
     //  great. Needs another refactor.
     this.props.changeRefreshIntervalAction({ exploreId, refreshInterval: RefreshPicker.offOption.value });
@@ -174,6 +184,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
       originPanelId,
     } = this.props;
 
+    const styles = getStyles();
     const originDashboardIsEditable = Number.isInteger(originPanelId);
     const panelReturnClasses = classNames('btn', 'navbar-button', {
       'btn--radius-right-0': originDashboardIsEditable,
@@ -293,14 +304,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
             </div>
 
             {hasLiveOption && (
-              <LiveTailButton
-                isLive={isLive}
-                isPaused={isPaused}
-                start={this.startLive}
-                pause={this.pauseLive}
-                resume={this.resumeLive}
-                stop={this.stopLive}
-              />
+              <div className={`explore-toolbar-content-item ${styles.liveTailButtons}`}>
+                <LiveTailButton
+                  isLive={isLive}
+                  isPaused={isPaused}
+                  start={this.startLive}
+                  pause={this.pauseLive}
+                  resume={this.resumeLive}
+                  stop={this.stopLive}
+                />
+              </div>
             )}
           </div>
         </div>

+ 2 - 2
public/app/features/explore/LiveLogs.tsx

@@ -157,7 +157,7 @@ class LiveLogs extends PureComponent<Props, State> {
     const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme);
 
     return (
-      <>
+      <div>
         <div
           onScroll={isPaused ? undefined : this.onScroll}
           className={cx(['logs-rows', styles.logsRowsLive])}
@@ -210,7 +210,7 @@ class LiveLogs extends PureComponent<Props, State> {
             </span>
           )}
         </div>
-      </>
+      </div>
     );
   }
 }

+ 62 - 23
public/app/features/explore/LiveTailButton.tsx

@@ -2,28 +2,30 @@ import React from 'react';
 import classNames from 'classnames';
 import { css } from 'emotion';
 import memoizeOne from 'memoize-one';
-import { GrafanaTheme, GrafanaThemeType, useTheme } from '@grafana/ui';
 import tinycolor from 'tinycolor2';
+import { CSSTransition } from 'react-transition-group';
 
-const orangeDark = '#FF780A';
-const orangeDarkLighter = tinycolor(orangeDark)
-  .lighten(10)
-  .toString();
-const orangeLight = '#ED5700';
-const orangeLightLighter = tinycolor(orangeLight)
-  .lighten(10)
-  .toString();
+import { GrafanaTheme, GrafanaThemeType, useTheme } from '@grafana/ui';
 
 const getStyles = memoizeOne((theme: GrafanaTheme) => {
-  const orange = theme.type === GrafanaThemeType.Dark ? orangeDark : orangeLight;
-  const orangeLighter = theme.type === GrafanaThemeType.Dark ? orangeDarkLighter : orangeLightLighter;
-  const textColor = theme.type === GrafanaThemeType.Dark ? theme.colors.white : theme.colors.black;
+  const orange = theme.type === GrafanaThemeType.Dark ? '#FF780A' : '#ED5700';
+  const orangeLighter = tinycolor(orange)
+    .lighten(10)
+    .toString();
+  const pulseTextColor = tinycolor(orange)
+    .desaturate(90)
+    .toString();
 
   return {
     noRightBorderStyle: css`
       label: noRightBorderStyle;
       border-right: 0;
     `,
+    liveButton: css`
+      label: liveButton;
+      transition: background-color 1s, border-color 1s, color 1s;
+      margin: 0;
+    `,
     isLive: css`
       label: isLive;
       border-color: ${orange};
@@ -43,7 +45,7 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => {
       label: isPaused;
       border-color: ${orange};
       background: transparent;
-      animation: pulse 2s ease-out 0s infinite normal forwards;
+      animation: pulse 3s ease-out 0s infinite normal forwards;
       &:focus {
         border-color: ${orange};
       }
@@ -53,16 +55,40 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => {
       }
       @keyframes pulse {
         0% {
-          color: ${textColor};
+          color: ${pulseTextColor};
         }
         50% {
           color: ${orange};
         }
         100% {
-          color: ${textColor};
+          color: ${pulseTextColor};
         }
       }
     `,
+    stopButtonEnter: css`
+      label: stopButtonEnter;
+      width: 0;
+      opacity: 0;
+      overflow: hidden;
+    `,
+    stopButtonEnterActive: css`
+      label: stopButtonEnterActive;
+      opacity: 1;
+      width: 32px;
+      transition: opacity 500ms ease-in 50ms, width 500ms ease-in 50ms;
+    `,
+    stopButtonExit: css`
+      label: stopButtonExit;
+      width: 32px;
+      opacity: 1;
+      overflow: hidden;
+    `,
+    stopButtonExitActive: css`
+      label: stopButtonExitActive;
+      opacity: 0;
+      width: 0;
+      transition: opacity 500ms ease-in 50ms, width 500ms ease-in 50ms;
+    `,
   };
 });
 
@@ -82,9 +108,9 @@ export function LiveTailButton(props: LiveTailButtonProps) {
   const onClickMain = isLive ? (isPaused ? resume : pause) : start;
 
   return (
-    <div className="explore-toolbar-content-item">
+    <>
       <button
-        className={classNames('btn navbar-button', {
+        className={classNames('btn navbar-button', styles.liveButton, {
           [`btn--radius-right-0 ${styles.noRightBorderStyle}`]: isLive,
           [styles.isLive]: isLive && !isPaused,
           [styles.isPaused]: isLive && isPaused,
@@ -94,11 +120,24 @@ export function LiveTailButton(props: LiveTailButtonProps) {
         <i className={classNames('fa', isPaused || !isLive ? 'fa-play' : 'fa-pause')} />
         &nbsp; Live tailing
       </button>
-      {isLive && (
-        <button className={`btn navbar-button navbar-button--attached ${styles.isLive}`} onClick={stop}>
-          <i className={'fa fa-stop'} />
-        </button>
-      )}
-    </div>
+      <CSSTransition
+        mountOnEnter={true}
+        unmountOnExit={true}
+        timeout={500}
+        in={isLive}
+        classNames={{
+          enter: styles.stopButtonEnter,
+          enterActive: styles.stopButtonEnterActive,
+          exit: styles.stopButtonExit,
+          exitActive: styles.stopButtonExitActive,
+        }}
+      >
+        <div>
+          <button className={`btn navbar-button navbar-button--attached ${styles.isLive}`} onClick={stop}>
+            <i className={'fa fa-stop'} />
+          </button>
+        </div>
+      </CSSTransition>
+    </>
   );
 }

+ 0 - 1
public/app/features/explore/Logs.tsx

@@ -1,4 +1,3 @@
-import _ from 'lodash';
 import React, { PureComponent } from 'react';
 
 import {

+ 39 - 36
public/app/features/explore/LogsContainer.tsx

@@ -27,6 +27,7 @@ import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/featur
 import { getTimeZone } from '../profile/state/selectors';
 import { LiveLogsWithTheme } from './LiveLogs';
 import { Logs } from './Logs';
+import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
 
 interface LogsContainerProps {
   datasourceInstance: DataSourceApi | null;
@@ -64,6 +65,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
 
   onStopLive = () => {
     const { exploreId } = this.props;
+    this.onPause();
     this.props.stopLive({ exploreId, refreshInterval: RefreshPicker.offOption.value });
   };
 
@@ -116,43 +118,44 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
       isLive,
     } = this.props;
 
-    if (isLive) {
-      return (
-        <Collapse label="Logs" loading={false} isOpen>
-          <LiveLogsWithTheme
-            logsResult={logsResult}
-            timeZone={timeZone}
-            stopLive={this.onStopLive}
-            isPaused={this.props.isPaused}
-            onPause={this.onPause}
-            onResume={this.onResume}
-          />
-        </Collapse>
-      );
-    }
-
     return (
-      <Collapse label="Logs" loading={loading} isOpen>
-        <Logs
-          dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
-          data={logsResult}
-          dedupedData={dedupedResult}
-          highlighterExpressions={logsHighlighterExpressions}
-          loading={loading}
-          onChangeTime={this.onChangeTime}
-          onClickLabel={onClickLabel}
-          onStartScanning={onStartScanning}
-          onStopScanning={onStopScanning}
-          onDedupStrategyChange={this.handleDedupStrategyChange}
-          onToggleLogLevel={this.handleToggleLogLevel}
-          absoluteRange={absoluteRange}
-          timeZone={timeZone}
-          scanning={scanning}
-          scanRange={range.raw}
-          width={width}
-          getRowContext={this.getLogRowContext}
-        />
-      </Collapse>
+      <>
+        <LogsCrossFadeTransition visible={isLive}>
+          <Collapse label="Logs" loading={false} isOpen>
+            <LiveLogsWithTheme
+              logsResult={logsResult}
+              timeZone={timeZone}
+              stopLive={this.onStopLive}
+              isPaused={this.props.isPaused}
+              onPause={this.onPause}
+              onResume={this.onResume}
+            />
+          </Collapse>
+        </LogsCrossFadeTransition>
+        <LogsCrossFadeTransition visible={!isLive}>
+          <Collapse label="Logs" loading={loading} isOpen>
+            <Logs
+              dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
+              data={logsResult}
+              dedupedData={dedupedResult}
+              highlighterExpressions={logsHighlighterExpressions}
+              loading={loading}
+              onChangeTime={this.onChangeTime}
+              onClickLabel={onClickLabel}
+              onStartScanning={onStartScanning}
+              onStopScanning={onStopScanning}
+              onDedupStrategyChange={this.handleDedupStrategyChange}
+              onToggleLogLevel={this.handleToggleLogLevel}
+              absoluteRange={absoluteRange}
+              timeZone={timeZone}
+              scanning={scanning}
+              scanRange={range.raw}
+              width={width}
+              getRowContext={this.getLogRowContext}
+            />
+          </Collapse>
+        </LogsCrossFadeTransition>
+      </>
     );
   }
 }

+ 12 - 2
public/app/features/explore/ResponsiveButton.tsx

@@ -28,9 +28,19 @@ export const ResponsiveButton = (props: Props) => {
       onClick={onClick}
       disabled={disabled || false}
     >
-      {iconClassName && iconSide === IconSide.left ? <i className={`${iconClassName}`} /> : null}
+      {iconClassName && iconSide === IconSide.left ? (
+        <>
+          <i className={`${iconClassName}`} />
+          &nbsp;
+        </>
+      ) : null}
       <span className="btn-title">{!splitted ? title : ''}</span>
-      {iconClassName && iconSide === IconSide.right ? <i className={`${iconClassName}`} /> : null}
+      {iconClassName && iconSide === IconSide.right ? (
+        <>
+          &nbsp;
+          <i className={`${iconClassName}`} />
+        </>
+      ) : null}
     </button>
   );
 };

+ 2 - 1
public/app/features/explore/RunButton.tsx

@@ -2,6 +2,7 @@ import React from 'react';
 import { RefreshPicker } from '@grafana/ui';
 import memoizeOne from 'memoize-one';
 import { css } from 'emotion';
+import classNames from 'classnames';
 
 import { ResponsiveButton } from './ResponsiveButton';
 
@@ -33,7 +34,7 @@ export function RunButton(props: Props) {
       splitted={splitted}
       title="Run Query"
       onClick={onRun}
-      buttonClassName="navbar-button--secondary btn--radius-right-0 "
+      buttonClassName={classNames('navbar-button--secondary', { 'btn--radius-right-0': showDropdown })}
       iconClassName={loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-refresh fa-fw'}
     />
   );

+ 1 - 1
public/app/features/explore/state/reducers.ts

@@ -206,7 +206,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
           state: live ? LoadingState.Streaming : LoadingState.NotStarted,
         },
         isLive: live,
-        isPaused: false,
+        isPaused: live ? false : state.isPaused,
         loading: live,
         logsResult,
       };

+ 69 - 0
public/app/features/explore/utils/LogsCrossFadeTransition.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+import memoizeOne from 'memoize-one';
+import { css } from 'emotion';
+import { CSSTransition } from 'react-transition-group';
+
+const transitionDuration = 500;
+// We add a bit of delay to the transition as another perf optimisation. As at the start we need to render
+// quite a bit of new rows, if we start transition at the same time there can be frame rate drop. This gives time
+// for react to first render them and then do the animation.
+const transitionDelay = 100;
+
+const getStyles = memoizeOne(() => {
+  return {
+    logsEnter: css`
+      label: logsEnter;
+      position: absolute;
+      opacity: 0;
+      height: auto;
+      width: auto;
+    `,
+    logsEnterActive: css`
+      label: logsEnterActive;
+      opacity: 1;
+      transition: opacity ${transitionDuration}ms ease-out ${transitionDelay}ms;
+    `,
+    logsExit: css`
+      label: logsExit;
+      position: absolute;
+      opacity: 1;
+      height: auto;
+      width: auto;
+    `,
+    logsExitActive: css`
+      label: logsExitActive;
+      opacity: 0;
+      transition: opacity ${transitionDuration}ms ease-out ${transitionDelay}ms;
+    `,
+  };
+});
+
+type Props = {
+  children: React.ReactNode;
+  visible: boolean;
+};
+
+/**
+ * Cross fade transition component that is tied a bit too much to the logs containers so not very useful elsewhere
+ * right now.
+ */
+export function LogsCrossFadeTransition(props: Props) {
+  const { visible, children } = props;
+  const styles = getStyles();
+  return (
+    <CSSTransition
+      in={visible}
+      mountOnEnter={true}
+      unmountOnExit={true}
+      timeout={transitionDuration + transitionDelay}
+      classNames={{
+        enter: styles.logsEnter,
+        enterActive: styles.logsEnterActive,
+        exit: styles.logsExit,
+        exitActive: styles.logsExitActive,
+      }}
+    >
+      {children}
+    </CSSTransition>
+  );
+}