Browser.tsx 10.4 KB
Newer Older
1
import React, { FunctionComponent, useEffect, useState } from 'react';
2
import { withRouter, RouteComponentProps } from 'react-router-dom';
3
import { connect } from 'react-redux';
4
import { replace, Replace } from 'connected-react-router';
5
6
import { useSpring, animated } from 'react-spring';
import { Link } from 'react-router-dom';
Andrey Azov's avatar
Andrey Azov committed
7
8
9
10
import find from 'lodash/find';
import isEqual from 'lodash/isEqual';
import upperFirst from 'lodash/upperFirst';

11
12
13
14
import BrowserBar from './browser-bar/BrowserBar';
import BrowserImage from './browser-image/BrowserImage';
import BrowserNavBar from './browser-nav/BrowserNavBar';
import TrackPanel from './track-panel/TrackPanel';
15
import AppBar from 'src/shared/components/app-bar/AppBar';
16

Andrey Azov's avatar
Andrey Azov committed
17
import { RootState } from 'src/store';
Imran Salam's avatar
Imran Salam committed
18
import { ChrLocation, ChrLocations } from './browserState';
19
import {
20
  changeBrowserLocation,
Andrey Azov's avatar
Andrey Azov committed
21
  changeFocusObject,
Andrey Azov's avatar
Andrey Azov committed
22
23
  setDataFromUrlAndSave,
  ParsedUrlPayload
24
} from './browserActions';
25
import {
26
  getBrowserNavOpened,
27
  getChrLocation,
Dan Sheppard's avatar
Dan Sheppard committed
28
  getGenomeSelectorActive,
29
30
31
  getBrowserActivated,
  getBrowserActiveGenomeId,
  getBrowserQueryParams,
Andrey Azov's avatar
Andrey Azov committed
32
33
34
  getBrowserActiveEnsObjectId,
  getBrowserActiveEnsObjectIds,
  getAllChrLocations
35
} from './browserSelectors';
36
import { getLaunchbarExpanded } from 'src/header/headerSelectors';
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
41
42
import { getEnabledCommittedSpecies } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import { CommittedItem } from 'src/content/app/species-selector/types/species-search';
import { getExampleEnsObjects } from 'src/ens-object/ensObjectSelectors';
Andrey Azov's avatar
Andrey Azov committed
43
import { EnsObject } from 'src/ens-object/ensObjectTypes';
44
import analyticsTracking from 'src/services/analytics-service';
45

Andrey Azov's avatar
Andrey Azov committed
46
import { fetchGenomeData } from 'src/genome/genomeActions';
Imran Salam's avatar
Imran Salam committed
47
48
49
50
51
import {
  changeDrawerView,
  closeDrawer,
  toggleDrawer
} from './drawer/drawerActions';
52

53
54
55
56
57
58
import browserStorageService from './browser-storage-service';
import { TrackStates } from './track-panel/trackPanelConfig';
import { AppName } from 'src/global/globalConfig';

import * as urlFor from 'src/shared/helpers/urlHelper';

59
import styles from './Browser.scss';
60

61
62
63
import 'static/browser/browser.js';

type StateProps = {
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;
72
  genomeSelectorActive: boolean;
Imran Salam's avatar
Imran Salam committed
73
74
  isDrawerOpened: boolean;
  isTrackPanelOpened: boolean;
75
  launchbarExpanded: boolean;
Andrey Azov's avatar
Andrey Azov committed
76
  exampleEnsObjects: EnsObject[];
77
  committedSpecies: CommittedItem[];
78
79
80
};

type DispatchProps = {
Andrey Azov's avatar
Andrey Azov committed
81
  changeBrowserLocation: (genomeId: string, chrLocation: ChrLocation) => void;
Andrey Azov's avatar
Andrey Azov committed
82
  changeFocusObject: (objectId: string) => void;
Imran Salam's avatar
Imran Salam committed
83
84
  changeDrawerView: (drawerView: string) => void;
  closeDrawer: () => void;
Andrey Azov's avatar
Andrey Azov committed
85
  fetchGenomeData: (genomeId: string) => void;
86
  replace: Replace;
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
92
type OwnProps = {};

93
type MatchParams = {
94
  genomeId: string;
95
96
97
98
99
100
};

type BrowserProps = RouteComponentProps<MatchParams> &
  StateProps &
  DispatchProps &
  OwnProps;
101

102
103
104
export const Browser: FunctionComponent<BrowserProps> = (
  props: BrowserProps
) => {
105
106
107
  const [trackStatesFromStorage, setTrackStatesFromStorage] = useState<
    TrackStates
  >({});
Andrey Azov's avatar
Andrey Azov committed
108

Imran Salam's avatar
Imran Salam committed
109
110
  const { isDrawerOpened, closeDrawer } = props;

Andrey Azov's avatar
Andrey Azov committed
111
112
113
114
115
116
117
  const setDataFromUrl = () => {
    const { genomeId = null } = props.match.params;
    const { focus = null, location = null } = props.browserQueryParams;
    const chrLocation = location ? getChrLocationFromStr(location) : null;

    if (
      !genomeId ||
118
      (genomeId === props.activeGenomeId &&
Andrey Azov's avatar
Andrey Azov committed
119
120
121
122
123
        focus === props.activeEnsObjectId &&
        isEqual(chrLocation, props.chrLocation))
    ) {
      return;
    }
124

Andrey Azov's avatar
Andrey Azov committed
125
126
127
128
129
130
131
132
    const payload = {
      activeGenomeId: genomeId,
      activeEnsObjectId: focus || null,
      chrLocation
    };

    props.setDataFromUrlAndSave(payload);

Andrey Azov's avatar
Andrey Azov committed
133
134
135
136
137
    if (chrLocation) {
      dispatchBrowserLocation(genomeId, chrLocation);
    } else if (focus) {
      props.changeFocusObject(focus);
    }
Andrey Azov's avatar
Andrey Azov committed
138
139
140
141
142
143
  };

  const dispatchBrowserLocation = (
    genomeId: string,
    chrLocation: ChrLocation
  ) => {
Andrey Azov's avatar
Andrey Azov committed
144
    props.changeBrowserLocation(genomeId, chrLocation);
145
146
  };

147
  const changeSelectedSpecies = (genomeId: string) => {
Andrey Azov's avatar
Andrey Azov committed
148
149
150
151
152
153
154
155
156
157
158
    const { allChrLocations, allActiveEnsObjectIds } = props;
    const chrLocation = allChrLocations[genomeId];
    const activeEnsObjectId = allActiveEnsObjectIds[genomeId];

    const params = {
      genomeId,
      focus: activeEnsObjectId,
      location: chrLocation ? getChrLocationStr(chrLocation) : null
    };

    props.replace(urlFor.browser(params));
159
160
  };

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

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

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

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

Andrey Azov's avatar
Andrey Azov committed
209
210
    if (props.browserActivated && genomeId && chrLocation) {
      dispatchBrowserLocation(genomeId, chrLocation);
Dan Sheppard's avatar
Dan Sheppard committed
211
    }
212
  }, [props.browserActivated]);
Dan Sheppard's avatar
Dan Sheppard committed
213

214
  const closeTrack = () => {
Imran Salam's avatar
Imran Salam committed
215
    if (!isDrawerOpened) {
216
217
      return;
    }
Imran Salam's avatar
Imran Salam committed
218
219

    closeDrawer();
220
  };
221

222
223
224
225
226
227
228
  const [trackAnimation, setTrackAnimation] = useSpring(() => ({
    config: { tension: 280, friction: 45 },
    height: '100%',
    width: 'calc(-36px + 100vw )'
  }));

  const getBrowserWidth = (): string => {
Imran Salam's avatar
Imran Salam committed
229
230
    if (isDrawerOpened) {
      return 'calc(41px + 0vw)'; // this format must be used for the react-spring animation to function properly
231
    }
Imran Salam's avatar
Imran Salam committed
232
    return props.isTrackPanelOpened
233
234
235
236
237
238
239
240
      ? 'calc(-356px + 100vw)'
      : 'calc(-36px + 100vw)';
  };

  useEffect(() => {
    setTrackAnimation({
      width: getBrowserWidth()
    });
Imran Salam's avatar
Imran Salam committed
241
  }, [isDrawerOpened, props.isTrackPanelOpened]);
242
243
244
245
246

  const getHeightClass = (launchbarExpanded: boolean): string => {
    return launchbarExpanded ? styles.shorter : styles.taller;
  };

247
  const browserBar = (
Imran Salam's avatar
Imran Salam committed
248
249
250
    <BrowserBar dispatchBrowserLocation={dispatchBrowserLocation} />
  );

251
252
253
  const shouldShowNavBar =
    props.browserActivated && props.browserNavOpened && !isDrawerOpened;

Andrey Azov's avatar
Andrey Azov committed
254
  return props.activeGenomeId ? (
255
256
257
258
259
260
261
    <>
      <AppBar
        currentAppName={AppName.GENOME_BROWSER}
        activeGenomeId={props.activeGenomeId}
        onTabSelect={changeSelectedSpecies}
      />

Andrey Azov's avatar
Andrey Azov committed
262
      {!props.browserQueryParams.focus && (
263
        <section className={styles.browser}>
264
          {browserBar}
Andrey Azov's avatar
Andrey Azov committed
265
          <ExampleObjectLinks {...props} />
266
267
        </section>
      )}
Andrey Azov's avatar
Andrey Azov committed
268
      {props.browserQueryParams.focus && (
269
        <section className={styles.browser}>
270
          {browserBar}
271
272
273
274
275
276
277
278
279
280
          {props.genomeSelectorActive && (
            <div className={styles.browserOverlay} />
          )}
          <div
            className={`${styles.browserInnerWrapper} ${getHeightClass(
              props.launchbarExpanded
            )}`}
          >
            <animated.div style={trackAnimation}>
              <div className={styles.browserImageWrapper} onClick={closeTrack}>
281
282
                {shouldShowNavBar && <BrowserNavBar />}
                <BrowserImage trackStates={trackStatesFromStorage} />
283
284
              </div>
            </animated.div>
Andrey Azov's avatar
Andrey Azov committed
285
            <TrackPanel />
286
          </div>
287
288
289
        </section>
      )}
    </>
Andrey Azov's avatar
Andrey Azov committed
290
291
292
293
294
295
296
297
298
299
300
  ) : null;
};

const ExampleObjectLinks = (props: BrowserProps) => {
  const { activeGenomeId } = props;
  if (!activeGenomeId) {
    return null;
  }
  const links = props.exampleEnsObjects.map((exampleObject: EnsObject) => {
    const path = urlFor.browser({
      genomeId: activeGenomeId,
Andrey Azov's avatar
Andrey Azov committed
301
      focus: exampleObject.object_id
Andrey Azov's avatar
Andrey Azov committed
302
303
304
    });

    return (
305
      <div key={exampleObject.object_id} className={styles.exampleLink}>
Andrey Azov's avatar
Andrey Azov committed
306
307
308
309
310
311
312
313
314
315
316
        <Link to={path}>
          <span className={styles.objectType}>
            {upperFirst(exampleObject.object_type)}
          </span>
          <span className={styles.objectLabel}>{exampleObject.label}</span>
        </Link>
      </div>
    );
  });

  return <div className={styles.exampleLinks}>{links}</div>;
317
};
318

319
const mapStateToProps = (state: RootState): StateProps => ({
320
321
  activeGenomeId: getBrowserActiveGenomeId(state),
  activeEnsObjectId: getBrowserActiveEnsObjectId(state),
Andrey Azov's avatar
Andrey Azov committed
322
  allActiveEnsObjectIds: getBrowserActiveEnsObjectIds(state),
Imran Salam's avatar
Imran Salam committed
323
  allChrLocations: getAllChrLocations(state),
324
  browserActivated: getBrowserActivated(state),
325
  browserNavOpened: getBrowserNavOpened(state),
326
  browserQueryParams: getBrowserQueryParams(state),
327
  chrLocation: getChrLocation(state),
328
  genomeSelectorActive: getGenomeSelectorActive(state),
Imran Salam's avatar
Imran Salam committed
329
330
  isDrawerOpened: getIsDrawerOpened(state),
  isTrackPanelOpened: getIsTrackPanelOpened(state),
331
332
333
  launchbarExpanded: getLaunchbarExpanded(state),
  exampleEnsObjects: getExampleEnsObjects(state),
  committedSpecies: getEnabledCommittedSpecies(state)
334
335
});

336
const mapDispatchToProps: DispatchProps = {
337
  changeBrowserLocation,
Andrey Azov's avatar
Andrey Azov committed
338
  changeFocusObject,
Imran Salam's avatar
Imran Salam committed
339
340
  changeDrawerView,
  closeDrawer,
Andrey Azov's avatar
Andrey Azov committed
341
  fetchGenomeData,
342
  replace,
343
  toggleDrawer,
Andrey Azov's avatar
Andrey Azov committed
344
  setDataFromUrlAndSave
345
346
};

347
348
349
350
351
352
export default withRouter(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(Browser)
);