Repository: mlflow/mlflow
Pull request: [Issue #2790] view PDFs in artifact viewer
## What changes are proposed in this pull request?

In response to Issue #2790, this PR introduces the ability to view PDFs in the artifact viewer. 

Here's an example of what it looks like:
![image](https://user-images.githubusercontent.com/8398819/82364938-cff80880-99c4-11ea-809d-0bf9b1db1fe3.png)

To accomplish this, the PR introduces the usage of [pdf.js](https://mozilla.github.io/pdf.js/) via [react-pdf](https://github.com/wojtekmaj/react-pdf). Pdf.js is a widely used library to do PDF rendering, and react-pdf is a well-supported React component that wraps the raw pdf.js library. Notably, pdf.js does not include an actual fully featured viewer. For the time being, pagination has been added via the antd pagination component. React-PDF supports things like search, zoom, etc., but it's not clear that it's really necessary to add all this functionality to the MLflow web UI. Users should download the PDF to use a fully featured PDF explorer.

#### Performance

The code uses pagination so only the first page is rendered when the artifact is selected. the PR uses a CDN loader for the pdf.js worker (https://github.com/wojtekmaj/react-pdf#standard-browserify-and-others), which loads the PDF in a separate thread than the UI thread. Existence of the worker thread can be confirmed in the console.

Would love to get feedback! In particular, what are people's opinions on whether the entire PDF page should show (and expand the artifact container past the original height) or whether the current behavior of having to scroll is better?

#### Known Issues

This PR introduces a warning unfortunately: "Critical dependency: require function is used in a way in which dependencies cannot be statically extracted", and the resolution is in this discussion: https://github.com/wojtekmaj/react-pdf/issues/280. It looks like the latest version of pdf.js fixes this, but it's in pre-release as of 2 months ago. Once react-pdf supports it (after it's out of pre-release), we can upgrade.

## How is this patch tested?

See React test for ShowArtifactPdfView

## Release Notes

### Is this a user-facing change?

- [ ] No. You can skip the rest of this section.
- [x] Yes. Give a description of this change to be included in the release notes for MLflow users.

(Details in 1-2 sentences. You can just refer to another PR with a description if this PR is part of a larger change.)

PDFs are now supported in the artifact viewer!

### What component(s), interfaces, languages, and integrations does this PR affect?
Components 
- [x] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for
Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: Local serving, model deployment tools, spark UDFs
- [ ] `area/tracking`: Tracking Service, tracking client APIs, autologging

Interface 
- [x] `area/uiux`: Front-end, user experience, JavaScript, plotting
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

Language 
- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients

Integrations
- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations

### How should the PR be classified in the release notes? Choose one:

- [ ] `rn/breaking-change` - The PR will be mentioned in the "Breaking Changes" section
- [ ] `rn/none` - No description will be included. The PR will be mentioned only by the PR number in the "Small Bugfixes and Documentation Updates" section
- [x] `rn/feature` - A new user-facing feature worth mentioning in the release notes
- [ ] `rn/bug-fix` - A user-facing bug fix worth mentioning in the release notes
- [ ] `rn/documentation` - A user-facing documentation change worth mentioning in the release notes


Problem statement:
[FR] Add PDF files to Preview
## Willingness to contribute
The MLflow Community encourages new feature contributions. Would you or another member of your organization be willing to contribute an implementation of this feature (either as an MLflow Plugin or an enhancement to the MLflow code base)?

- [ ] Yes. I can contribute this feature independently.
- [ ] Yes. I would be willing to contribute this feature with guidance from the MLflow community.
- [x] No. I cannot contribute this feature at this time.

## Proposal Summary

Graphs are often exported as PDF files. It would be great to support PDF files as supported format for file preview when they are logged as artifacts.

## Motivation
- What is the use case for this feature? 
  - Look at PDF images directly in the UI.
- Why is this use case valuable to support for MLflow users in general? 
  - It is helpful to look at PDF images directly in the UI as it is for other file formats.
- Why is this use case valuable to support for your project(s) or organization?
- Why is it currently difficult to achieve this use case? (please be as specific as possible about why related MLflow features and components are insufficient)
  - PDF are not supported by the UI.

## Which MLflow component(s) does this feature affect?

- [x] UI
- [ ] Command Line Interface
- [ ] API
- [ ] Examples
- [ ] Docs
- [ ] Tracking
- [ ] Projects
- [x] Artifacts
- [ ] Models
- [ ] Model Registry
- [ ] Scoring
- [ ] R
- [ ] Java
- [ ] Python

## Details

I don't know how difficult it would be to support PDF images, but it would be extremely helpful.

Hints from issue discussion:
Thank you for filling this feature request. We hope that a community member can take this task.
Short of it, you can save the artifact, download it, and view it in a pdf reader. 
@pogil a PR for this is up here: #2859

Commit message: Drop extra function bindings

Co-authored-by: Harutaka Kawamura <hkawamura0130@gmail.com>

Patch to review:
diff --git a/mlflow/server/js/package-lock.json b/mlflow/server/js/package-lock.json
index 524533232a8b5..7b349ba01a7e3 100644
--- a/mlflow/server/js/package-lock.json
+++ b/mlflow/server/js/package-lock.json
@@ -12523,6 +12523,11 @@
       "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
       "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
     },
+    "lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+    },
     "lodash.sortby": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -12615,6 +12620,11 @@
         }
       }
     },
+    "make-event-props": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.2.0.tgz",
+      "integrity": "sha512-BmWFkm/jZzVH9A0tEBdkjAARUz/eha+5IRyfOndeSMKRadkgR5DawoBHoRwLxkYmjJOI5bHkXKpaZocxj+dKgg=="
+    },
     "makeerror": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
@@ -12849,6 +12859,11 @@
       "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz",
       "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="
     },
+    "merge-class-names": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.3.0.tgz",
+      "integrity": "sha512-k0Qaj36VBpKgdc8c188LEZvo6v/zzry/FUufwopWbMSp6/knfVFU/KIB55/hJjeIpg18IH2WskXJCRnM/1BrdQ=="
+    },
     "merge-deep": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz",
@@ -13442,6 +13457,11 @@
         "lower-case": "^1.1.1"
       }
     },
+    "node-ensure": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
+      "integrity": "sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc="
+    },
     "node-fetch": {
       "version": "1.7.3",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
@@ -14277,6 +14297,15 @@
         "sha.js": "^2.4.8"
       }
     },
+    "pdfjs-dist": {
+      "version": "2.1.266",
+      "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.1.266.tgz",
+      "integrity": "sha512-Jy7o1wE3NezPxozexSbq4ltuLT0Z21ew/qrEiAEeUZzHxMHGk4DUV1D7RuCXg5vJDvHmjX1YssN+we9QfRRgXQ==",
+      "requires": {
+        "node-ensure": "^0.0.0",
+        "worker-loader": "^2.0.0"
+      }
+    },
     "performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -18054,6 +18083,31 @@
         }
       }
     },
+    "react-pdf": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-4.1.0.tgz",
+      "integrity": "sha512-SYwkWc+vRQHfrpDls3DOgn4G+wT0mYGJRor20e28GPRW8VB+6o8WqZ4QZxsl50z+dKM7UscXFnK/02eN3NXi2g==",
+      "requires": {
+        "@babel/runtime": "^7.0.0",
+        "lodash.once": "^4.1.1",
+        "make-event-props": "^1.1.0",
+        "merge-class-names": "^1.1.1",
+        "pdfjs-dist": "2.1.266",
+        "prop-types": "^15.6.2"
+      },
+      "dependencies": {
+        "prop-types": {
+          "version": "15.7.2",
+          "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+          "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+          "requires": {
+            "loose-envify": "^1.4.0",
+            "object-assign": "^4.1.1",
+            "react-is": "^16.8.1"
+          }
+        }
+      }
+    },
     "react-plotly.js": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.2.0.tgz",
@@ -23478,6 +23532,26 @@
         "errno": "~0.1.7"
       }
     },
+    "worker-loader": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz",
+      "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==",
+      "requires": {
+        "loader-utils": "^1.0.0",
+        "schema-utils": "^0.4.0"
+      },
+      "dependencies": {
+        "schema-utils": {
+          "version": "0.4.7",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
+          "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        }
+      }
+    },
     "world-calendars": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.3.tgz",
diff --git a/mlflow/server/js/package.json b/mlflow/server/js/package.json
index 2fe8b92d8f1ee..c4030e7de96d1 100644
--- a/mlflow/server/js/package.json
+++ b/mlflow/server/js/package.json
@@ -29,6 +29,7 @@
     "react-iframe": "1.8.0",
     "react-mde": "^7.6.2",
     "react-modal": "^3.4.4",
+    "react-pdf": "^4.1.0",
     "react-plotly.js": "2.2.0",
     "react-redux": "5.0.7",
     "react-router": "4.2.0",
diff --git a/mlflow/server/js/src/common/utils/FileUtils.js b/mlflow/server/js/src/common/utils/FileUtils.js
index 79e7c1d286da6..33ca5342319c6 100644
--- a/mlflow/server/js/src/common/utils/FileUtils.js
+++ b/mlflow/server/js/src/common/utils/FileUtils.js
@@ -27,3 +27,4 @@ export const TEXT_EXTENSIONS = new Set([
 ]);
 export const HTML_EXTENSIONS = new Set(['html']);
 export const MAP_EXTENSIONS = new Set(['geojson']);
+export const PDF_EXTENSIONS = new Set(['pdf']);
diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.js b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.js
index 29c302605dd56..e3c4a55da5e1d 100644
--- a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.js
+++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.js
@@ -6,11 +6,13 @@ import {
   TEXT_EXTENSIONS,
   MAP_EXTENSIONS,
   HTML_EXTENSIONS,
+  PDF_EXTENSIONS,
 } from '../../../common/utils/FileUtils';
 import ShowArtifactImageView from './ShowArtifactImageView';
 import ShowArtifactTextView from './ShowArtifactTextView';
 import ShowArtifactMapView from './ShowArtifactMapView';
 import ShowArtifactHtmlView from './ShowArtifactHtmlView';
+import ShowArtifactPdfView from './ShowArtifactPdfView';
 import previewIcon from '../../../common/static/preview-icon.png';
 import './ShowArtifactPage.css';
 
@@ -33,6 +35,8 @@ class ShowArtifactPage extends Component {
           return <ShowArtifactMapView runUuid={this.props.runUuid} path={this.props.path} />;
         } else if (HTML_EXTENSIONS.has(normalizedExtension.toLowerCase())) {
           return <ShowArtifactHtmlView runUuid={this.props.runUuid} path={this.props.path} />;
+        } else if (PDF_EXTENSIONS.has(normalizedExtension.toLowerCase())) {
+          return <ShowArtifactPdfView runUuid={this.props.runUuid} path={this.props.path} />;
         }
       }
     }
@@ -45,7 +49,7 @@ class ShowArtifactPage extends Component {
           <div className='select-preview-text'>
             <span className='select-preview-header'>Select a file to preview</span>
             <span className='select-preview-supported-formats'>
-              Supported formats: image, text, html, geojson files
+              Supported formats: image, text, html, pdf, geojson files
             </span>
           </div>
         </div>
diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.css b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.css
new file mode 100644
index 0000000000000..b213f7b65f943
--- /dev/null
+++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.css
@@ -0,0 +1,26 @@
+.pdf-outer-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 500px;
+    width: 100%;
+    padding-left: 16px;
+}
+.pdf-viewer {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+.paginator {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.document {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding-top: 15px;
+}
diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.js b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.js
new file mode 100644
index 0000000000000..d03adaca0b3ac
--- /dev/null
+++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.js
@@ -0,0 +1,110 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import './ShowArtifactImageView.css';
+import { getSrc } from './ShowArtifactPage';
+import { Document, Page, pdfjs } from 'react-pdf';
+import { Pagination, Spin } from 'antd';
+import { getArtifactBytesContent } from './ShowArtifactUtils';
+import './ShowArtifactPdfView.css';
+
+class ShowArtifactPdfView extends Component {
+  constructor(props) {
+    super(props);
+  }
+
+  state = {
+    loading: true,
+    error: undefined,
+    pdfData: undefined,
+    currentPage: 1,
+    numPages: 1,
+  };
+
+  static propTypes = {
+    runUuid: PropTypes.string.isRequired,
+    path: PropTypes.string.isRequired,
+    getArtifact: PropTypes.func,
+  };
+
+  static defaultProps = {
+    getArtifact: getArtifactBytesContent,
+  };
+
+  /** Fetches artifacts and updates component state with the result */
+  fetchPdf() {
+    const artifactLocation = getSrc(this.props.path, this.props.runUuid);
+    this.props
+      .getArtifact(artifactLocation)
+      .then((artifactPdfData) => {
+        this.setState({ pdfData: { data: artifactPdfData }, loading: false });
+      })
+      .catch((error) => {
+        this.setState({ error: error, loading: false });
+      });
+  }
+
+  componentDidMount = () => {
+    // eslint-disable-next-line max-len
+    pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
+    this.fetchPdf();
+  };
+
+  componentDidUpdate(prevProps) {
+    if (this.props.path !== prevProps.path || this.props.runUuid !== prevProps.runUuid) {
+      this.fetchPdf();
+    }
+  }
+
+  onDocumentLoadSuccess = ({ numPages }) => {
+    this.setState({ numPages });
+  };
+
+  onPageChange = (newPageNumber, itemsPerPage) => {
+    this.setState({ currentPage: newPageNumber });
+  };
+
+  renderPdf = () => {
+    return (
+      <React.Fragment>
+        <div className='pdf-viewer'>
+          <div className='paginator'>
+            <Pagination
+              simple
+              current={this.state.currentPage}
+              total={this.state.numPages}
+              pageSize={1}
+              onChange={this.onPageChange}
+            />
+          </div>
+          <div className='document'>
+            <Document
+              file={this.state.pdfData}
+              onLoadSuccess={this.onDocumentLoadSuccess}
+              loading={<Spin />}
+            >
+              <Page pageNumber={this.state.currentPage} loading={<Spin />} />
+            </Document>
+          </div>
+        </div>
+      </React.Fragment>
+    );
+  };
+
+  render() {
+    if (this.state.loading) {
+      return <div className='artifact-pdf-view-loading'>Loading...</div>;
+    }
+    if (this.state.error) {
+      return (
+        <div className='artifact-pdf-view-error'>
+          Oops we couldn't load your file because of an error.
+          Please reload the page to try again.
+        </div>
+      );
+    } else {
+      return <div className='pdf-outer-container'>{this.renderPdf()}</div>;
+    }
+  }
+}
+
+export default ShowArtifactPdfView;
diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.test.js b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.test.js
new file mode 100644
index 0000000000000..ec4666cb72989
--- /dev/null
+++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.test.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import ShowArtifactPdfView from './ShowArtifactPdfView';
+
+describe('ShowArtifactPdfView', () => {
+  let wrapper;
+  let instance;
+  let minimalProps;
+  let commonProps;
+
+  beforeEach(() => {
+    minimalProps = {
+      path: 'fakepath',
+      runUuid: 'fakeUuid',
+    };
+    // Mock the `getArtifact` function to avoid spurious network errors
+    // during testing
+    const getArtifact = jest.fn((artifactLocation) => {
+      return Promise.resolve('some content');
+    });
+    commonProps = { ...minimalProps, getArtifact: getArtifact };
+    wrapper = shallow(<ShowArtifactPdfView {...commonProps} />);
+  });
+
+  test('should render with minimal props without exploding', () => {
+    wrapper = shallow(<ShowArtifactPdfView {...minimalProps} />);
+    expect(wrapper.length).toBe(1);
+  });
+
+  test('should render loading text when view is loading', () => {
+    instance = wrapper.instance();
+    instance.setState({ loading: true });
+    expect(wrapper.find('.artifact-pdf-view-loading').length).toBe(1);
+  });
+
+  test('should render error message when error occurs', (done) => {
+    const getArtifact = jest.fn((artifactLocation) => {
+      return Promise.reject(new Error('my error text'));
+    });
+    const props = { ...minimalProps, getArtifact: getArtifact };
+    wrapper = shallow(<ShowArtifactPdfView {...props} />);
+    setImmediate(() => {
+      expect(wrapper.find('.artifact-pdf-view-error').length).toBe(1);
+      expect(wrapper.instance().state.loading).toBe(false);
+      expect(wrapper.instance().state.html).toBeUndefined();
+      expect(wrapper.instance().state.error).toBeDefined();
+      done();
+    });
+  });
+
+  test('should render PDF in container', () => {
+    wrapper.setState({ loading: false });
+    wrapper.setProps({ path: 'fake.pdf', runUuid: 'fakeRunId' });
+    expect(wrapper.find('.pdf-outer-container')).toHaveLength(1);
+    expect(wrapper.find('.pdf-viewer')).toHaveLength(1);
+    expect(wrapper.find('.paginator')).toHaveLength(1);
+    expect(wrapper.find('.document')).toHaveLength(1);
+  });
+
+  test('should call fetchPdf on component update', () => {
+    instance = wrapper.instance();
+    instance.fetchPdf = jest.fn();
+    wrapper.setProps({ path: 'newpath', runUuid: 'newRunId' });
+    expect(instance.fetchPdf).toBeCalled();
+  });
+});
diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactUtils.js b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactUtils.js
index 46867e14e9551..8b08d897d4bf9 100644
--- a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactUtils.js
+++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactUtils.js
@@ -2,7 +2,8 @@ import { getRequestHeaders } from '../../../setupAjaxHeaders';
 
 /**
  * Fetches the specified artifact, returning a Promise that resolves with
- * the artifact content if the fetch is successful, and rejects otherwise
+ * the raw content converted to text of the artifact if the fetch is
+ * successful, and rejects otherwise
  */
 export function getArtifactContent(artifactLocation) {
   return new Promise((resolve, reject) => {
@@ -26,3 +27,30 @@ export function getArtifactContent(artifactLocation) {
       .catch((error) => reject(error));
   });
 }
+
+/**
+ * Fetches the specified artifact, returning a Promise that resolves with
+ * the raw content in bytes of the artifact if the fetch is successful, and rejects otherwise
+ */
+export function getArtifactBytesContent(artifactLocation) {
+  return new Promise((resolve, reject) => {
+    const getArtifactRequest = new Request(artifactLocation, {
+      method: 'GET',
+      redirect: 'follow',
+      headers: new Headers(getRequestHeaders(document.cookie)),
+    });
+    fetch(getArtifactRequest)
+      .then((response) => {
+        return response.blob();
+      })
+      .then((blob) => {
+        const fileReader = new FileReader();
+        fileReader.onload = (event) => {
+          // Resolve promise with artifact contents
+          resolve(event.target.result);
+        };
+        fileReader.readAsArrayBuffer(blob);
+      })
+      .catch((error) => reject(error));
+  });
+}


Code review comments:
diff_hunk: @@ -0,0 +1,110 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import './ShowArtifactImageView.css';
+import { getSrc } from './ShowArtifactPage';
+import { Document, Page, pdfjs } from 'react-pdf';
+import { Pagination, Spin } from 'antd';
+import { getArtifactBytesContent } from './ShowArtifactUtils';
+import './ShowArtifactPdfView.css';
+
+class ShowArtifactPdfView extends Component {
+  constructor(props) {
+    super(props);
+  }
+
+  state = {
+    loading: true,
+    error: undefined,
+    pdfData: undefined,
+    currentPage: 1,
+    numPages: 1,
+  };
+
+  static propTypes = {
+    runUuid: PropTypes.string.isRequired,
+    path: PropTypes.string.isRequired,
+    getArtifact: PropTypes.func,
+  };
+
+  static defaultProps = {
+    getArtifact: getArtifactBytesContent,
+  };
+
+  /** Fetches artifacts and updates component state with the result */
+  fetchPdf() {
+    const artifactLocation = getSrc(this.props.path, this.props.runUuid);
+    this.props
+      .getArtifact(artifactLocation)
+      .then((artifactPdfData) => {
+        this.setState({ pdfData: { data: artifactPdfData }, loading: false });
+      })
+      .catch((error) => {
+        this.setState({ error: error, loading: false });
+      });
+  }
+
+  componentDidMount = () => {
path: mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPdfView.js
text: @user1:
```suggestion
  componentDidMount() {
```

I think a React lifecycle method should not be an arrow function.

@author:
Done - thanks for the note!