Repository: prometheus/prometheus (language: go)

Issue:
React UI: Alerts page stuck loading (100% CPU usage)
**What did you do?**

Open the Alerts page.

**What did you expect to see?**

The list of Alerts.

**What did you see instead? Under which circumstances?**

The page is stuck, showing the loading spinner, but never displaying any content.
At the same time it uses 100% CPU (tested with Chrome/Firefox/Edge).
No relevant logs are displayed in the browser's console, even though the data has been fetched successfully from the API endpoint (after a few seconds).

On the other end, using the classic UI, albeit slowly (50s total), the Alerts page loads just fine.

Note: there are quite a few alerts (almost 13k).

**Environment**

* System information:

Linux 5.4.0-1036-azure x86_64

* Prometheus version:

prometheus, version 2.24.0 (branch: HEAD, revision: 02e92236a8bad3503ff5eec3e04ac205a3b8e4fe)
  build user:       root@d9f90f0b1f76
  build date:       20210106-13:48:37
  go version:       go1.15.6
  platform:         linux/amd64

* Alertmanager version:

Not relevant

* Prometheus configuration file:

The full config is quite big (especially the rules with the alerts).
I can share it on demand, hopefully only a few sections are relevant.
Perhaps the Alerts API output is more useful.

* Alertmanager configuration file:

Not relevant


* Logs:

Nothing relevant is displayed.
From the browser it just hangs at:
```
VM9:1 Fetch finished loading: GET "https://<URL>/prometheus/api/v1/rules?type=alert"
```

![image](https://user-images.githubusercontent.com/5684713/109484638-f7662080-7a80-11eb-9819-d6f999deee0f.png)



Discussion / hints:
Thanks for your report.

cc  @juliusv 
Thanks for the report! I can totally reproduce this by generating an alerts file like this:

```python
print "groups:"

for i in range(0, 1000):
  print "  - name: name_%d" % i
  print "    rules:"
  for j in range(0, 20):
    print "    - alert: FooAlert_%d_%d" % (i, j)
    print "      expr: vector(1)"
    print "      for: 5m"
    print "      labels:"
    print "        severity: critical"
```

The problem at least in my case is the React performance in the inner-most loop (https://github.com/prometheus/prometheus/blob/646556a2632700f7fca42cec51d0100294d43c52/web/ui/react-app/src/pages/alerts/CollapsibleAlertPanel.tsx#L26-L108) where it renders the individual alert detail panels. This is a similar problem to the one we have when someone has many targets. It seems React performance just gets too slow with very long nested structures like this, and it's much cheaper to generate those kinds of pages with server-side templates like in the Classic UI. Typically the answer that kind of performance issue with React seems to be "well, don't render lists that long", meaning either pagination or virtualized lists (with `react-window` or `react-virtualized`) that only render what's currently visible. But those aren't ideal either since they make it hard to do a full-page search through everything.

I don't have a solution yet, but hope to get funding soon to look deeper into UI issues like these. I would guess that having that many alerting rules in one Prometheus server is very uncommon (most people will have hundreds at most), but definitely something we need to support.
When some elements re-renders react needs to re-render everything underneath it just to check if there are any DOM updates to apply, which often is expensive.
You could try to improve it with [React.memo()](https://reactjs.org/docs/react-api.html#reactmemo) and/or [useMemo](https://reactjs.org/docs/hooks-reference.html#usememo) if there are any expensive calls. It does help a lot if component can consume data and map it directly to props, instead of doing any logic on it, as that removes the need to memoize anything.
Sadly often the best way is to limit how much you render, either by pagination of virtualised views.
@prymitive As far as I understand the issue here is really already the initial render (not re-rendering) taking so long that things just seem stuck.
I've been looking to contribute for a while and I thought this looked interesting, so I spent some time on a possible intermediate solution. Following the lines of the discussion in #6881 I tried out adding a search field to this page, which would allow the filter checkboxes to default to disabled if the counts are too high to render. The results seem ok with a few thoughts:
1. Using local storage for the filters means its possible to "brick" the page if you have the checkboxes in the wrong state and try to open the page with a huge alert set. Maybe some kind of max render value to prevent this in the short term? This could potentially just completely hide alarming rules if their count exceeds this max.
2. Search semantics between groups and rules wasn't so clear to me. My implementation prefers fuzzy matching rule names, but I can see a use case for matching a group with all child rule results instead. Going down the route of more expressive search semantics seemed over-complicated to me, but I could be completely wrong.
3. Probably there needs to be a visual indicator that some results are currently hidden, given the possibly that those results could be important. 
4. This implementation chooses to hide groups that don't have any matching rules.
5. You still have to use quite a bit of caution if you have a huge alert set, since just deleting the search field will freeze your browser without any other safety net.

Below are a few images demoing the implementation. If this seems like something that would be useful, I'd be happy to get this PR out.


Quick search demo

![AlertSearchExample](https://user-images.githubusercontent.com/5090588/113376359-5d66f180-9326-11eb-85db-277551ab6ec9.gif)

Demo with the offending example alerts file above

![BadFileWithResultExample](https://user-images.githubusercontent.com/5090588/113377081-21cd2700-9328-11eb-963e-94e931b94db2.gif)


Hi,

In order to solve this issue I think we could have:
* A scrolling loading, like that we are sure we have a limited number of element that would be loaded for the first time when coming on this page. For example, each scrolling loading could display 500 more elements.
If we don't like the srolling loading, the other solution is to create a pagination. Both are fine and it depends more on the taste.

* As not anymore every elements are displayed but are in memory, we will need the searching bar.
As the alert are quite complexed and people would perhaps search other things than just the name of the alert (like a part of the PromQL expression), it could be an idea to use a sort of grammar (like Github search) where you defined what you want to search precisely, like that: 

`expr: "sum by", alertname: "awesomeAlert"`

In the example above, the `fuzzy` search would first filter every rule that has an expression matching `sum by`, then this list is then again filtered with only the alertrule that matched `awesomeAlert` 

We could search on:
* the PromQL expression
* the labels
* the annotations
* the name of the alert 

Of course if the fuzzy search is returning more than a defined limit, then the solution described in the first point is applying. Like that the fuzzy search won't break the page whatever the user typed.

* To tackle the point raised by @coury-clark 
> Probably there needs to be a visual indicator that some results are currently hidden, given the possibly that those results could be important.

a message that would say something like `search matched ~15k result, only the first 500 are displayed, please scroll to load the rest`
could be enough. (A bit like what google is saying when you are searching something`

WDYT ?
/cc @juliusv 
and I think we can tackle these points in different PR (one for each). Like that we can benefits on what @coury-clark already did on his side (of course) if he is still ok to create the PR. It's a been a while afterall :/.
It seems that all the hidden alert details nodes (i.e. the `<pre><code>...</code></pre>` of the YAML rule definitions) are all rendered in React and in the DOM, but are hidden via CSS - I'd say all this hidden DOM content is using a bunch of the CPU.

A solution could be to not use CSS-based hiding, but to conditionally render the content in React.

https://github.com/prometheus/prometheus/blob/d44b4c5f1da2b7e84ad86c73cfde05ca030595c1/web/ui/react-app/src/pages/alerts/CollapsibleAlertPanel.tsx#L24-L31

Instead of using this `<Collapse isOpen{open} ...>....</Collapse>` element I'd go with:

```tsx
const [open, toggle] = useState(false);

let contents = null;
if (open) {
  contents = (
    <pre  className="alert-cell"> ... </pre>
  );
}

return (
  <>
    <Alert ...>...</Alert>
    {contents}
  </>
);
```

This way, the default is to render nothing in the virtual or real dom for collapsed elements (apart from the clickable `<Alert>` heading) and only render when the alert has been clicked on. This would still render each and every clickable alert, which might still take up a bunch of CPU for a large number of alerts.

I haven't tested any of this - I might when I have some time (our alerts page takes a good 30s+ to render ⏳ ) and come back with results/PR if it's any good.
Hey there, I am new over here and I am eager to contribute, I read through the issue,
What I understand is that there is a single fetch request of a huge JSON, and then the rendering of that on the dom kills the browser, the idea of virtualised list is great, I was wondering If I can implement it (basically do conditional rendering as a function of scroll distance). Also for searching, instead of doing substr algo I can implement the Knuth-Morris-Pratt KMP String Matching Algorithm which will bring down the searching from O(n*m) to O(n+m), where n is the large json and m is the search string? I don't know about the time complexity of the fuzzy search, can you elaborate on that? Thanks :)
Hey :),

The fuzzy search is a bit more sofisticate than just a simple search of finding the first occurrence of a sub-string.

The fussy search is finding the different occurrence of the sub-string but the different character can be separated by others characters too.

For example, you have the pattern to search:  `bac` and the string `baac`.

A fuzzy algorithm will match the string `baac` because the pattern exists like that `<ba>` a `<c>`

At then end a fuzzy searching is usually associating for each result a score that can be used to sort the final result list. A simple approach for the score algorithm is to give a higher score to a string that contains the exact pattern without any separated caracters.

You have an example of a fuzzy implementation here: https://github.com/Nexucis/fuzzy/blob/master/src/index.ts

And if the PR https://github.com/prometheus/prometheus/pull/8988 is merged, then it will be the algo used in prometheus to filter the different list of data in the react-ui.

Finally from my point of view, even if this lib is not choosen for the fuzzy algo, I don't think reducing the complexity of the search will improve the performance of the display of the page which is the first point of this issue.

Perfomance will be improved probably by integrating a pagination. That's the simplest way to start to fix the issue.

Hope it helps you @anuraagbarde.
Thanks for the detailed explanation. I will go through the implementation for sure.

But what are your comments on the _do conditional rendering as a function of scroll distance_ approach? Pagination can get easily irritable for large lists. On the scroll distance, we can set a front buffer for rendering new dom elements and maybe remove the ones above (which are very far), or maybe do a combination of pagination and lazy conditional rendering? 

The problem with conditional rendering might be if someone wants to scrape the info off the website/UI.
Hi there!

So first of all, I think it's important to differentiate rendering speed issues on multiple different levels:

* The level of **rule groups**: relevant when there are many rule groups, but I think it's the least likely to have many elements on this layer, so maybe we don't have to do anything for that.
* The level of **rules**: relevant for when there are many alerting rules configured. This starts to get slow at hundreds to thousands of configured rules. This is already a bit more likely to happen. But on this level, even if I configure 20 groups with 100 rules each (2000 rules in total, but no firing alert elements), the page still loads reasonably fast (~5s).
* The level of **alert elements**: I think this is where things are most critical, we have been rendering way too much and people may have many thousands of concurrently firing alert elements.

So I can second what @porty said earlier, that a lot of the problem is caused by having React render lists of pending/firing alerts even for collapsed alert panels. I tried that out locally with a set of 200 alerting rules which each had ~100 alert elements present (see config at https://gist.github.com/juliusv/8def27432bc3e0ea887b0945d64ce22d), and the load time initially was ~30s for me. When applying a fix that only rendered the collapsed content when it's expanded, the load time for that case went to 1s. I created a really simple pull request for that: https://github.com/prometheus/prometheus/pull/9005

So the question is whether that is already enough to not need windowing actually, or whether people will also have such a huge cardinality on the level of rule groups (hundreds or thousands of rule groups) or rules (10ks of rules) themselves that we need to make that level more efficient as well.
And @lorenzo-biava I know it's been a while since you filed the issue, but could you let us know on which level you had a lot of elements? Probably also on the level of actually firing/pending alert elements? Or did you have a humongous number of rules / rule groups?

And btw. thanks to everyone for chiming in and sorry for the radio silence :) @anuraagbarde also totally happy about contributions like that in general, I just want to figure out first whether the windowing in this case will be really needed. We have a similar issue on the /targets page and an issue for that: https://github.com/prometheus/prometheus/issues/6881. So maybe even if we don't end up needing windowing on /alerts, it'll still be relevant there.
If I'm counting them correctly, that would be:
- Rule Groups: 1309
- Rules: 14626
- Firing alerts: 1770 instances (53 rules)

PS: Please don't judge me, it's a big dev environment (which could also use some clean up) 😂 
@lorenzo-biava Thanks for that detail, and no worries, I can imagine how that can happen, especially when generating final rule files from a bunch of different sources :)

So to cover that specific case, my fix on the level of firing alerts will not help much and we'll likely still want windowing or a similarly effective fix on the level of rule groups + rules. @anuraagbarde It would be great if you want to experiment with that.
Thanks for the mention, I am beginning to understand the complexity of the situation now and am looking into it and will try something out.

Fix patch:
diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json
index f9ca0139bc4..153a5cf4b58 100644
--- a/web/ui/react-app/package.json
+++ b/web/ui/react-app/package.json
@@ -51,7 +51,8 @@
     "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
     "eject": "react-scripts eject",
     "lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
-    "lint": "eslint --fix \"src/**/*.{ts,tsx}\""
+    "lint": "eslint --fix \"src/**/*.{ts,tsx}\"",
+    "snapshot": "react-scripts test --updateSnapshot"
   },
   "prettier": {
     "singleQuote": true,
diff --git a/web/ui/react-app/src/pages/alerts/AlertContents.tsx b/web/ui/react-app/src/pages/alerts/AlertContents.tsx
index f96ae752326..64d0c8f6756 100644
--- a/web/ui/react-app/src/pages/alerts/AlertContents.tsx
+++ b/web/ui/react-app/src/pages/alerts/AlertContents.tsx
@@ -1,10 +1,13 @@
-import React, { FC, Fragment } from 'react';
-import { Badge } from 'reactstrap';
+import React, { ChangeEvent, FC, Fragment, useEffect, useState } from 'react';
+import { Badge, Col, Row } from 'reactstrap';
 import CollapsibleAlertPanel from './CollapsibleAlertPanel';
 import Checkbox from '../../components/Checkbox';
 import { isPresent } from '../../utils';
 import { Rule } from '../../types/types';
 import { useLocalStorage } from '../../hooks/useLocalStorage';
+import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
+import { KVSearch } from '@nexucis/kvsearch';
+import SearchBar from '../../components/SearchBar';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export type RuleState = keyof RuleStatus<any>;
@@ -35,13 +38,33 @@ interface RuleGroup {
   interval: number;
 }
 
+const kvSearchRule = new KVSearch({
+  shouldSort: true,
+  indexedKeys: ['name', 'labels', ['labels', /.*/]],
+});
+
 const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [
   ['inactive', 'success'],
   ['pending', 'warning'],
   ['firing', 'danger'],
 ];
 
+function GroupContent(showAnnotations: boolean) {
+  const Content: FC<InfiniteScrollItemsProps<Rule>> = ({ items }) => {
+    return (
+      <>
+        {items.map((rule, j) => (
+          <CollapsibleAlertPanel key={rule.name + j} showAnnotations={showAnnotations} rule={rule} />
+        ))}
+      </>
+    );
+  };
+  return Content;
+}
+
 const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
+  const [groupList, setGroupList] = useState(groups);
+  const [filteredList, setFilteredList] = useState(groups);
   const [filter, setFilter] = useLocalStorage('alerts-status-filter', {
     firing: true,
     pending: true,
@@ -56,50 +79,80 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
     });
   };
 
+  const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
+    if (e.target.value !== '') {
+      const pattern = e.target.value.trim();
+      const result: RuleGroup[] = [];
+      for (const group of groups) {
+        const ruleFilterList = kvSearchRule.filter(pattern, group.rules);
+        if (ruleFilterList.length > 0) {
+          result.push({
+            file: group.file,
+            name: group.name,
+            interval: group.interval,
+            rules: ruleFilterList.map((value) => value.original as unknown as Rule),
+          });
+        }
+      }
+      setGroupList(result);
+    } else {
+      setGroupList(groups);
+    }
+  };
+
+  useEffect(() => {
+    const result: RuleGroup[] = [];
+    for (const group of groupList) {
+      const newGroup = {
+        file: group.file,
+        name: group.name,
+        interval: group.interval,
+        rules: group.rules.filter((value) => filter[value.state]),
+      };
+      if (newGroup.rules.length > 0) {
+        result.push(newGroup);
+      }
+    }
+    setFilteredList(result);
+  }, [groupList, filter]);
+
   return (
     <>
-      <div className="d-flex togglers-wrapper">
-        {stateColorTuples.map(([state, color]) => {
-          return (
-            <Checkbox
-              key={state}
-              wrapperStyles={{ marginRight: 20 }}
-              checked={filter[state]}
-              id={`${state}-toggler`}
-              onChange={toggleFilter(state)}
-            >
-              <Badge color={color} className="text-capitalize">
-                {state} ({statsCount[state]})
-              </Badge>
-            </Checkbox>
-          );
-        })}
-        <Checkbox
-          wrapperStyles={{ marginLeft: 'auto' }}
-          checked={showAnnotations.checked}
-          id="show-annotations-toggler"
-          onChange={({ target }) => setShowAnnotations({ checked: target.checked })}
-        >
-          <span style={{ fontSize: '0.9rem', lineHeight: 1.9 }}>Show annotations</span>
-        </Checkbox>
-      </div>
-      {groups.map((group, i) => {
-        const hasFilterOn = group.rules.some((rule) => filter[rule.state]);
-        return hasFilterOn ? (
-          <Fragment key={i}>
-            <GroupInfo rules={group.rules}>
-              {group.file} &gt; {group.name}
-            </GroupInfo>
-            {group.rules.map((rule, j) => {
-              return (
-                filter[rule.state] && (
-                  <CollapsibleAlertPanel key={rule.name + j} showAnnotations={showAnnotations.checked} rule={rule} />
-                )
-              );
-            })}
-          </Fragment>
-        ) : null;
-      })}
+      <Row className="align-items-center">
+        <Col className="d-flex" lg="4" md="5">
+          {stateColorTuples.map(([state, color]) => {
+            return (
+              <Checkbox key={state} checked={filter[state]} id={`${state}-toggler`} onChange={toggleFilter(state)}>
+                <Badge color={color} className="text-capitalize">
+                  {state} ({statsCount[state]})
+                </Badge>
+              </Checkbox>
+            );
+          })}
+        </Col>
+        <Col lg="5" md="4">
+          <SearchBar handleChange={handleSearchChange} placeholder="Filter by name or labels" />
+        </Col>
+        <Col className="d-flex flex-row-reverse" md="3">
+          <Checkbox
+            checked={showAnnotations.checked}
+            id="show-annotations-toggler"
+            onChange={({ target }) => setShowAnnotations({ checked: target.checked })}
+          >
+            <span style={{ fontSize: '0.9rem', lineHeight: 1.9, display: 'inline-block', whiteSpace: 'nowrap' }}>
+              Show annotations
+            </span>
+          </Checkbox>
+        </Col>
+      </Row>
+      {filteredList.map((group, i) => (
+        <Fragment key={i}>
+          <GroupInfo rules={group.rules}>
+            {group.file} &gt; {group.name}
+          </GroupInfo>
+          <CustomInfiniteScroll allItems={group.rules} child={GroupContent(showAnnotations.checked)} />
+        </Fragment>
+      ))}
     </>
   );
 };
diff --git a/web/ui/react-app/src/types/types.ts b/web/ui/react-app/src/types/types.ts
index 8f42f0836dd..69da46ce410 100644
--- a/web/ui/react-app/src/types/types.ts
+++ b/web/ui/react-app/src/types/types.ts
@@ -16,7 +16,7 @@ export interface QueryParams {
   resolution: number;
 }
 
-export interface Rule {
+export type Rule = {
   alerts: Alert[];
   annotations: Record<string, string>;
   duration: number;
@@ -29,7 +29,7 @@ export interface Rule {
   query: string;
   state: RuleState;
   type: string;
-}
+};
 
 export interface WALReplayData {
   min: number;


Test patch:
diff --git a/web/ui/react-app/src/pages/alerts/__snapshots__/AlertContents.test.tsx.snap b/web/ui/react-app/src/pages/alerts/__snapshots__/AlertContents.test.tsx.snap
index ff0b72cf8e3..856a8acab18 100644
--- a/web/ui/react-app/src/pages/alerts/__snapshots__/AlertContents.test.tsx.snap
+++ b/web/ui/react-app/src/pages/alerts/__snapshots__/AlertContents.test.tsx.snap
@@ -2,99 +2,141 @@
 
 exports[`AlertsContent matches a snapshot 1`] = `
 <Fragment>
-  <div
-    className="d-flex togglers-wrapper"
+  <Row
+    className="align-items-center"
+    tag="div"
+    widths={
+      Array [
+        "xs",
+        "sm",
+        "md",
+        "lg",
+        "xl",
+      ]
+    }
   >
-    <Memo(Checkbox)
-      checked={true}
-      id="inactive-toggler"
-      key="inactive"
-      onChange={[Function]}
-      wrapperStyles={
-        Object {
-          "marginRight": 20,
-        }
+    <Col
+      className="d-flex"
+      lg="4"
+      md="5"
+      tag="div"
+      widths={
+        Array [
+          "xs",
+          "sm",
+          "md",
+          "lg",
+          "xl",
+        ]
       }
     >
-      <Badge
-        className="text-capitalize"
-        color="success"
-        pill={false}
-        tag="span"
+      <Memo(Checkbox)
+        checked={true}
+        id="inactive-toggler"
+        key="inactive"
+        onChange={[Function]}
       >
-        inactive
-         (
-        0
-        )
-      </Badge>
-    </Memo(Checkbox)>
-    <Memo(Checkbox)
-      checked={true}
-      id="pending-toggler"
-      key="pending"
-      onChange={[Function]}
-      wrapperStyles={
-        Object {
-          "marginRight": 20,
-        }
-      }
-    >
-      <Badge
-        className="text-capitalize"
-        color="warning"
-        pill={false}
-        tag="span"
+        <Badge
+          className="text-capitalize"
+          color="success"
+          pill={false}
+          tag="span"
+        >
+          inactive
+           (
+          0
+          )
+        </Badge>
+      </Memo(Checkbox)>
+      <Memo(Checkbox)
+        checked={true}
+        id="pending-toggler"
+        key="pending"
+        onChange={[Function]}
+      >
+        <Badge
+          className="text-capitalize"
+          color="warning"
+          pill={false}
+          tag="span"
+        >
+          pending
+           (
+          0
+          )
+        </Badge>
+      </Memo(Checkbox)>
+      <Memo(Checkbox)
+        checked={true}
+        id="firing-toggler"
+        key="firing"
+        onChange={[Function]}
       >
-        pending
-         (
-        0
-        )
-      </Badge>
-    </Memo(Checkbox)>
-    <Memo(Checkbox)
-      checked={true}
-      id="firing-toggler"
-      key="firing"
-      onChange={[Function]}
-      wrapperStyles={
-        Object {
-          "marginRight": 20,
-        }
+        <Badge
+          className="text-capitalize"
+          color="danger"
+          pill={false}
+          tag="span"
+        >
+          firing
+           (
+          0
+          )
+        </Badge>
+      </Memo(Checkbox)>
+    </Col>
+    <Col
+      lg="5"
+      md="4"
+      tag="div"
+      widths={
+        Array [
+          "xs",
+          "sm",
+          "md",
+          "lg",
+          "xl",
+        ]
       }
     >
-      <Badge
-        className="text-capitalize"
-        color="danger"
-        pill={false}
-        tag="span"
-      >
-        firing
-         (
-        0
-        )
-      </Badge>
-    </Memo(Checkbox)>
-    <Memo(Checkbox)
-      checked={false}
-      id="show-annotations-toggler"
-      onChange={[Function]}
-      wrapperStyles={
-        Object {
-          "marginLeft": "auto",
-        }
+      <SearchBar
+        handleChange={[Function]}
+        placeholder="Filter by name or labels"
+      />
+    </Col>
+    <Col
+      className="d-flex flex-row-reverse"
+      md="3"
+      tag="div"
+      widths={
+        Array [
+          "xs",
+          "sm",
+          "md",
+          "lg",
+          "xl",
+        ]
       }
     >
-      <span
-        style={
-          Object {
-            "fontSize": "0.9rem",
-            "lineHeight": 1.9,
-          }
-        }
+      <Memo(Checkbox)
+        checked={false}
+        id="show-annotations-toggler"
+        onChange={[Function]}
       >
-        Show annotations
-      </span>
-    </Memo(Checkbox)>
-  </div>
+        <span
+          style={
+            Object {
+              "display": "inline-block",
+              "fontSize": "0.9rem",
+              "lineHeight": 1.9,
+              "whiteSpace": "nowrap",
+            }
+          }
+        >
+          Show annotations
+        </span>
+      </Memo(Checkbox)>
+    </Col>
+  </Row>
 </Fragment>
 `;