Browser.tsx 10.1 KB
Newer Older
Andrey Azov's avatar
Andrey Azov committed
1
import React, { useEffect, useState, useCallback } from 'react';
2
import { useLocation, useParams } from 'react-router-dom';
3
import { connect } from 'react-redux';
4
import { replace, Replace } from 'connected-react-router';
5
import { Link } from 'react-router-dom';
Andrey Azov's avatar
Andrey Azov committed
6 7 8 9
import find from 'lodash/find';
import isEqual from 'lodash/isEqual';
import upperFirst from 'lodash/upperFirst';

10 11 12 13 14
import analyticsTracking from 'src/services/analytics-service';
import browserStorageService from './browser-storage-service';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { BrowserTrackStates } from './track-panel/trackPanelConfig';
import { BreakpointWidth } from 'src/global/globalConfig';
15

16
import {
17
  changeBrowserLocation,
Andrey Azov's avatar
Andrey Azov committed
18
  changeFocusObject,
Andrey Azov's avatar
Andrey Azov committed
19
  setDataFromUrlAndSave,
20 21
  ParsedUrlPayload,
  restoreBrowserTrackStates
22
} from './browserActions';
23 24 25 26
import { fetchGenomeData } from 'src/shared/state/genome/genomeActions';
import { toggleTrackPanel } from 'src/content/app/browser/track-panel/trackPanelActions';
import { toggleDrawer } from './drawer/drawerActions';

27
import {
28
  getBrowserNavOpened,
29
  getChrLocation,
30 31 32
  getBrowserActivated,
  getBrowserActiveGenomeId,
  getBrowserQueryParams,
Andrey Azov's avatar
Andrey Azov committed
33 34 35
  getBrowserActiveEnsObjectId,
  getBrowserActiveEnsObjectIds,
  getAllChrLocations
36
} from './browserSelectors';
Imran Salam's avatar
Imran Salam committed
37
import { getIsTrackPanelOpened } from './track-panel/trackPanelSelectors';
38
import { getChrLocationFromStr, getChrLocationStr } from './browserHelper';
Imran Salam's avatar
Imran Salam committed
39
import { getIsDrawerOpened } from './drawer/drawerSelectors';
40
import { getEnabledCommittedSpecies } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
41
import { getExampleEnsObjects } from 'src/shared/state/ens-object/ensObjectSelectors';
42
import { getBreakpointWidth } from 'src/global/globalSelectors';
43

44 45 46 47 48 49 50 51 52
import BrowserBar from './browser-bar/BrowserBar';
import BrowserImage from './browser-image/BrowserImage';
import BrowserNavBar from './browser-nav/BrowserNavBar';
import TrackPanel from './track-panel/TrackPanel';
import TrackPanelBar from './track-panel/track-panel-bar/TrackPanelBar';
import TrackPanelTabs from './track-panel/track-panel-tabs/TrackPanelTabs';
import BrowserAppBar from './browser-app-bar/BrowserAppBar';
import Drawer from './drawer/Drawer';
import { StandardAppLayout } from 'src/shared/components/layout';
53

54 55 56 57
import { RootState } from 'src/store';
import { ChrLocation, ChrLocations } from './browserState';
import { CommittedItem } from 'src/content/app/species-selector/types/species-search';
import { EnsObject } from 'src/shared/state/ens-object/ensObjectTypes';
58

59
import 'ensembl-genome-browser';
60

61 62
import styles from './Browser.scss';

63
export type BrowserProps = {
Andrey Azov's avatar
Andrey Azov committed
64 65 66
  activeGenomeId: string | null;
  activeEnsObjectId: string | null;
  allActiveEnsObjectIds: { [genomeId: string]: string };
Imran Salam's avatar
Imran Salam committed
67
  allChrLocations: ChrLocations;
68
  browserActivated: boolean;
69
  browserNavOpened: boolean;
70
  browserQueryParams: { [key: string]: string };
Andrey Azov's avatar
Andrey Azov committed
71
  chrLocation: ChrLocation | null;
Imran Salam's avatar
Imran Salam committed
72 73
  isDrawerOpened: boolean;
  isTrackPanelOpened: boolean;
Andrey Azov's avatar
Andrey Azov committed
74
  exampleEnsObjects: EnsObject[];
75
  committedSpecies: CommittedItem[];
76
  viewportWidth: BreakpointWidth;
77 78 79 80 81
  changeBrowserLocation: (locationData: {
    genomeId: string;
    ensObjectId: string | null;
    chrLocation: ChrLocation;
  }) => void;
Andrey Azov's avatar
Andrey Azov committed
82
  changeFocusObject: (objectId: string) => void;
83
  restoreBrowserTrackStates: () => void;
Andrey Azov's avatar
Andrey Azov committed
84
  fetchGenomeData: (genomeId: string) => void;
85
  replace: Replace;
86
  toggleTrackPanel: (isOpen: boolean) => void;
Imran Salam's avatar
Imran Salam committed
87
  toggleDrawer: (isDrawerOpened: boolean) => void;
Andrey Azov's avatar
Andrey Azov committed
88
  setDataFromUrlAndSave: (payload: ParsedUrlPayload) => void;
89
};
90

91
export const Browser = (props: BrowserProps) => {
92
  const [, setTrackStatesFromStorage] = useState<BrowserTrackStates>({});
Andrey Azov's avatar
Andrey Azov committed
93

94
  const { isDrawerOpened } = props;
95 96
  const params: { [key: string]: string } = useParams();
  const location = useLocation();
Imran Salam's avatar
Imran Salam committed
97

Andrey Azov's avatar
Andrey Azov committed
98
  const setDataFromUrl = () => {
99
    const { genomeId } = params;
Andrey Azov's avatar
Andrey Azov committed
100 101 102 103 104
    const { focus = null, location = null } = props.browserQueryParams;
    const chrLocation = location ? getChrLocationFromStr(location) : null;

    if (
      !genomeId ||
105
      (genomeId === props.activeGenomeId &&
Andrey Azov's avatar
Andrey Azov committed
106 107 108 109 110
        focus === props.activeEnsObjectId &&
        isEqual(chrLocation, props.chrLocation))
    ) {
      return;
    }
111

Andrey Azov's avatar
Andrey Azov committed
112 113 114 115 116 117
    const payload = {
      activeGenomeId: genomeId,
      activeEnsObjectId: focus || null,
      chrLocation
    };

118 119 120 121 122 123 124 125
    if (focus && !chrLocation) {
      /*
       changeFocusObject needs to be called before setDataFromUrlAndSave
       in order to prevent creating an previouslyViewedObject entry
       for the focus object that is viewed first.
       */
      props.changeFocusObject(focus);
    } else if (focus && chrLocation) {
Andrey Azov's avatar
Andrey Azov committed
126
      props.changeFocusObject(focus);
127 128 129 130 131 132 133 134 135 136 137
      props.changeBrowserLocation({
        genomeId,
        ensObjectId: focus,
        chrLocation
      });
    } else if (chrLocation) {
      props.changeBrowserLocation({
        genomeId,
        ensObjectId: focus,
        chrLocation
      });
Andrey Azov's avatar
Andrey Azov committed
138
    }
Andrey Azov's avatar
Andrey Azov committed
139

140
    props.setDataFromUrlAndSave(payload);
141 142
  };

143 144 145 146 147
  const changeSelectedSpecies = useCallback(
    (genomeId: string) => {
      const { allChrLocations, allActiveEnsObjectIds } = props;
      const chrLocation = allChrLocations[genomeId];
      const activeEnsObjectId = allActiveEnsObjectIds[genomeId];
Andrey Azov's avatar
Andrey Azov committed
148

149 150 151 152 153
      const params = {
        genomeId,
        focus: activeEnsObjectId,
        location: chrLocation ? getChrLocationStr(chrLocation) : null
      };
Andrey Azov's avatar
Andrey Azov committed
154

155 156 157 158
      props.replace(urlFor.browser(params));
    },
    [props.allChrLocations, props.allActiveEnsObjectIds]
  );
159

Andrey Azov's avatar
Andrey Azov committed
160
  // handle url changes
161
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
162
    // handle navigation to /app/browser
163
    if (!params.genomeId) {
Andrey Azov's avatar
Andrey Azov committed
164 165
      // select either the species that the user viewed during the previous visit,
      // of the first selected species
166 167
      const { activeGenomeId, committedSpecies } = props;
      if (
Andrey Azov's avatar
Andrey Azov committed
168
        activeGenomeId &&
169 170 171 172 173 174 175
        find(
          committedSpecies,
          ({ genome_id }: CommittedItem) => genome_id === activeGenomeId
        )
      ) {
        changeSelectedSpecies(activeGenomeId);
      } else {
176 177 178
        if (committedSpecies[0]) {
          changeSelectedSpecies(committedSpecies[0].genome_id);
        }
179
      }
Andrey Azov's avatar
Andrey Azov committed
180 181 182
    } else {
      // handle navigation to /app/browser/:genomeId?focus=:focus&location=:location
      setDataFromUrl();
183
    }
184
  }, [params.genomeId, location.search]);
185

186
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
187
    const { activeGenomeId, fetchGenomeData } = props;
188 189 190 191 192
    if (!activeGenomeId) {
      return;
    }
    fetchGenomeData(activeGenomeId);
    analyticsTracking.setSpeciesDimension(activeGenomeId);
Andrey Azov's avatar
Andrey Azov committed
193
  }, [props.activeGenomeId]);
194

195
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
196
    setTrackStatesFromStorage(browserStorageService.getTrackStates());
197
    props.restoreBrowserTrackStates();
Andrey Azov's avatar
Andrey Azov committed
198
  }, [props.activeGenomeId, props.activeEnsObjectId]);
199

Dan Sheppard's avatar
Dan Sheppard committed
200
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
201 202 203
    const {
      browserQueryParams: { location }
    } = props;
204
    const { genomeId } = params;
Andrey Azov's avatar
Andrey Azov committed
205
    const chrLocation = location ? getChrLocationFromStr(location) : null;
206

Andrey Azov's avatar
Andrey Azov committed
207
    if (props.browserActivated && genomeId && chrLocation) {
208
      props.changeBrowserLocation({ genomeId, chrLocation, ensObjectId: null });
Dan Sheppard's avatar
Dan Sheppard committed
209
    }
210
  }, [props.browserActivated]);
Dan Sheppard's avatar
Dan Sheppard committed
211

212 213
  const onSidebarToggle = () => {
    props.toggleTrackPanel(!props.isTrackPanelOpened); // FIXME
214
  };
215

216 217
  const toggleDrawer = () => {
    props.toggleDrawer(!props.isDrawerOpened);
218 219
  };

220 221 222
  const shouldShowNavBar =
    props.browserActivated && props.browserNavOpened && !isDrawerOpened;

223 224 225 226
  if (!props.activeGenomeId) {
    return null;
  }

227
  const mainContent = (
228
    <>
229 230 231 232 233 234 235
      {shouldShowNavBar && <BrowserNavBar />}
      <BrowserImage />
    </>
  );

  return (
    <div className={styles.browserInnerWrapper}>
236
      <BrowserAppBar onSpeciesSelect={changeSelectedSpecies} />
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
      {props.browserQueryParams.focus ? (
        <StandardAppLayout
          mainContent={mainContent}
          sidebarContent={<TrackPanel />}
          sidebarNavigation={<TrackPanelTabs />}
          sidebarToolstripContent={<TrackPanelBar />}
          onSidebarToggle={onSidebarToggle}
          topbarContent={<BrowserBar />}
          isSidebarOpen={props.isTrackPanelOpened}
          isDrawerOpen={props.isDrawerOpened}
          drawerContent={<Drawer />}
          onDrawerClose={toggleDrawer}
          viewportWidth={props.viewportWidth}
        />
      ) : (
        <ExampleObjectLinks {...props} />
253
      )}
254
    </div>
255
  );
Andrey Azov's avatar
Andrey Azov committed
256 257
};

258
export const ExampleObjectLinks = (props: BrowserProps) => {
Andrey Azov's avatar
Andrey Azov committed
259
  const { activeGenomeId } = props;
260

Andrey Azov's avatar
Andrey Azov committed
261 262 263
  if (!activeGenomeId) {
    return null;
  }
264

Andrey Azov's avatar
Andrey Azov committed
265 266 267
  const links = props.exampleEnsObjects.map((exampleObject: EnsObject) => {
    const path = urlFor.browser({
      genomeId: activeGenomeId,
Andrey Azov's avatar
Andrey Azov committed
268
      focus: exampleObject.object_id
Andrey Azov's avatar
Andrey Azov committed
269 270 271
    });

    return (
272
      <div key={exampleObject.object_id} className={styles.exampleLink}>
Andrey Azov's avatar
Andrey Azov committed
273 274 275 276 277 278 279 280 281 282
        <Link to={path}>
          <span className={styles.objectType}>
            {upperFirst(exampleObject.object_type)}
          </span>
          <span className={styles.objectLabel}>{exampleObject.label}</span>
        </Link>
      </div>
    );
  });

283 284
  return (
    <div>
285
      <div className={styles.exampleLinks__emptyTopbar} />
286 287 288
      <div className={styles.exampleLinks}>{links}</div>
    </div>
  );
289
};
290

Andrey Azov's avatar
Andrey Azov committed
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
const mapStateToProps = (state: RootState) => {
  const activeGenomeId = getBrowserActiveGenomeId(state);
  return {
    activeGenomeId,
    activeEnsObjectId: getBrowserActiveEnsObjectId(state),
    allActiveEnsObjectIds: getBrowserActiveEnsObjectIds(state),
    allChrLocations: getAllChrLocations(state),
    browserActivated: getBrowserActivated(state),
    browserNavOpened: getBrowserNavOpened(state),
    browserQueryParams: getBrowserQueryParams(state),
    chrLocation: getChrLocation(state),
    isDrawerOpened: getIsDrawerOpened(state),
    isTrackPanelOpened: getIsTrackPanelOpened(state),
    exampleEnsObjects: activeGenomeId
      ? getExampleEnsObjects(state, activeGenomeId)
      : [],
    committedSpecies: getEnabledCommittedSpecies(state),
    viewportWidth: getBreakpointWidth(state)
  };
};
311

312
const mapDispatchToProps = {
313
  changeBrowserLocation,
Andrey Azov's avatar
Andrey Azov committed
314
  changeFocusObject,
Andrey Azov's avatar
Andrey Azov committed
315
  fetchGenomeData,
316
  replace,
317
  toggleDrawer,
318
  setDataFromUrlAndSave,
319 320
  restoreBrowserTrackStates,
  toggleTrackPanel
321 322
};

323
export default connect(mapStateToProps, mapDispatchToProps)(Browser);