Просмотр исходного кода

Explore: Design integration

* style header like other grafana components
* use panel container for graph and same styles for query field
* fix typeahead CSS selector (was created outside of .explore)
* use navbar buttons for +/- of rows
* moved elapsed time under run query button
* fix JS error on multiple timeseries being returned
* fix color for graph lines
* show prometheus query errors
David Kaltschmidt 7 лет назад
Родитель
Сommit
eadaff6191

+ 71 - 57
public/app/containers/Explore/Explore.tsx

@@ -4,7 +4,6 @@ import colors from 'app/core/utils/colors';
 import TimeSeries from 'app/core/time_series2';
 
 import ElapsedTime from './ElapsedTime';
-import Legend from './Legend';
 import QueryRows from './QueryRows';
 import Graph from './Graph';
 import Table from './Table';
@@ -16,9 +15,7 @@ import { decodePathComponent } from 'app/core/utils/location_util';
 function makeTimeSeriesList(dataList, options) {
   return dataList.map((seriesData, index) => {
     const datapoints = seriesData.datapoints || [];
-    const responseAlias = seriesData.target;
-    const query = options.targets[index].expr;
-    const alias = responseAlias && responseAlias !== '{}' ? responseAlias : query;
+    const alias = seriesData.target;
     const colorIndex = index % colors.length;
     const color = colors[colorIndex];
 
@@ -54,6 +51,7 @@ interface IExploreState {
   latency: number;
   loading: any;
   queries: any;
+  queryError: any;
   range: any;
   requestOptions: any;
   showingGraph: boolean;
@@ -76,6 +74,7 @@ export class Explore extends React.Component<any, IExploreState> {
       latency: 0,
       loading: false,
       queries: ensureQueries(queries),
+      queryError: null,
       range: range || { ...DEFAULT_RANGE },
       requestOptions: null,
       showingGraph: true,
@@ -94,6 +93,10 @@ export class Explore extends React.Component<any, IExploreState> {
     }
   }
 
+  componentDidCatch(error) {
+    console.error(error);
+  }
+
   handleAddQueryRow = index => {
     const { queries } = this.state;
     const nextQueries = [
@@ -155,7 +158,7 @@ export class Explore extends React.Component<any, IExploreState> {
     if (!hasQuery(queries)) {
       return;
     }
-    this.setState({ latency: 0, loading: true, graphResult: null });
+    this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
     const now = Date.now();
     const options = buildQueryOptions({
       format: 'time_series',
@@ -169,9 +172,10 @@ export class Explore extends React.Component<any, IExploreState> {
       const result = makeTimeSeriesList(res.data, options);
       const latency = Date.now() - now;
       this.setState({ latency, loading: false, graphResult: result, requestOptions: options });
-    } catch (error) {
-      console.error(error);
-      this.setState({ loading: false, graphResult: error });
+    } catch (response) {
+      console.error(response);
+      const queryError = response.data ? response.data.error : response;
+      this.setState({ loading: false, queryError });
     }
   }
 
@@ -180,7 +184,7 @@ export class Explore extends React.Component<any, IExploreState> {
     if (!hasQuery(queries)) {
       return;
     }
-    this.setState({ latency: 0, loading: true, tableResult: null });
+    this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
     const now = Date.now();
     const options = buildQueryOptions({
       format: 'table',
@@ -194,9 +198,10 @@ export class Explore extends React.Component<any, IExploreState> {
       const tableModel = res.data[0];
       const latency = Date.now() - now;
       this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
-    } catch (error) {
-      console.error(error);
-      this.setState({ loading: false, tableResult: null });
+    } catch (response) {
+      console.error(response);
+      const queryError = response.data ? response.data.error : response;
+      this.setState({ loading: false, queryError });
     }
   }
 
@@ -214,6 +219,7 @@ export class Explore extends React.Component<any, IExploreState> {
       latency,
       loading,
       queries,
+      queryError,
       range,
       requestOptions,
       showingGraph,
@@ -221,55 +227,63 @@ export class Explore extends React.Component<any, IExploreState> {
       tableResult,
     } = this.state;
     const showingBoth = showingGraph && showingTable;
-    const graphHeight = showingBoth ? '200px' : null;
-    const graphButtonClassName = showingBoth || showingGraph ? 'btn m-r-1' : 'btn btn-inverse m-r-1';
-    const tableButtonClassName = showingBoth || showingTable ? 'btn m-r-1' : 'btn btn-inverse m-r-1';
+    const graphHeight = showingBoth ? '200px' : '400px';
+    const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
+    const tableButtonActive = showingBoth || showingTable ? 'active' : '';
     return (
       <div className="explore">
-        <div className="page-body page-full">
-          <h2 className="page-sub-heading">Explore</h2>
-          {datasourceLoading ? <div>Loading datasource...</div> : null}
+        <div className="navbar">
+          <div>
+            <a className="navbar-page-btn">
+              <i className="fa fa-rocket" />
+              Explore
+            </a>
+          </div>
+          <div className="navbar__spacer" />
+          <div className="navbar-buttons">
+            <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
+              Graph
+            </button>
+            <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
+              Table
+            </button>
+          </div>
+          <TimePicker range={range} onChangeTime={this.handleChangeTime} />
+          <div className="navbar-buttons relative">
+            <button className="btn navbar-button--primary" onClick={this.handleSubmit}>
+              Run Query <i className="fa fa-level-down run-icon" />
+            </button>
+            {loading || latency ? <ElapsedTime time={latency} className="text-info" /> : null}
+          </div>
+        </div>
 
-          {datasourceError ? <div title={datasourceError}>Error connecting to datasource.</div> : null}
+        {datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
 
-          {datasource ? (
-            <div className="m-r-3">
-              <div className="nav m-b-1 navbar">
-                <div className="navbar-buttons">
-                  <button className={graphButtonClassName} onClick={this.handleClickGraphButton}>
-                    Graph
-                  </button>
-                  <button className={tableButtonClassName} onClick={this.handleClickTableButton}>
-                    Table
-                  </button>
-                </div>
-                <div className="navbar__spacer" />
-                <TimePicker range={range} onChangeTime={this.handleChangeTime} />
-                <div className="navbar-buttons">
-                  <button type="submit" className="btn btn-primary" onClick={this.handleSubmit}>
-                    <i className="fa fa-return" /> Run Query
-                  </button>
-                </div>
-                {loading || latency ? <ElapsedTime time={latency} className="" /> : null}
-              </div>
-              <QueryRows
-                queries={queries}
-                request={this.request}
-                onAddQueryRow={this.handleAddQueryRow}
-                onChangeQuery={this.handleChangeQuery}
-                onExecuteQuery={this.handleSubmit}
-                onRemoveQueryRow={this.handleRemoveQueryRow}
-              />
-              <main className="m-t-2">
-                {showingGraph ? (
-                  <Graph data={graphResult} id="explore-1" options={requestOptions} height={graphHeight} />
-                ) : null}
-                {showingGraph ? <Legend data={graphResult} /> : null}
-                {showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
-              </main>
-            </div>
-          ) : null}
-        </div>
+        {datasourceError ? (
+          <div className="explore-container" title={datasourceError}>
+            Error connecting to datasource.
+          </div>
+        ) : null}
+
+        {datasource ? (
+          <div className="explore-container">
+            <QueryRows
+              queries={queries}
+              request={this.request}
+              onAddQueryRow={this.handleAddQueryRow}
+              onChangeQuery={this.handleChangeQuery}
+              onExecuteQuery={this.handleSubmit}
+              onRemoveQueryRow={this.handleRemoveQueryRow}
+            />
+            {queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
+            <main className="m-t-2">
+              {showingGraph ? (
+                <Graph data={graphResult} id="explore-1" options={requestOptions} height={graphHeight} />
+              ) : null}
+              {showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
+            </main>
+          </div>
+        ) : null}
       </div>
     );
   }

+ 11 - 8
public/app/containers/Explore/Graph.tsx

@@ -2,11 +2,12 @@ import $ from 'jquery';
 import React, { Component } from 'react';
 import moment from 'moment';
 
+import 'vendor/flot/jquery.flot';
+import 'vendor/flot/jquery.flot.time';
 import * as dateMath from 'app/core/utils/datemath';
 import TimeSeries from 'app/core/time_series2';
 
-import 'vendor/flot/jquery.flot';
-import 'vendor/flot/jquery.flot.time';
+import Legend from './Legend';
 
 // Copied from graph.ts
 function time_format(ticks, min, max) {
@@ -86,6 +87,7 @@ class Graph extends Component<any, any> {
       return;
     }
     const series = data.map((ts: TimeSeries) => ({
+      color: ts.color,
       label: ts.label,
       data: ts.getFlotPairs('null'),
     }));
@@ -120,12 +122,13 @@ class Graph extends Component<any, any> {
   }
 
   render() {
-    const style = {
-      height: this.props.height || '400px',
-      width: this.props.width || '100%',
-    };
-
-    return <div id={this.props.id} style={style} />;
+    const { data, height } = this.props;
+    return (
+      <div className="panel-container">
+        <div id={this.props.id} className="explore-graph" style={{ height }} />
+        <Legend data={data} />
+      </div>
+    );
   }
 }
 

+ 1 - 1
public/app/containers/Explore/QueryField.tsx

@@ -50,7 +50,7 @@ class Portal extends React.Component {
   constructor(props) {
     super(props);
     this.node = document.createElement('div');
-    this.node.classList.add(`query-field-portal-${props.index}`);
+    this.node.classList.add('explore-typeahead', `explore-typeahead-${props.index}`);
     document.body.appendChild(this.node);
   }
 

+ 3 - 2
public/app/containers/Explore/QueryRows.tsx

@@ -48,10 +48,10 @@ class QueryRow extends PureComponent<any, any> {
     return (
       <div className="query-row">
         <div className="query-row-tools">
-          <button className="btn btn-small btn-inverse" onClick={this.handleClickAddButton}>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickAddButton}>
             <i className="fa fa-plus" />
           </button>
-          <button className="btn btn-small btn-inverse" onClick={this.handleClickRemoveButton}>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickRemoveButton}>
             <i className="fa fa-minus" />
           </button>
         </div>
@@ -60,6 +60,7 @@ class QueryRow extends PureComponent<any, any> {
             initialQuery={edited ? null : query}
             onPressEnter={this.handlePressEnter}
             onQueryChange={this.handleChangeQuery}
+            placeholder="Enter a PromQL query"
             request={request}
           />
         </div>

+ 1 - 0
public/app/plugins/datasource/prometheus/datasource.ts

@@ -164,6 +164,7 @@ export class PrometheusDatasource {
           legendFormat: activeTargets[index].legendFormat,
           start: start,
           end: end,
+          query: queries[index].expr,
           responseListLength: responseList.length,
           responseIndex: index,
           refId: activeTargets[index].refId,

+ 8 - 3
public/app/plugins/datasource/prometheus/result_transformer.ts

@@ -123,11 +123,16 @@ export class ResultTransformer {
   }
 
   createMetricLabel(labelData, options) {
+    let label = '';
     if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
-      return this.getOriginalMetricName(labelData);
+      label = this.getOriginalMetricName(labelData);
+    } else {
+      label = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData);
     }
-
-    return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}';
+    if (!label || label === '{}') {
+      label = options.query;
+    }
+    return label;
   }
 
   renderTemplate(aliasPattern, aliasData) {

+ 232 - 205
public/sass/pages/_explore.scss

@@ -1,14 +1,35 @@
 .explore {
-  .navbar {
-    padding-left: 0;
-    padding-right: 0;
+  .explore-container {
+    padding: 2rem;
+  }
+
+  .explore-graph {
+    width: 100%;
+    height: 100%;
+  }
+
+  .panel-container {
+    padding: 10px 10px 5px 10px;
+  }
+
+  .navbar-page-btn .fa {
+    position: relative;
+    top: -1px;
+    font-size: 19px;
+    line-height: 8px;
+    opacity: 0.75;
+    margin-right: 8px;
   }
 
   .elapsed-time {
     position: absolute;
-    right: -2.4rem;
-    top: 1.2rem;
+    left: 0;
+    right: 0;
+    top: 3.5rem;
+    text-align: center;
+    font-size: 0.8rem;
   }
+
   .graph-legend {
     flex-wrap: wrap;
   }
@@ -16,10 +37,19 @@
   .timepicker {
     display: flex;
   }
+
+  .run-icon {
+    margin-left: 0.5em;
+    transform: rotate(90deg);
+  }
+
+  .relative {
+    position: relative;
+  }
 }
 
 .query-row {
-  position: relative;
+  display: flex;
 
   & + & {
     margin-top: 0.5rem;
@@ -27,12 +57,7 @@
 }
 
 .query-row-tools {
-  position: absolute;
-  left: -4rem;
-  top: 0.33rem;
-  > * {
-    margin-right: 0.25rem;
-  }
+  width: 4rem;
 }
 
 .query-field {
@@ -49,14 +74,14 @@
   cursor: text;
   line-height: 1.5;
   color: rgba(0, 0, 0, 0.65);
-  background-color: #fff;
+  background-color: $panel-bg;
   background-image: none;
-  border: 1px solid lightgray;
+  border: $panel-border;
   border-radius: 3px;
   transition: all 0.3s;
 }
 
-.explore {
+.explore-typeahead {
   .typeahead {
     position: absolute;
     z-index: auto;
@@ -117,221 +142,223 @@
  * @author Tim  Shedor
  */
 
-code[class*='language-'],
-pre[class*='language-'] {
-  color: black;
-  background: none;
-  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
-  text-align: left;
-  white-space: pre;
-  word-spacing: normal;
-  word-break: normal;
-  word-wrap: normal;
-  line-height: 1.5;
+.explore {
+  code[class*='language-'],
+  pre[class*='language-'] {
+    color: black;
+    background: none;
+    font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+    text-align: left;
+    white-space: pre;
+    word-spacing: normal;
+    word-break: normal;
+    word-wrap: normal;
+    line-height: 1.5;
 
-  -moz-tab-size: 4;
-  -o-tab-size: 4;
-  tab-size: 4;
+    -moz-tab-size: 4;
+    -o-tab-size: 4;
+    tab-size: 4;
 
-  -webkit-hyphens: none;
-  -moz-hyphens: none;
-  -ms-hyphens: none;
-  hyphens: none;
-}
+    -webkit-hyphens: none;
+    -moz-hyphens: none;
+    -ms-hyphens: none;
+    hyphens: none;
+  }
 
-/* Code blocks */
-pre[class*='language-'] {
-  position: relative;
-  margin: 0.5em 0;
-  overflow: visible;
-  padding: 0;
-}
-pre[class*='language-'] > code {
-  position: relative;
-  border-left: 10px solid #358ccb;
-  box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
-  background-color: #fdfdfd;
-  background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
-  background-size: 3em 3em;
-  background-origin: content-box;
-  background-attachment: local;
-}
+  /* Code blocks */
+  pre[class*='language-'] {
+    position: relative;
+    margin: 0.5em 0;
+    overflow: visible;
+    padding: 0;
+  }
+  pre[class*='language-'] > code {
+    position: relative;
+    border-left: 10px solid #358ccb;
+    box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
+    background-color: #fdfdfd;
+    background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
+    background-size: 3em 3em;
+    background-origin: content-box;
+    background-attachment: local;
+  }
 
-code[class*='language'] {
-  max-height: inherit;
-  height: inherit;
-  padding: 0 1em;
-  display: block;
-  overflow: auto;
-}
+  code[class*='language'] {
+    max-height: inherit;
+    height: inherit;
+    padding: 0 1em;
+    display: block;
+    overflow: auto;
+  }
 
-/* Margin bottom to accomodate shadow */
-:not(pre) > code[class*='language-'],
-pre[class*='language-'] {
-  background-color: #fdfdfd;
-  -webkit-box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  box-sizing: border-box;
-  margin-bottom: 1em;
-}
+  /* Margin bottom to accomodate shadow */
+  :not(pre) > code[class*='language-'],
+  pre[class*='language-'] {
+    background-color: #fdfdfd;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+    margin-bottom: 1em;
+  }
 
-/* Inline code */
-:not(pre) > code[class*='language-'] {
-  position: relative;
-  padding: 0.2em;
-  border-radius: 0.3em;
-  color: #c92c2c;
-  border: 1px solid rgba(0, 0, 0, 0.1);
-  display: inline;
-  white-space: normal;
-}
+  /* Inline code */
+  :not(pre) > code[class*='language-'] {
+    position: relative;
+    padding: 0.2em;
+    border-radius: 0.3em;
+    color: #c92c2c;
+    border: 1px solid rgba(0, 0, 0, 0.1);
+    display: inline;
+    white-space: normal;
+  }
 
-pre[class*='language-']:before,
-pre[class*='language-']:after {
-  content: '';
-  z-index: -2;
-  display: block;
-  position: absolute;
-  bottom: 0.75em;
-  left: 0.18em;
-  width: 40%;
-  height: 20%;
-  max-height: 13em;
-  box-shadow: 0px 13px 8px #979797;
-  -webkit-transform: rotate(-2deg);
-  -moz-transform: rotate(-2deg);
-  -ms-transform: rotate(-2deg);
-  -o-transform: rotate(-2deg);
-  transform: rotate(-2deg);
-}
+  pre[class*='language-']:before,
+  pre[class*='language-']:after {
+    content: '';
+    z-index: -2;
+    display: block;
+    position: absolute;
+    bottom: 0.75em;
+    left: 0.18em;
+    width: 40%;
+    height: 20%;
+    max-height: 13em;
+    box-shadow: 0px 13px 8px #979797;
+    -webkit-transform: rotate(-2deg);
+    -moz-transform: rotate(-2deg);
+    -ms-transform: rotate(-2deg);
+    -o-transform: rotate(-2deg);
+    transform: rotate(-2deg);
+  }
 
-:not(pre) > code[class*='language-']:after,
-pre[class*='language-']:after {
-  right: 0.75em;
-  left: auto;
-  -webkit-transform: rotate(2deg);
-  -moz-transform: rotate(2deg);
-  -ms-transform: rotate(2deg);
-  -o-transform: rotate(2deg);
-  transform: rotate(2deg);
-}
+  :not(pre) > code[class*='language-']:after,
+  pre[class*='language-']:after {
+    right: 0.75em;
+    left: auto;
+    -webkit-transform: rotate(2deg);
+    -moz-transform: rotate(2deg);
+    -ms-transform: rotate(2deg);
+    -o-transform: rotate(2deg);
+    transform: rotate(2deg);
+  }
 
-.token.comment,
-.token.block-comment,
-.token.prolog,
-.token.doctype,
-.token.cdata {
-  color: #7d8b99;
-}
+  .token.comment,
+  .token.block-comment,
+  .token.prolog,
+  .token.doctype,
+  .token.cdata {
+    color: #7d8b99;
+  }
 
-.token.punctuation {
-  color: #5f6364;
-}
+  .token.punctuation {
+    color: #5f6364;
+  }
 
-.token.property,
-.token.tag,
-.token.boolean,
-.token.number,
-.token.function-name,
-.token.constant,
-.token.symbol,
-.token.deleted {
-  color: #c92c2c;
-}
+  .token.property,
+  .token.tag,
+  .token.boolean,
+  .token.number,
+  .token.function-name,
+  .token.constant,
+  .token.symbol,
+  .token.deleted {
+    color: #c92c2c;
+  }
 
-.token.selector,
-.token.attr-name,
-.token.string,
-.token.char,
-.token.function,
-.token.builtin,
-.token.inserted {
-  color: #2f9c0a;
-}
+  .token.selector,
+  .token.attr-name,
+  .token.string,
+  .token.char,
+  .token.function,
+  .token.builtin,
+  .token.inserted {
+    color: #2f9c0a;
+  }
 
-.token.operator,
-.token.entity,
-.token.url,
-.token.variable {
-  color: #a67f59;
-  background: rgba(255, 255, 255, 0.5);
-}
+  .token.operator,
+  .token.entity,
+  .token.url,
+  .token.variable {
+    color: #a67f59;
+    background: rgba(255, 255, 255, 0.5);
+  }
 
-.token.atrule,
-.token.attr-value,
-.token.keyword,
-.token.class-name {
-  color: #1990b8;
-}
+  .token.atrule,
+  .token.attr-value,
+  .token.keyword,
+  .token.class-name {
+    color: #1990b8;
+  }
 
-.token.regex,
-.token.important {
-  color: #e90;
-}
+  .token.regex,
+  .token.important {
+    color: #e90;
+  }
 
-.language-css .token.string,
-.style .token.string {
-  color: #a67f59;
-  background: rgba(255, 255, 255, 0.5);
-}
+  .language-css .token.string,
+  .style .token.string {
+    color: #a67f59;
+    background: rgba(255, 255, 255, 0.5);
+  }
 
-.token.important {
-  font-weight: normal;
-}
+  .token.important {
+    font-weight: normal;
+  }
 
-.token.bold {
-  font-weight: bold;
-}
-.token.italic {
-  font-style: italic;
-}
+  .token.bold {
+    font-weight: bold;
+  }
+  .token.italic {
+    font-style: italic;
+  }
 
-.token.entity {
-  cursor: help;
-}
+  .token.entity {
+    cursor: help;
+  }
 
-.namespace {
-  opacity: 0.7;
-}
+  .namespace {
+    opacity: 0.7;
+  }
 
-@media screen and (max-width: 767px) {
-  pre[class*='language-']:before,
-  pre[class*='language-']:after {
-    bottom: 14px;
-    box-shadow: none;
+  @media screen and (max-width: 767px) {
+    pre[class*='language-']:before,
+    pre[class*='language-']:after {
+      bottom: 14px;
+      box-shadow: none;
+    }
   }
-}
 
-/* Plugin styles */
-.token.tab:not(:empty):before,
-.token.cr:before,
-.token.lf:before {
-  color: #e0d7d1;
-}
+  /* Plugin styles */
+  .token.tab:not(:empty):before,
+  .token.cr:before,
+  .token.lf:before {
+    color: #e0d7d1;
+  }
 
-/* Plugin styles: Line Numbers */
-pre[class*='language-'].line-numbers {
-  padding-left: 0;
-}
+  /* Plugin styles: Line Numbers */
+  pre[class*='language-'].line-numbers {
+    padding-left: 0;
+  }
 
-pre[class*='language-'].line-numbers code {
-  padding-left: 3.8em;
-}
+  pre[class*='language-'].line-numbers code {
+    padding-left: 3.8em;
+  }
 
-pre[class*='language-'].line-numbers .line-numbers-rows {
-  left: 0;
-}
+  pre[class*='language-'].line-numbers .line-numbers-rows {
+    left: 0;
+  }
 
-/* Plugin styles: Line Highlight */
-pre[class*='language-'][data-line] {
-  padding-top: 0;
-  padding-bottom: 0;
-  padding-left: 0;
-}
-pre[data-line] code {
-  position: relative;
-  padding-left: 4em;
-}
-pre .line-highlight {
-  margin-top: 0;
+  /* Plugin styles: Line Highlight */
+  pre[class*='language-'][data-line] {
+    padding-top: 0;
+    padding-bottom: 0;
+    padding-left: 0;
+  }
+  pre[data-line] code {
+    position: relative;
+    padding-left: 4em;
+  }
+  pre .line-highlight {
+    margin-top: 0;
+  }
 }