<commit_before>import cx from "classnames";
import { fromPairs, set, find } from "lodash/fp";
import PropTypes from "prop-types";
import React from "react";

import {
  getPhyloTree,
  getPhyloTrees,
  retryPhyloTree,
  saveVisualization,
} from "~/api";
import {
  ANALYTICS_EVENT_NAMES,
  logAnalyticsEvent,
  withAnalytics,
} from "~/api/analytics";
import { getPhyloTreeNg, rerunPhyloTreeNg } from "~/api/phylo_tree_ngs";
import BasicPopup from "~/components/BasicPopup";
import DetailsSidebar from "~/components/common/DetailsSidebar";
import { UserContext } from "~/components/common/UserContext";
import NarrowContainer from "~/components/layout/NarrowContainer";
import ExternalLink from "~/components/ui/controls/ExternalLink";
import PrimaryButton from "~/components/ui/controls/buttons/PrimaryButton";
import {
  PHYLO_TREE_LINK,
  MAIL_TO_HELP_LINK,
} from "~/components/utils/documentationLinks";
import { PHYLO_TREE_NG_FEATURE } from "~/components/utils/features";
import SampleMessage from "~/components/views/SampleView/SampleMessage";
import PhyloTreeHeatmapErrorModal from "~/components/views/phylo_tree/PhyloTreeHeatmapErrorModal";
import { copyShortUrlToClipboard, parseUrlParams } from "~/helpers/url";
import { SaveButton, ShareButton } from "~ui/controls/buttons";
import { IconAlert } from "~ui/icons";
import Notification from "~ui/notifications/Notification";

import Divider from "../../layout/Divider";
import ViewHeader from "../../layout/ViewHeader/ViewHeader";
import PhyloTreeDownloadButton from "./PhyloTreeDownloadButton";
import PhyloTreeVis from "./PhyloTreeVis";
import cs from "./phylo_tree_list_view.scss";

// Old PhyloTree statuses:
const STATUS_INITIALIZED = 0;
// Unused: const STATUS_READY = 1;
const STATUS_FAILED = 2;
const STATUS_IN_PROGRESS = 3;

// PhyloTreeNg statuses:
const NG_STATUS_CREATED = "CREATED";
const NG_STATUS_RUNNING = "RUNNING";
const NG_STATUS_FAILED = "FAILED";

class PhyloTreeListView extends React.Component {
  constructor(props) {
    super(props);

    const urlParams = parseUrlParams();

    this.state = {
      currentTree: null,
      heatmapErrorModalOpen: false,
      phyloTreeMap: fromPairs(
        (props.phyloTrees || []).map(tree => [tree.id, tree])
      ),
      phyloTrees: props.phyloTrees || [],
      selectedPipelineRunId: null,
      selectedSampleId: null,
      selectedPhyloTreeId: this.getDefaultSelectedTreeId(
        urlParams,
        props.phyloTrees
      ),
      showOldTreeWarning: true,
      selectedPhyloTreeNgId: props.selectedPhyloTreeNgId,
      sidebarConfig: null,
      sidebarMode: null,
      sidebarVisible: false,
      treeContainer: null,
    };
    this.selectedMetadata = urlParams.selectedMetadata;
  }

  getDefaultSelectedTreeId(urlParams, phyloTrees = []) {
    const selectedId =
      urlParams.treeId || parseInt(window.sessionStorage.getItem("treeId"));

    // If the selected tree doesn't exist, default to the first one.
    if (!selectedId || !find({ id: selectedId }, phyloTrees)) {
      return (phyloTrees[0] || {}).id;
    }

    return selectedId;
  }

  async componentDidMount() {
    const { allowedFeatures = [] } = this.context || {};
    const { phyloTrees } = this.props;
    const { selectedPhyloTreeId, selectedPhyloTreeNgId } = this.state;

    if (selectedPhyloTreeNgId) {
      const currentTree = await getPhyloTreeNg(selectedPhyloTreeNgId);
      this.setState({
        currentTree,
        showOldTreeWarning: false,
        heatmapErrorModalOpen: currentTree.clustermap_svg_url,
      });
    } else if (selectedPhyloTreeId) {
      this.setState({ currentTree: await getPhyloTree(selectedPhyloTreeId) });
    }

    // Populating the dropdown list of trees: First make sure the list of old
    // trees is there. Then add new NG trees if feature is enabled:
    let allPhyloTrees = phyloTrees || [];
    if (!phyloTrees) {
      const { phyloTrees } = await getPhyloTrees();
      allPhyloTrees = phyloTrees;
    }
    if (allowedFeatures.includes(PHYLO_TREE_NG_FEATURE)) {
      const { phyloTrees: phyloTreeNgs } = await getPhyloTrees({
        nextGeneration: true,
      });
      allPhyloTrees = [...phyloTreeNgs, ...allPhyloTrees];
    }
    this.setState({ phyloTrees: allPhyloTrees });
  }

  handleTreeChange = async (newPhyloTreeId, nextGeneration = false) => {
    if (nextGeneration) {
      const currentTree = await getPhyloTreeNg(newPhyloTreeId);
      this.setState({
        currentTree,
        selectedPhyloTreeId: null,
        selectedPhyloTreeNgId: newPhyloTreeId,
        showOldTreeWarning: false,
        sidebarVisible: false,
      });

      window.history.replaceState(
        window.history.state,
        document.title,
        `/phylo_tree_ngs/${newPhyloTreeId}`
      );
      // NG isn't using the sessionStorage 'default tree' functionality

      logAnalyticsEvent("PhyloTreeListView_phylo-tree_changed", {
        selectedPhyloTreeNgId: newPhyloTreeId,
      });
    } else {
      // OLD TREE SELECTED
      const currentTree = await getPhyloTree(newPhyloTreeId);
      this.setState({
        currentTree,
        selectedPhyloTreeId: newPhyloTreeId,
        selectedPhyloTreeNgId: null,
        showOldTreeWarning: true,
        sidebarVisible: false,
      });

      window.history.replaceState(
        window.history.state,
        document.title,
        `/phylo_trees/index?treeId=${newPhyloTreeId}`
      );
      // TODO (gdingle): do we want to keep using sessionStorage and cookies and urlparams and db saving?!
      window.sessionStorage.setItem("treeId", newPhyloTreeId);

      logAnalyticsEvent("PhyloTreeListView_phylo-tree_changed", {
        selectedPhyloTreeId: newPhyloTreeId,
      });
    }
  };

  afterSelectedMetadataChange = selectedMetadata => {
    this.persistInUrl("selectedMetadata", selectedMetadata);
  };

  persistInUrl = (param, value) => {
    try {
      const url = new URL(window.location.href);
      url.searchParams.set(param, value);
      // TODO (gdingle): make back button load previous vis state
      history.replaceState(window.history.state, document.title, url);
    } catch (e) {
      // eslint-disable-next-line no-console
      console.error(e);
    }
  };

  handleMetadataUpdate = (key, newValue) => {
    // Update the metadata stored locally.
    this.setState({
      currentTree: set(
        [
          "sampleDetailsByNodeName",
          this.state.selectedPipelineRunId,
          "metadata",
          key,
        ],
        newValue,
        this.state.currentTree
      ),
    });
  };

  handleShareClick = async () => {
    await copyShortUrlToClipboard();
  };

  handleSaveClick = async () => {
    // TODO (gdingle): add analytics tracking?
    let params = parseUrlParams();
    const sampleIds = Object.values(
      this.state.currentTree.sampleDetailsByNodeName
    )
      .map(details => details.sample_id)
      .filter(s => !!s);
    params.sampleIds = new Set(sampleIds);
    await saveVisualization("phylo_tree", params);
  };

  getTreeStatus(tree) {
    let statusMessage = "";
    switch (tree) {
      case NG_STATUS_CREATED:
      case NG_STATUS_RUNNING:
      case STATUS_IN_PROGRESS:
      case STATUS_INITIALIZED:
        statusMessage = "Computation in progress. Please check back later!";
        break;
      default:
        // TODO: process error
        statusMessage = "Tree unavailable!";
        break;
    }
    return statusMessage;
  }

  handleTaxonModeOpen = () => {
    const currentTree = this.state.currentTree;
    if (
      this.state.sidebarMode === "taxonDetails" &&
      this.state.sidebarVisible &&
      currentTree.taxid === this.state.sidebarConfig.taxonId
    ) {
      this.setState({
        sidebarVisible: false,
      });
      logAnalyticsEvent("PhyloTreeListView_taxon-details-sidebar_closed", {
        sidebarMode: "taxonDetails",
        treeName: currentTree.name,
        treeId: currentTree.id,
      });
    } else {
      this.setState({
        sidebarVisible: true,
        sidebarConfig: {
          parentTaxonId: currentTree.parent_taxid,
          taxonId: currentTree.taxid,
          taxonName: currentTree.tax_name,
        },
        sidebarMode: "taxonDetails",
      });
      logAnalyticsEvent("PhyloTreeListView_taxon-details-sidebar_opened", {
        sidebarMode: "taxonDetails",
        treeName: currentTree.name,
        treeId: currentTree.id,
        parentTaxonId: currentTree.parent_taxid,
        taxonId: currentTree.taxid,
        taxonName: currentTree.tax_name,
      });
    }
  };

  handleSampleNodeClick = (sampleId, pipelineRunId) => {
    if (!sampleId) {
      this.setState({
        sidebarVisible: false,
      });
      return;
    }

    if (
      this.state.sidebarVisible &&
      this.state.sidebarMode === "sampleDetails" &&
      this.state.selectedSampleId === sampleId
    ) {
      this.setState({
        sidebarVisible: false,
      });
      logAnalyticsEvent("PhyloTreeListView_sample-details-sidebar_closed", {
        sidebarMode: "sampleDetails",
        selectedSampleId: sampleId,
      });
    } else {
      this.setState({
        selectedSampleId: sampleId,
        selectedPipelineRunId: pipelineRunId,
        sidebarConfig: {
          sampleId,
          onMetadataUpdate: this.handleMetadataUpdate,
          showReportLink: true,
        },
        sidebarMode: "sampleDetails",
        sidebarVisible: true,
      });
      logAnalyticsEvent("PhyloTreeListView_sample-details-sidebar_opened", {
        sidebarMode: "sampleDetails",
        selectedSampleId: sampleId,
      });
    }
  };

  closeSidebar = () => {
    this.setState({
      sidebarVisible: false,
    });
  };

  renderAdminPanel = () => {
    const { currentTree, phyloTreeMap } = this.state;
    return (
      <div className={cs.adminPanel}>
        <div className={cs.header}>Admin panel:</div>
        <PrimaryButton
          text="Retry Phylo Tree"
          onClick={() => {
            retryPhyloTree(currentTree.id);
            location.reload();
          }}
        />
        <PrimaryButton
          text="Retry Phylo Tree NG"
          onClick={() => {
            rerunPhyloTreeNg(currentTree.id);
            location.reload();
          }}
        />
        <div>
          <pre>{JSON.stringify(phyloTreeMap[currentTree.id], null, 2)}</pre>
        </div>
      </div>
    );
  };

  hideOldTreeWarning = () => {
    this.setState({ showOldTreeWarning: false });
  };

  handleCloseHeatmapErrorModal = () => {
    this.setState({ heatmapErrorModalOpen: false });
  };

  renderVisualization = () => {
    const { currentTree } = this.state;
    const clustermapSvgUrl = currentTree
      ? currentTree.clustermap_svg_url
      : false;

    if (currentTree.newick) {
      return (
        <PhyloTreeVis
          afterSelectedMetadataChange={this.afterSelectedMetadataChange}
          defaultMetadata={this.selectedMetadata}
          newick={currentTree.newick}
          nodeData={currentTree.sampleDetailsByNodeName}
          onMetadataUpdate={this.handleMetadataUpdate}
          onNewTreeContainer={t => this.setState({ treeContainer: t })}
          onSampleNodeClick={this.handleSampleNodeClick}
          phyloTreeId={this.state.selectedPhyloTreeId}
        />
      );
    } else if (clustermapSvgUrl) {
      return <img className={cs.heatmap} src={clustermapSvgUrl} />;
    } else if ([STATUS_FAILED, NG_STATUS_FAILED].includes(currentTree.status)) {
      return (
        <SampleMessage
          icon={<IconAlert className={cs.iconAlert} type={"error"} />}
          link={MAIL_TO_HELP_LINK}
          linkText="Contact us for help."
          message="Sorry, we were unable to compute a phylogenetic tree."
          status="Tree Failed"
          type="error"
          onClick={() =>
            logAnalyticsEvent(
              ANALYTICS_EVENT_NAMES.PHYLO_TREE_LIST_VIEW_PIPELINE_ERROR_HELP_CLICKED
            )
          }
        />
      );
    } else {
      return (
        <div className={cs.noTreeBanner}>
          {this.getTreeStatus(currentTree.status)}
        </div>
      );
    }
  };

  renderHeader = () => {
    const {
      currentTree,
      phyloTrees,
      selectedPhyloTreeNgId,
      treeContainer,
    } = this.state;
    return (
      <NarrowContainer>
        <ViewHeader title="Phylogenetic Trees" className={cs.viewHeader}>
          <ViewHeader.Content>
            <ViewHeader.Pretitle>
              Phylogenetic Tree{" "}
              {currentTree.tax_name && (
                <span>
                  &nbsp;-&nbsp;
                  <span
                    className={cs.taxonName}
                    onClick={this.handleTaxonModeOpen}
                  >
                    {currentTree.tax_name}
                  </span>
                </span>
              )}
            </ViewHeader.Pretitle>
            <ViewHeader.Title
              label={currentTree.name}
              id={this.state.selectedPhyloTreeId}
              options={phyloTrees.map(tree => ({
                label: tree.name,
                id: `${tree.id}-${tree.nextGeneration}`,
                onClick: () =>
                  this.handleTreeChange(tree.id, tree.nextGeneration),
              }))}
            />
          </ViewHeader.Content>
          <ViewHeader.Controls className={cs.controls}>
            <BasicPopup
              trigger={
                <ShareButton
                  onClick={withAnalytics(
                    this.handleShareClick,
                    "PhyloTreeListView_share-button_clicked",
                    {
                      treeName: currentTree.name,
                      treeId: currentTree.id,
                    }
                  )}
                  className={cs.controlElement}
                />
              }
              content="A shareable URL was copied to your clipboard!"
              on="click"
              hideOnScroll
            />
            <SaveButton
              onClick={withAnalytics(
                this.handleSaveClick,
                "PhyloTreeListView_save-button_clicked",
                {
                  treeName: currentTree.name,
                  treeId: currentTree.id,
                }
              )}
              className={cs.controlElement}
            />
            <PhyloTreeDownloadButton
              className={cs.controlElement}
              showPhyloTreeNgOptions={!!selectedPhyloTreeNgId}
              tree={currentTree}
              treeContainer={treeContainer}
            />
          </ViewHeader.Controls>
        </ViewHeader>
      </NarrowContainer>
    );
  };

  renderOldTreeWarning = () => {
    const { showOldTreeWarning } = this.state;
    return (
      <Notification
        className={cx(cs.notification, showOldTreeWarning ? cs.show : cs.hide)}
        type="info"
        displayStyle="flat"
        onClose={this.hideOldTreeWarning}
        closeWithDismiss={false}
      >
        <div className={cs.notificationContent}>
          <span className={cs.warning}>
            This tree was created with a previous version of our phylogenetic
            tree module that used kSNP3.
          </span>{" "}
          Please create a new tree from these samples to use our new module,
          which uses SKA, for continued analysis.{" "}
          <ExternalLink
            coloredBackground={true}
            href={PHYLO_TREE_LINK}
            analyticsEventName={
              ANALYTICS_EVENT_NAMES.OLD_PHYLO_TREE_WARNING_BANNER_HELP_LINK_CLICKED
            }
          >
            Learn more
          </ExternalLink>
          .
        </div>
      </Notification>
    );
  };

  render() {
    const {
      currentTree,
      heatmapErrorModalOpen,
      selectedPhyloTreeId,
      selectedPhyloTreeNgId,
      showOldTreeWarning,
    } = this.state;

    const { admin } = this.context || {};
    const { allowedFeatures = [] } = this.context || {};

    if (!selectedPhyloTreeId && !selectedPhyloTreeNgId) {
      return (
        <div className={cs.noTreeBanner}>
          No phylogenetic trees were found. You can create trees from the report
          page.
        </div>
      );
    }

    if (!currentTree) return null;
    return (
      <div className={cs.phyloTreeListView}>
        {this.renderHeader()}
        <Divider />
        <DetailsSidebar
          visible={this.state.sidebarVisible}
          mode={this.state.sidebarMode}
          onClose={withAnalytics(
            this.closeSidebar,
            "PhyloTreeListView_details-sidebar_closed",
            {
              treeName: currentTree.name,
              treeId: currentTree.id,
            }
          )}
          params={this.state.sidebarConfig}
        />
        <NarrowContainer>
          {showOldTreeWarning &&
            allowedFeatures.includes(PHYLO_TREE_NG_FEATURE) &&
            this.renderOldTreeWarning()}
          {this.renderVisualization()}
        </NarrowContainer>
        {admin &&
          currentTree.status === STATUS_FAILED &&
          this.renderAdminPanel()}
        {heatmapErrorModalOpen && (
          <PhyloTreeHeatmapErrorModal
            open
            onContinue={this.handleCloseHeatmapErrorModal}
          />
        )}
      </div>
    );
  }
}

PhyloTreeListView.propTypes = {
  allowedFeatures: PropTypes.array,
  phyloTrees: PropTypes.array,
  selectedPhyloTreeNgId: PropTypes.number,
};

PhyloTreeListView.contextType = UserContext;

export default PhyloTreeListView;
<commit_msg>Fix links on TaxonDetailsSidebar for PhyloTreeNgs (#1180)<commit_after>import cx from "classnames";
import { fromPairs, set, find } from "lodash/fp";
import PropTypes from "prop-types";
import React from "react";

import {
  getPhyloTree,
  getPhyloTrees,
  retryPhyloTree,
  saveVisualization,
} from "~/api";
import {
  ANALYTICS_EVENT_NAMES,
  logAnalyticsEvent,
  withAnalytics,
} from "~/api/analytics";
import { getPhyloTreeNg, rerunPhyloTreeNg } from "~/api/phylo_tree_ngs";
import BasicPopup from "~/components/BasicPopup";
import DetailsSidebar from "~/components/common/DetailsSidebar";
import { UserContext } from "~/components/common/UserContext";
import NarrowContainer from "~/components/layout/NarrowContainer";
import ExternalLink from "~/components/ui/controls/ExternalLink";
import PrimaryButton from "~/components/ui/controls/buttons/PrimaryButton";
import {
  PHYLO_TREE_LINK,
  MAIL_TO_HELP_LINK,
} from "~/components/utils/documentationLinks";
import { PHYLO_TREE_NG_FEATURE } from "~/components/utils/features";
import SampleMessage from "~/components/views/SampleView/SampleMessage";
import PhyloTreeHeatmapErrorModal from "~/components/views/phylo_tree/PhyloTreeHeatmapErrorModal";
import { copyShortUrlToClipboard, parseUrlParams } from "~/helpers/url";
import { SaveButton, ShareButton } from "~ui/controls/buttons";
import { IconAlert } from "~ui/icons";
import Notification from "~ui/notifications/Notification";

import Divider from "../../layout/Divider";
import ViewHeader from "../../layout/ViewHeader/ViewHeader";
import PhyloTreeDownloadButton from "./PhyloTreeDownloadButton";
import PhyloTreeVis from "./PhyloTreeVis";
import cs from "./phylo_tree_list_view.scss";

// Old PhyloTree statuses:
const STATUS_INITIALIZED = 0;
// Unused: const STATUS_READY = 1;
const STATUS_FAILED = 2;
const STATUS_IN_PROGRESS = 3;

// PhyloTreeNg statuses:
const NG_STATUS_CREATED = "CREATED";
const NG_STATUS_RUNNING = "RUNNING";
const NG_STATUS_FAILED = "FAILED";

class PhyloTreeListView extends React.Component {
  constructor(props) {
    super(props);

    const urlParams = parseUrlParams();

    this.state = {
      currentTree: null,
      heatmapErrorModalOpen: false,
      phyloTreeMap: fromPairs(
        (props.phyloTrees || []).map(tree => [tree.id, tree])
      ),
      phyloTrees: props.phyloTrees || [],
      selectedPipelineRunId: null,
      selectedSampleId: null,
      selectedPhyloTreeId: this.getDefaultSelectedTreeId(
        urlParams,
        props.phyloTrees
      ),
      showOldTreeWarning: true,
      selectedPhyloTreeNgId: props.selectedPhyloTreeNgId,
      sidebarConfig: null,
      sidebarMode: null,
      sidebarVisible: false,
      treeContainer: null,
    };
    this.selectedMetadata = urlParams.selectedMetadata;
  }

  getDefaultSelectedTreeId(urlParams, phyloTrees = []) {
    const selectedId =
      urlParams.treeId || parseInt(window.sessionStorage.getItem("treeId"));

    // If the selected tree doesn't exist, default to the first one.
    if (!selectedId || !find({ id: selectedId }, phyloTrees)) {
      return (phyloTrees[0] || {}).id;
    }

    return selectedId;
  }

  async componentDidMount() {
    const { allowedFeatures = [] } = this.context || {};
    const { phyloTrees } = this.props;
    const { selectedPhyloTreeId, selectedPhyloTreeNgId } = this.state;

    if (selectedPhyloTreeNgId) {
      const currentTree = await getPhyloTreeNg(selectedPhyloTreeNgId);
      this.setState({
        currentTree,
        showOldTreeWarning: false,
        heatmapErrorModalOpen: currentTree.clustermap_svg_url,
      });
    } else if (selectedPhyloTreeId) {
      this.setState({ currentTree: await getPhyloTree(selectedPhyloTreeId) });
    }

    // Populating the dropdown list of trees: First make sure the list of old
    // trees is there. Then add new NG trees if feature is enabled:
    let allPhyloTrees = phyloTrees || [];
    if (!phyloTrees) {
      const { phyloTrees } = await getPhyloTrees();
      allPhyloTrees = phyloTrees;
    }
    if (allowedFeatures.includes(PHYLO_TREE_NG_FEATURE)) {
      const { phyloTrees: phyloTreeNgs } = await getPhyloTrees({
        nextGeneration: true,
      });
      allPhyloTrees = [...phyloTreeNgs, ...allPhyloTrees];
    }
    this.setState({ phyloTrees: allPhyloTrees });
  }

  handleTreeChange = async (newPhyloTreeId, nextGeneration = false) => {
    if (nextGeneration) {
      const currentTree = await getPhyloTreeNg(newPhyloTreeId);
      this.setState({
        currentTree,
        selectedPhyloTreeId: null,
        selectedPhyloTreeNgId: newPhyloTreeId,
        showOldTreeWarning: false,
        sidebarVisible: false,
      });

      window.history.replaceState(
        window.history.state,
        document.title,
        `/phylo_tree_ngs/${newPhyloTreeId}`
      );
      // NG isn't using the sessionStorage 'default tree' functionality

      logAnalyticsEvent("PhyloTreeListView_phylo-tree_changed", {
        selectedPhyloTreeNgId: newPhyloTreeId,
      });
    } else {
      // OLD TREE SELECTED
      const currentTree = await getPhyloTree(newPhyloTreeId);
      this.setState({
        currentTree,
        selectedPhyloTreeId: newPhyloTreeId,
        selectedPhyloTreeNgId: null,
        showOldTreeWarning: true,
        sidebarVisible: false,
      });

      window.history.replaceState(
        window.history.state,
        document.title,
        `/phylo_trees/index?treeId=${newPhyloTreeId}`
      );
      // TODO (gdingle): do we want to keep using sessionStorage and cookies and urlparams and db saving?!
      window.sessionStorage.setItem("treeId", newPhyloTreeId);

      logAnalyticsEvent("PhyloTreeListView_phylo-tree_changed", {
        selectedPhyloTreeId: newPhyloTreeId,
      });
    }
  };

  afterSelectedMetadataChange = selectedMetadata => {
    this.persistInUrl("selectedMetadata", selectedMetadata);
  };

  persistInUrl = (param, value) => {
    try {
      const url = new URL(window.location.href);
      url.searchParams.set(param, value);
      // TODO (gdingle): make back button load previous vis state
      history.replaceState(window.history.state, document.title, url);
    } catch (e) {
      // eslint-disable-next-line no-console
      console.error(e);
    }
  };

  handleMetadataUpdate = (key, newValue) => {
    // Update the metadata stored locally.
    this.setState({
      currentTree: set(
        [
          "sampleDetailsByNodeName",
          this.state.selectedPipelineRunId,
          "metadata",
          key,
        ],
        newValue,
        this.state.currentTree
      ),
    });
  };

  handleShareClick = async () => {
    await copyShortUrlToClipboard();
  };

  handleSaveClick = async () => {
    // TODO (gdingle): add analytics tracking?
    let params = parseUrlParams();
    const sampleIds = Object.values(
      this.state.currentTree.sampleDetailsByNodeName
    )
      .map(details => details.sample_id)
      .filter(s => !!s);
    params.sampleIds = new Set(sampleIds);
    await saveVisualization("phylo_tree", params);
  };

  getTreeStatus(tree) {
    let statusMessage = "";
    switch (tree) {
      case NG_STATUS_CREATED:
      case NG_STATUS_RUNNING:
      case STATUS_IN_PROGRESS:
      case STATUS_INITIALIZED:
        statusMessage = "Computation in progress. Please check back later!";
        break;
      default:
        // TODO: process error
        statusMessage = "Tree unavailable!";
        break;
    }
    return statusMessage;
  }

  handleTaxonModeOpen = () => {
    const currentTree = this.state.currentTree;
    if (
      this.state.sidebarMode === "taxonDetails" &&
      this.state.sidebarVisible &&
      (currentTree.taxid === this.state.sidebarConfig.taxonId ||
        currentTree.tax_id === this.state.sidebarConfig.taxonId)
    ) {
      this.setState({
        sidebarVisible: false,
      });
      logAnalyticsEvent("PhyloTreeListView_taxon-details-sidebar_closed", {
        sidebarMode: "taxonDetails",
        treeName: currentTree.name,
        treeId: currentTree.id,
      });
    } else {
      this.setState({
        sidebarVisible: true,
        sidebarConfig: {
          parentTaxonId: currentTree.parent_taxid,
          taxonId: currentTree.taxid || currentTree.tax_id,
          taxonName: currentTree.tax_name,
        },
        sidebarMode: "taxonDetails",
      });
      logAnalyticsEvent("PhyloTreeListView_taxon-details-sidebar_opened", {
        sidebarMode: "taxonDetails",
        treeName: currentTree.name,
        treeId: currentTree.id,
        parentTaxonId: currentTree.parent_taxid,
        taxonId: currentTree.taxid || currentTree.tax_id,
        taxonName: currentTree.tax_name,
      });
    }
  };

  handleSampleNodeClick = (sampleId, pipelineRunId) => {
    if (!sampleId) {
      this.setState({
        sidebarVisible: false,
      });
      return;
    }

    if (
      this.state.sidebarVisible &&
      this.state.sidebarMode === "sampleDetails" &&
      this.state.selectedSampleId === sampleId
    ) {
      this.setState({
        sidebarVisible: false,
      });
      logAnalyticsEvent("PhyloTreeListView_sample-details-sidebar_closed", {
        sidebarMode: "sampleDetails",
        selectedSampleId: sampleId,
      });
    } else {
      this.setState({
        selectedSampleId: sampleId,
        selectedPipelineRunId: pipelineRunId,
        sidebarConfig: {
          sampleId,
          onMetadataUpdate: this.handleMetadataUpdate,
          showReportLink: true,
        },
        sidebarMode: "sampleDetails",
        sidebarVisible: true,
      });
      logAnalyticsEvent("PhyloTreeListView_sample-details-sidebar_opened", {
        sidebarMode: "sampleDetails",
        selectedSampleId: sampleId,
      });
    }
  };

  closeSidebar = () => {
    this.setState({
      sidebarVisible: false,
    });
  };

  renderAdminPanel = () => {
    const { currentTree, phyloTreeMap } = this.state;
    return (
      <div className={cs.adminPanel}>
        <div className={cs.header}>Admin panel:</div>
        <PrimaryButton
          text="Retry Phylo Tree"
          onClick={() => {
            retryPhyloTree(currentTree.id);
            location.reload();
          }}
        />
        <PrimaryButton
          text="Retry Phylo Tree NG"
          onClick={() => {
            rerunPhyloTreeNg(currentTree.id);
            location.reload();
          }}
        />
        <div>
          <pre>{JSON.stringify(phyloTreeMap[currentTree.id], null, 2)}</pre>
        </div>
      </div>
    );
  };

  hideOldTreeWarning = () => {
    this.setState({ showOldTreeWarning: false });
  };

  handleCloseHeatmapErrorModal = () => {
    this.setState({ heatmapErrorModalOpen: false });
  };

  renderVisualization = () => {
    const { currentTree } = this.state;
    const clustermapSvgUrl = currentTree
      ? currentTree.clustermap_svg_url
      : false;

    if (currentTree.newick) {
      return (
        <PhyloTreeVis
          afterSelectedMetadataChange={this.afterSelectedMetadataChange}
          defaultMetadata={this.selectedMetadata}
          newick={currentTree.newick}
          nodeData={currentTree.sampleDetailsByNodeName}
          onMetadataUpdate={this.handleMetadataUpdate}
          onNewTreeContainer={t => this.setState({ treeContainer: t })}
          onSampleNodeClick={this.handleSampleNodeClick}
          phyloTreeId={this.state.selectedPhyloTreeId}
        />
      );
    } else if (clustermapSvgUrl) {
      return <img className={cs.heatmap} src={clustermapSvgUrl} />;
    } else if ([STATUS_FAILED, NG_STATUS_FAILED].includes(currentTree.status)) {
      return (
        <SampleMessage
          icon={<IconAlert className={cs.iconAlert} type={"error"} />}
          link={MAIL_TO_HELP_LINK}
          linkText="Contact us for help."
          message="Sorry, we were unable to compute a phylogenetic tree."
          status="Tree Failed"
          type="error"
          onClick={() =>
            logAnalyticsEvent(
              ANALYTICS_EVENT_NAMES.PHYLO_TREE_LIST_VIEW_PIPELINE_ERROR_HELP_CLICKED
            )
          }
        />
      );
    } else {
      return (
        <div className={cs.noTreeBanner}>
          {this.getTreeStatus(currentTree.status)}
        </div>
      );
    }
  };

  renderHeader = () => {
    const {
      currentTree,
      phyloTrees,
      selectedPhyloTreeNgId,
      treeContainer,
    } = this.state;
    return (
      <NarrowContainer>
        <ViewHeader title="Phylogenetic Trees" className={cs.viewHeader}>
          <ViewHeader.Content>
            <ViewHeader.Pretitle>
              Phylogenetic Tree{" "}
              {currentTree.tax_name && (
                <span>
                  &nbsp;-&nbsp;
                  <span
                    className={cs.taxonName}
                    onClick={this.handleTaxonModeOpen}
                  >
                    {currentTree.tax_name}
                  </span>
                </span>
              )}
            </ViewHeader.Pretitle>
            <ViewHeader.Title
              label={currentTree.name}
              id={this.state.selectedPhyloTreeId}
              options={phyloTrees.map(tree => ({
                label: tree.name,
                id: `${tree.id}-${tree.nextGeneration}`,
                onClick: () =>
                  this.handleTreeChange(tree.id, tree.nextGeneration),
              }))}
            />
          </ViewHeader.Content>
          <ViewHeader.Controls className={cs.controls}>
            <BasicPopup
              trigger={
                <ShareButton
                  onClick={withAnalytics(
                    this.handleShareClick,
                    "PhyloTreeListView_share-button_clicked",
                    {
                      treeName: currentTree.name,
                      treeId: currentTree.id,
                    }
                  )}
                  className={cs.controlElement}
                />
              }
              content="A shareable URL was copied to your clipboard!"
              on="click"
              hideOnScroll
            />
            <SaveButton
              onClick={withAnalytics(
                this.handleSaveClick,
                "PhyloTreeListView_save-button_clicked",
                {
                  treeName: currentTree.name,
                  treeId: currentTree.id,
                }
              )}
              className={cs.controlElement}
            />
            <PhyloTreeDownloadButton
              className={cs.controlElement}
              showPhyloTreeNgOptions={!!selectedPhyloTreeNgId}
              tree={currentTree}
              treeContainer={treeContainer}
            />
          </ViewHeader.Controls>
        </ViewHeader>
      </NarrowContainer>
    );
  };

  renderOldTreeWarning = () => {
    const { showOldTreeWarning } = this.state;
    return (
      <Notification
        className={cx(cs.notification, showOldTreeWarning ? cs.show : cs.hide)}
        type="info"
        displayStyle="flat"
        onClose={this.hideOldTreeWarning}
        closeWithDismiss={false}
      >
        <div className={cs.notificationContent}>
          <span className={cs.warning}>
            This tree was created with a previous version of our phylogenetic
            tree module that used kSNP3.
          </span>{" "}
          Please create a new tree from these samples to use our new module,
          which uses SKA, for continued analysis.{" "}
          <ExternalLink
            coloredBackground={true}
            href={PHYLO_TREE_LINK}
            analyticsEventName={
              ANALYTICS_EVENT_NAMES.OLD_PHYLO_TREE_WARNING_BANNER_HELP_LINK_CLICKED
            }
          >
            Learn more
          </ExternalLink>
          .
        </div>
      </Notification>
    );
  };

  render() {
    const {
      currentTree,
      heatmapErrorModalOpen,
      selectedPhyloTreeId,
      selectedPhyloTreeNgId,
      showOldTreeWarning,
    } = this.state;

    const { admin } = this.context || {};
    const { allowedFeatures = [] } = this.context || {};

    if (!selectedPhyloTreeId && !selectedPhyloTreeNgId) {
      return (
        <div className={cs.noTreeBanner}>
          No phylogenetic trees were found. You can create trees from the report
          page.
        </div>
      );
    }

    if (!currentTree) return null;
    return (
      <div className={cs.phyloTreeListView}>
        {this.renderHeader()}
        <Divider />
        <DetailsSidebar
          visible={this.state.sidebarVisible}
          mode={this.state.sidebarMode}
          onClose={withAnalytics(
            this.closeSidebar,
            "PhyloTreeListView_details-sidebar_closed",
            {
              treeName: currentTree.name,
              treeId: currentTree.id,
            }
          )}
          params={this.state.sidebarConfig}
        />
        <NarrowContainer>
          {showOldTreeWarning &&
            allowedFeatures.includes(PHYLO_TREE_NG_FEATURE) &&
            this.renderOldTreeWarning()}
          {this.renderVisualization()}
        </NarrowContainer>
        {admin &&
          currentTree.status === STATUS_FAILED &&
          this.renderAdminPanel()}
        {heatmapErrorModalOpen && (
          <PhyloTreeHeatmapErrorModal
            open
            onContinue={this.handleCloseHeatmapErrorModal}
          />
        )}
      </div>
    );
  }
}

PhyloTreeListView.propTypes = {
  allowedFeatures: PropTypes.array,
  phyloTrees: PropTypes.array,
  selectedPhyloTreeNgId: PropTypes.number,
};

PhyloTreeListView.contextType = UserContext;

export default PhyloTreeListView;

{'fraction_non_alphanumeric': 0.10676217276370768, 'fraction_numerical': 0.0005116398055768739, 'mean_word_length': 2.250069284064665, 'pattern_counts': {'":': 0, '<': 107, '<?xml version=': 0, '>': 155, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4554771', 'n_tokens_mistral': 10432, 'n_tokens_neox': 10183, 'n_words': 1824}