<commit_before>import React from "react";
import PropTypes from "prop-types";
import UrlQueryParser from "~/components/utils/UrlQueryParser";
import moment from "moment";
import {
  capitalize,
  clone,
  compact,
  concat,
  escapeRegExp,
  find,
  isEmpty,
  keyBy,
  map,
  mapKeys,
  mapValues,
  merge,
  partition,
  pick,
  replace,
  sumBy,
  union,
  values,
  xor,
  xorBy,
} from "lodash/fp";

import { getSearchSuggestions } from "~/api";
import { logAnalyticsEvent } from "~/api/analytics";
import { get } from "~/api/core";
import { openUrl } from "~utils/links";
import NarrowContainer from "~/components/layout/NarrowContainer";
import { Divider } from "~/components/layout";
import { MAP_CLUSTER_ENABLED_LEVELS } from "~/components/views/discovery/mapping/constants";
import { indexOfMapLevel } from "~/components/views/discovery/mapping/utils";
import { publicSampleNotificationsByProject } from "~/components/views/samples/notifications";

import DiscoveryHeader from "./DiscoveryHeader";
import ProjectsView from "../projects/ProjectsView";
import SamplesView from "../samples/SamplesView";
import VisualizationsView from "../visualizations/VisualizationsView";
import DiscoverySidebar from "./DiscoverySidebar";
import DiscoveryFilters from "./DiscoveryFilters";
import { DiscoveryDataLayer } from "./DiscoveryDataLayer";
import ProjectHeader from "./ProjectHeader";
import {
  getDiscoveryDimensions,
  getDiscoveryStats,
  getDiscoveryLocations,
  DISCOVERY_DOMAIN_ALL_DATA,
  DISCOVERY_DOMAIN_MY_DATA,
  DISCOVERY_DOMAIN_PUBLIC,
} from "./discovery_api";
import NoResultsBanner from "./NoResultsBanner";
import MapPreviewSidebar from "./mapping/MapPreviewSidebar";

import cs from "./discovery_view.scss";

// Data available
// (A) non-filtered dimensions: for filter options
// (B) filtered dimensions: for sidebar
// (C) filtered stats
// (D) synchronous data: for projects and visualization tables
// (E) asynchronous data: for samples, triggered on demand by infinite table
// Data workflow
// * Initial load:
//   - load (A) non-filtered dimensions, (C) filtered stats and (D) synchronous table data
//   * if filter or project is set
//     - load (B) filtered dimensions and (C) filtered stats
// * On filter change:
//   - load (B) filtered dimensions, (C) filtered stats
//     * if project not set
//       load (D) synchronous table data
// * On project selected
//   - load (A) non-filtered dimensions, (B) filtered dimensions and (C) filtered stats
//     (synchronous data not needed for now because we do not show projects and visualizations)
class DiscoveryView extends React.Component {
  constructor(props) {
    super(props);

    const { projectId } = this.props;

    this.urlParser = new UrlQueryParser({
      sampleActiveColumns: "object",
      filters: "object",
      projectId: "number",
      showFilters: "boolean",
      showStats: "boolean",
    });

    const urlState = this.urlParser.parse(location.search);
    let sessionState = this.loadState(sessionStorage, "DiscoveryViewOptions");
    let localState = this.loadState(localStorage, "DiscoveryViewOptions");

    // values are copied from left to right to the first argument (last arguments override previous)
    this.state = Object.assign(
      {
        currentDisplay: "table",
        currentTab:
          projectId || this.props.domain === "all_data"
            ? "samples"
            : "projects",
        filteredProjectDimensions: [],
        filteredSampleDimensions: [],
        filteredProjectStats: {},
        filteredSampleStats: {},
        filters: {},
        loadingDimensions: true,
        loadingLocations: true,
        loadingStats: true,
        mapLevel: "country",
        mapLocationData: {},
        mapPreviewedLocationId: null,
        mapSidebarProjectDimensions: [],
        mapSidebarSampleDimensions: [],
        mapSidebarSampleStats: {},
        mapSidebarTab: "summary",
        project: null,
        projectDimensions: [],
        projectId: projectId,
        rawMapLocationData: {},
        sampleActiveColumns: undefined,
        sampleDimensions: [],
        search: null,
        selectedSampleIds: new Set(),
        showFilters: true,
        showStats: true,
      },
      localState,
      sessionState,
      urlState
    );
    this.dataLayer = new DiscoveryDataLayer(this.props.domain);
    const conditions = this.getConditions();
    this.samples = this.dataLayer.samples.createView({ conditions });
    this.projects = this.dataLayer.projects.createView({
      conditions,
      onViewChange: this.refreshProjectStats,
    });
    this.visualizations = this.dataLayer.visualizations.createView({
      conditions,
    });
    this.mapPreviewProjects = this.projects;
    this.mapPreviewSamples = this.samples;

    // hold references to the views to allow resetting the tables
    this.projectsView = null;
    this.samplesView = null;
    this.mapPreviewSidebar = null;

    // preload first pages
    const pageSize = 50;
    const firstPageArgs = { startIndex: 0, stopIndex: pageSize - 1 };
    this.projects.handleLoadObjectRows(firstPageArgs);
    this.samples.handleLoadObjectRows(firstPageArgs);
    this.visualizations.handleLoadObjectRows(firstPageArgs);

    this.updateBrowsingHistory("replace");
  }

  async componentDidMount() {
    this.initialLoad();
    this.checkPublicSamples();

    window.onpopstate = () => {
      this.setState(history.state, () => {
        this.resetData({
          callback: this.initialLoad,
        });
      });
    };
  }

  loadState = (store, key) => {
    try {
      return JSON.parse(store.getItem(key)) || {};
    } catch (e) {
      // Avoid possible bad transient state related crash
      // eslint-disable-next-line no-console
      console.warn(`Bad state: ${e}`);
    }
    return {};
  };

  updateBrowsingHistory = (action = "push") => {
    const { domain } = this.props;

    const localFields = [
      "currentTab",
      "sampleActiveColumns",
      "showFilters",
      "showStats",
    ];
    const sessionFields = concat(localFields, [
      "currentDisplay",
      "mapSidebarTab",
    ]);
    const urlFields = concat(sessionFields, ["filters", "projectId", "search"]);
    const stateFields = concat(urlFields, ["project"]);

    const localState = pick(localFields, this.state);
    const sessionState = pick(sessionFields, this.state);
    const urlState = pick(urlFields, this.state);
    const historyState = pick(stateFields, this.state);

    // Saving on URL enables sharing current view with other users
    let urlQuery = this.urlParser.stringify(urlState);
    if (urlQuery) {
      urlQuery = `?${urlQuery}`;
    }

    // History state may include some small fields that enable direct loading of previous pages
    // from browser history without having to request those fields from server (e.g. project)
    if (action === "push") {
      history.pushState(
        historyState,
        `DiscoveryView:${domain}`,
        `/${domain}${urlQuery}`
      );
    } else {
      history.replaceState(
        historyState,
        `DiscoveryView:${domain}`,
        `/${domain}${urlQuery}`
      );
    }

    // We want to persist all options when user navigates to other pages within the same session
    sessionStorage.setItem(
      "DiscoveryViewOptions",
      JSON.stringify(sessionState)
    );

    // We want to persist some options when user returns to the page on a different session
    localStorage.setItem("DiscoveryViewOptions", JSON.stringify(localState));
  };

  preparedFilters = () => {
    const { filters } = this.state;
    let preparedFilters = mapKeys(replace("Selected", ""), filters);

    // Time is an exception: we translate values into date ranges
    if (preparedFilters.time) {
      const startDate = {
        "1_week": () => moment().subtract(7, "days"),
        "1_month": () => moment().subtract(1, "months"),
        "3_month": () => moment().subtract(3, "months"),
        "6_month": () => moment().subtract(6, "months"),
        "1_year": () => moment().subtract(1, "years"),
      };

      preparedFilters.time = [
        startDate[preparedFilters.time]().format("YYYYMMDD"),
        moment()
          .add(1, "days")
          .format("YYYYMMDD"),
      ];
    }

    // Taxon is an exception: this filter needs to store complete option, so need to convert to values only
    if (preparedFilters.taxon && preparedFilters.taxon.length) {
      preparedFilters.taxon = map("value", preparedFilters.taxon);
    }

    return preparedFilters;
  };

  resetData = ({ callback } = {}) => {
    const conditions = this.getConditions();

    this.samples.reset({ conditions });
    this.projects.reset({ conditions });
    this.visualizations.reset({ conditions });
    this.mapPreviewSamples.reset({ conditions });
    this.mapPreviewProjects.reset({ conditions });

    this.setState(
      {
        filteredProjectDimensions: [],
        filteredSampleDimensions: [],
        filteredSampleStats: {},
        loadingDimensions: true,
      },
      () => {
        this.samplesView && this.samplesView.reset();
        this.projectsView && this.projectsView.reset();
        callback && callback();
      }
    );
  };

  initialLoad = () => {
    const { project } = this.state;
    // * Initial load:
    //   - load (A) non-filtered dimensions, (C) filtered stats, (D) filtered locations, and (E) synchronous table data
    this.refreshDimensions();
    this.refreshFilteredStats();
    this.refreshFilteredLocations();
    //   * if filter or project is set
    //     - load (B) filtered dimensions
    (this.getFilterCount() || project) && this.refreshFilteredDimensions();
  };

  resetDataFromFilterChange = () => {
    this.resetData({
      callback: () => {
        // * On filter change:
        //   - load (B) filtered dimensions, (C) filtered stats, (D) filtered locations
        this.refreshFilteredDimensions();
        this.refreshFilteredStats();
        this.refreshFilteredLocations();
      },
    });
  };

  refreshDataFromProjectChange = () => {
    this.resetData({
      callback: () => {
        // * On project selected
        //   - load (A) non-filtered dimensions, (B) filtered dimensions and (C) filtered stats
        //     (synchronous data not needed for now because we do not show projects and visualizations)
        this.refreshDimensions();
        this.refreshFilteredDimensions();
        this.refreshFilteredStats();
        this.refreshFilteredLocations();
      },
    });
  };

  refreshDimensions = async () => {
    const { domain } = this.props;
    const { projectId } = this.state;

    this.setState({
      loadingDimensions: true,
    });

    const {
      projectDimensions,
      sampleDimensions,
    } = await getDiscoveryDimensions({
      domain,
      projectId,
    });

    this.setState({
      projectDimensions,
      sampleDimensions,
      loadingDimensions: false,
    });
  };

  refreshFilteredStats = async () => {
    const { domain } = this.props;
    const { projectId, search } = this.state;

    this.setState({
      loadingStats: true,
    });

    const { sampleStats: filteredSampleStats } = await getDiscoveryStats({
      domain,
      projectId,
      filters: this.preparedFilters(),
      search,
    });

    this.setState({
      filteredSampleStats,
      loadingStats: false,
    });
  };

  refreshProjectStats = () => {
    this.setState({
      filteredProjectStats: {
        count: this.projects.length,
      },
    });
  };

  refreshFilteredDimensions = async () => {
    const { domain } = this.props;
    const { projectId, search } = this.state;

    this.setState({
      loadingDimensions: true,
    });

    const {
      projectDimensions: filteredProjectDimensions,
      sampleDimensions: filteredSampleDimensions,
    } = await getDiscoveryDimensions({
      domain,
      projectId,
      filters: this.preparedFilters(),
      search,
    });

    this.setState({
      filteredProjectDimensions,
      filteredSampleDimensions,
      loadingDimensions: false,
    });
  };

  refreshFilteredLocations = async () => {
    const { domain } = this.props;<commit_msg>[Data Discovery] Bug fix - project name on refresh project page (#2597)<commit_after>import React from "react";
import PropTypes from "prop-types";
import UrlQueryParser from "~/components/utils/UrlQueryParser";
import moment from "moment";
import {
  capitalize,
  clone,
  compact,
  concat,
  escapeRegExp,
  find,
  isEmpty,
  keyBy,
  map,
  mapKeys,
  mapValues,
  merge,
  partition,
  pick,
  replace,
  sumBy,
  union,
  values,
  xor,
  xorBy,
} from "lodash/fp";

import { getSearchSuggestions } from "~/api";
import { logAnalyticsEvent } from "~/api/analytics";
import { get } from "~/api/core";
import { openUrl } from "~utils/links";
import NarrowContainer from "~/components/layout/NarrowContainer";
import { Divider } from "~/components/layout";
import { MAP_CLUSTER_ENABLED_LEVELS } from "~/components/views/discovery/mapping/constants";
import { indexOfMapLevel } from "~/components/views/discovery/mapping/utils";
import { publicSampleNotificationsByProject } from "~/components/views/samples/notifications";

import DiscoveryHeader from "./DiscoveryHeader";
import ProjectsView from "../projects/ProjectsView";
import SamplesView from "../samples/SamplesView";
import VisualizationsView from "../visualizations/VisualizationsView";
import DiscoverySidebar from "./DiscoverySidebar";
import DiscoveryFilters from "./DiscoveryFilters";
import { DiscoveryDataLayer } from "./DiscoveryDataLayer";
import ProjectHeader from "./ProjectHeader";
import {
  getDiscoveryDimensions,
  getDiscoveryStats,
  getDiscoveryLocations,
  DISCOVERY_DOMAIN_ALL_DATA,
  DISCOVERY_DOMAIN_MY_DATA,
  DISCOVERY_DOMAIN_PUBLIC,
} from "./discovery_api";
import NoResultsBanner from "./NoResultsBanner";
import MapPreviewSidebar from "./mapping/MapPreviewSidebar";

import cs from "./discovery_view.scss";

// Data available
// (A) non-filtered dimensions: for filter options
// (B) filtered dimensions: for sidebar
// (C) filtered stats
// (D) synchronous data: for projects and visualization tables
// (E) asynchronous data: for samples, triggered on demand by infinite table
// Data workflow
// * Initial load:
//   - load (A) non-filtered dimensions, (C) filtered stats and (D) synchronous table data
//   * if filter or project is set
//     - load (B) filtered dimensions and (C) filtered stats
// * On filter change:
//   - load (B) filtered dimensions, (C) filtered stats
//     * if project not set
//       load (D) synchronous table data
// * On project selected
//   - load (A) non-filtered dimensions, (B) filtered dimensions and (C) filtered stats
//     (synchronous data not needed for now because we do not show projects and visualizations)
class DiscoveryView extends React.Component {
  constructor(props) {
    super(props);

    const { projectId } = this.props;

    this.urlParser = new UrlQueryParser({
      sampleActiveColumns: "object",
      filters: "object",
      projectId: "number",
      showFilters: "boolean",
      showStats: "boolean",
    });

    const urlState = this.urlParser.parse(location.search);
    let sessionState = this.loadState(sessionStorage, "DiscoveryViewOptions");
    let localState = this.loadState(localStorage, "DiscoveryViewOptions");

    // values are copied from left to right to the first argument (last arguments override previous)
    this.state = Object.assign(
      {
        currentDisplay: "table",
        currentTab:
          projectId || this.props.domain === "all_data"
            ? "samples"
            : "projects",
        filteredProjectDimensions: [],
        filteredSampleDimensions: [],
        filteredProjectStats: {},
        filteredSampleStats: {},
        filters: {},
        loadingDimensions: true,
        loadingLocations: true,
        loadingStats: true,
        mapLevel: "country",
        mapLocationData: {},
        mapPreviewedLocationId: null,
        mapSidebarProjectDimensions: [],
        mapSidebarSampleDimensions: [],
        mapSidebarSampleStats: {},
        mapSidebarTab: "summary",
        project: null,
        projectDimensions: [],
        projectId: projectId,
        rawMapLocationData: {},
        sampleActiveColumns: undefined,
        sampleDimensions: [],
        search: null,
        selectedSampleIds: new Set(),
        showFilters: true,
        showStats: true,
      },
      localState,
      sessionState,
      urlState
    );
    this.dataLayer = new DiscoveryDataLayer(this.props.domain);
    const conditions = this.getConditions();
    this.samples = this.dataLayer.samples.createView({ conditions });
    this.projects = this.dataLayer.projects.createView({
      conditions,
      onViewChange: this.refreshProjectData,
    });
    this.visualizations = this.dataLayer.visualizations.createView({
      conditions,
    });
    this.mapPreviewProjects = this.projects;
    this.mapPreviewSamples = this.samples;

    // hold references to the views to allow resetting the tables
    this.projectsView = null;
    this.samplesView = null;
    this.mapPreviewSidebar = null;

    // preload first pages
    const pageSize = 50;
    const firstPageArgs = { startIndex: 0, stopIndex: pageSize - 1 };
    this.projects.handleLoadObjectRows(firstPageArgs);
    this.samples.handleLoadObjectRows(firstPageArgs);
    this.visualizations.handleLoadObjectRows(firstPageArgs);

    this.updateBrowsingHistory("replace");
  }

  async componentDidMount() {
    this.initialLoad();
    this.checkPublicSamples();

    window.onpopstate = () => {
      this.setState(history.state, () => {
        this.resetData({
          callback: this.initialLoad,
        });
      });
    };
  }

  loadState = (store, key) => {
    try {
      return JSON.parse(store.getItem(key)) || {};
    } catch (e) {
      // Avoid possible bad transient state related crash
      // eslint-disable-next-line no-console
      console.warn(`Bad state: ${e}`);
    }
    return {};
  };

  updateBrowsingHistory = (action = "push") => {
    const { domain } = this.props;
    const { projectId } = this.state;

    const localFields = [
      ...(projectId ? [] : ["currentTab"]),
      "sampleActiveColumns",
      "showFilters",
      "showStats",
    ];

    const sessionFields = concat(localFields, [
      "currentDisplay",
      "mapSidebarTab",
    ]);
    const urlFields = concat(sessionFields, ["filters", "projectId", "search"]);
    const stateFields = concat(urlFields, ["project"]);

    const localState = pick(localFields, this.state);
    const sessionState = pick(sessionFields, this.state);
    const urlState = pick(urlFields, this.state);
    const historyState = pick(stateFields, this.state);

    // Saving on URL enables sharing current view with other users
    let urlQuery = this.urlParser.stringify(urlState);
    if (urlQuery) {
      urlQuery = `?${urlQuery}`;
    }

    // History state may include some small fields that enable direct loading of previous pages
    // from browser history without having to request those fields from server (e.g. project)
    if (action === "push") {
      history.pushState(
        historyState,
        `DiscoveryView:${domain}`,
        `/${domain}${urlQuery}`
      );
    } else {
      history.replaceState(
        historyState,
        `DiscoveryView:${domain}`,
        `/${domain}${urlQuery}`
      );
    }

    // We want to persist all options when user navigates to other pages within the same session
    sessionStorage.setItem(
      "DiscoveryViewOptions",
      JSON.stringify(sessionState)
    );

    // We want to persist some options when user returns to the page on a different session
    localStorage.setItem("DiscoveryViewOptions", JSON.stringify(localState));
  };

  preparedFilters = () => {
    const { filters } = this.state;
    let preparedFilters = mapKeys(replace("Selected", ""), filters);

    // Time is an exception: we translate values into date ranges
    if (preparedFilters.time) {
      const startDate = {
        "1_week": () => moment().subtract(7, "days"),
        "1_month": () => moment().subtract(1, "months"),
        "3_month": () => moment().subtract(3, "months"),
        "6_month": () => moment().subtract(6, "months"),
        "1_year": () => moment().subtract(1, "years"),
      };

      preparedFilters.time = [
        startDate[preparedFilters.time]().format("YYYYMMDD"),
        moment()
          .add(1, "days")
          .format("YYYYMMDD"),
      ];
    }

    // Taxon is an exception: this filter needs to store complete option, so need to convert to values only
    if (preparedFilters.taxon && preparedFilters.taxon.length) {
      preparedFilters.taxon = map("value", preparedFilters.taxon);
    }

    return preparedFilters;
  };

  resetData = ({ callback } = {}) => {
    const conditions = this.getConditions();

    this.samples.reset({ conditions });
    this.projects.reset({ conditions });
    this.visualizations.reset({ conditions });
    this.mapPreviewSamples.reset({ conditions });
    this.mapPreviewProjects.reset({ conditions });

    this.setState(
      {
        filteredProjectDimensions: [],
        filteredSampleDimensions: [],
        filteredSampleStats: {},
        loadingDimensions: true,
      },
      () => {
        this.samplesView && this.samplesView.reset();
        this.projectsView && this.projectsView.reset();
        callback && callback();
      }
    );
  };

  initialLoad = () => {
    const { project } = this.state;
    // * Initial load:
    //   - load (A) non-filtered dimensions, (C) filtered stats, (D) filtered locations, and (E) synchronous table data
    this.refreshDimensions();
    this.refreshFilteredStats();
    this.refreshFilteredLocations();
    //   * if filter or project is set
    //     - load (B) filtered dimensions
    (this.getFilterCount() || project) && this.refreshFilteredDimensions();
  };

  resetDataFromFilterChange = () => {
    this.resetData({
      callback: () => {
        // * On filter change:
        //   - load (B) filtered dimensions, (C) filtered stats, (D) filtered locations
        this.refreshFilteredDimensions();
        this.refreshFilteredStats();
        this.refreshFilteredLocations();
      },
    });
  };

  refreshDataFromProjectChange = () => {
    this.resetData({
      callback: () => {
        // * On project selected
        //   - load (A) non-filtered dimensions, (B) filtered dimensions and (C) filtered stats
        //     (synchronous data not needed for now because we do not show projects and visualizations)
        this.refreshDimensions();
        this.refreshFilteredDimensions();
        this.refreshFilteredStats();
        this.refreshFilteredLocations();
      },
    });
  };

  refreshDimensions = async () => {
    const { domain } = this.props;
    const { projectId } = this.state;

    this.setState({
      loadingDimensions: true,
    });

    const {
      projectDimensions,
      sampleDimensions,
    } = await getDiscoveryDimensions({
      domain,
      projectId,
    });

    this.setState({
      projectDimensions,
      sampleDimensions,
      loadingDimensions: false,
    });
  };

  refreshFilteredStats = async () => {
    const { domain } = this.props;
    const { projectId, search } = this.state;

    this.setState({
      loadingStats: true,
    });

    const { sampleStats: filteredSampleStats } = await getDiscoveryStats({
      domain,
      projectId,
      filters: this.preparedFilters(),
      search,
    });

    this.setState({
      filteredSampleStats,
      loadingStats: false,
    });
  };

  refreshProjectStats = () => {
    this.setState({
      filteredProjectStats: {
        count: this.projects.length,
      },
    });
  };

  refreshProject = () => {
    const { projectId } = this.state;

    this.setState({
      project: this.projects.get(projectId),
    });
  };

  refreshProjectData = () => {
    this.refreshProject();
    this.refreshProjectStats();
  };

  refreshFilteredDimensions = async () => {
    const { domain } = this.props;
    const { projectId, search } = this.state;

    this.setState({
      loadingDimensions: true,
    });

    const {
      projectDimensions: filteredProjectDimensions,
      sampleDimensions: filteredSampleDimensions,
    } = await getDiscoveryDimensions({
      domain,
      projectId,
      filters: this.preparedFilters(),
      search,
    });

    this.setState({
      filteredProjectDimensions,
      filteredSampleDimensions,
      loadingDimensions: false,
    });
  };

  refreshFilteredLocations = async () => {
    const { domain } = this.props;
{'fraction_non_alphanumeric': 0.12320453166093466, 'fraction_numerical': 0.0013756827837345742, 'mean_word_length': 2.873374079297916, 'pattern_counts': {'":': 10, '<': 3, '<?xml version=': 0, '>': 49, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3312979', 'n_tokens_mistral': 6997, 'n_tokens_neox': 6846, 'n_words': 1693}