Browser.tsx 10.2 KB
Newer Older
1
2
import React, { useEffect, useState } from 'react';
import { useLocation, useParams } 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 BrowserAppBar from './browser-app-bar/BrowserAppBar';
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
  setDataFromUrlAndSave,
23
24
  ParsedUrlPayload,
  restoreBrowserTrackStates
25
} from './browserActions';
26
import {
27
  getBrowserNavOpened,
28
  getChrLocation,
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
import browserStorageService from './browser-storage-service';
54
import { BrowserTrackStates } from './track-panel/trackPanelConfig';
55
56
import * as urlFor from 'src/shared/helpers/urlHelper';

57
import styles from './Browser.scss';
58

59
60
import 'static/browser/browser.js';

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

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

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

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

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

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

119
120
121
122
123
124
125
126
    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
127
      props.changeFocusObject(focus);
128
129
130
131
132
133
134
135
136
137
138
      props.changeBrowserLocation({
        genomeId,
        ensObjectId: focus,
        chrLocation
      });
    } else if (chrLocation) {
      props.changeBrowserLocation({
        genomeId,
        ensObjectId: focus,
        chrLocation
      });
Andrey Azov's avatar
Andrey Azov committed
139
    }
Andrey Azov's avatar
Andrey Azov committed
140

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

144
  const changeSelectedSpecies = (genomeId: string) => {
Andrey Azov's avatar
Andrey Azov committed
145
146
147
148
149
150
151
152
153
154
155
    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));
156
157
  };

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

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

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

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

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

210
  const closeTrack = () => {
Imran Salam's avatar
Imran Salam committed
211
    if (!isDrawerOpened) {
212
213
      return;
    }
Imran Salam's avatar
Imran Salam committed
214
215

    closeDrawer();
216
  };
217

218
219
220
221
222
223
224
  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
225
226
    if (isDrawerOpened) {
      return 'calc(41px + 0vw)'; // this format must be used for the react-spring animation to function properly
227
    }
Imran Salam's avatar
Imran Salam committed
228
    return props.isTrackPanelOpened
229
230
231
232
233
234
235
236
      ? 'calc(-356px + 100vw)'
      : 'calc(-36px + 100vw)';
  };

  useEffect(() => {
    setTrackAnimation({
      width: getBrowserWidth()
    });
Imran Salam's avatar
Imran Salam committed
237
  }, [isDrawerOpened, props.isTrackPanelOpened]);
238
239
240
241
242

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

243
  const browserBar = <BrowserBar />;
Imran Salam's avatar
Imran Salam committed
244

245
246
247
  const shouldShowNavBar =
    props.browserActivated && props.browserNavOpened && !isDrawerOpened;

248
249
250
251
252
  if (!props.activeGenomeId) {
    return null;
  }

  return (
253
    <>
254
      <BrowserAppBar onSpeciesSelect={changeSelectedSpecies} />
Andrey Azov's avatar
Andrey Azov committed
255
      {!props.browserQueryParams.focus && (
256
        <section className={styles.browser}>
257
          {browserBar}
Andrey Azov's avatar
Andrey Azov committed
258
          <ExampleObjectLinks {...props} />
259
260
        </section>
      )}
261
      {!!props.browserQueryParams.focus && (
262
        <section className={styles.browser}>
263
          {browserBar}
264
265
266
267
268
269
270
          <div
            className={`${styles.browserInnerWrapper} ${getHeightClass(
              props.launchbarExpanded
            )}`}
          >
            <animated.div style={trackAnimation}>
              <div className={styles.browserImageWrapper} onClick={closeTrack}>
271
                {shouldShowNavBar && <BrowserNavBar />}
272
                <BrowserImage />
273
274
              </div>
            </animated.div>
Andrey Azov's avatar
Andrey Azov committed
275
            <TrackPanel />
276
          </div>
277
278
279
        </section>
      )}
    </>
280
  );
Andrey Azov's avatar
Andrey Azov committed
281
282
};

283
export const ExampleObjectLinks = (props: BrowserProps) => {
Andrey Azov's avatar
Andrey Azov committed
284
  const { activeGenomeId } = props;
285

Andrey Azov's avatar
Andrey Azov committed
286
287
288
  if (!activeGenomeId) {
    return null;
  }
289

Andrey Azov's avatar
Andrey Azov committed
290
291
292
  const links = props.exampleEnsObjects.map((exampleObject: EnsObject) => {
    const path = urlFor.browser({
      genomeId: activeGenomeId,
Andrey Azov's avatar
Andrey Azov committed
293
      focus: exampleObject.object_id
Andrey Azov's avatar
Andrey Azov committed
294
295
296
    });

    return (
297
      <div key={exampleObject.object_id} className={styles.exampleLink}>
Andrey Azov's avatar
Andrey Azov committed
298
299
300
301
302
303
304
305
306
307
308
        <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>;
309
};
310

311
const mapStateToProps = (state: RootState) => ({
312
313
  activeGenomeId: getBrowserActiveGenomeId(state),
  activeEnsObjectId: getBrowserActiveEnsObjectId(state),
Andrey Azov's avatar
Andrey Azov committed
314
  allActiveEnsObjectIds: getBrowserActiveEnsObjectIds(state),
Imran Salam's avatar
Imran Salam committed
315
  allChrLocations: getAllChrLocations(state),
316
  browserActivated: getBrowserActivated(state),
317
  browserNavOpened: getBrowserNavOpened(state),
318
  browserQueryParams: getBrowserQueryParams(state),
319
  chrLocation: getChrLocation(state),
Imran Salam's avatar
Imran Salam committed
320
321
  isDrawerOpened: getIsDrawerOpened(state),
  isTrackPanelOpened: getIsTrackPanelOpened(state),
322
323
324
  launchbarExpanded: getLaunchbarExpanded(state),
  exampleEnsObjects: getExampleEnsObjects(state),
  committedSpecies: getEnabledCommittedSpecies(state)
325
326
});

327
const mapDispatchToProps = {
328
  changeBrowserLocation,
Andrey Azov's avatar
Andrey Azov committed
329
  changeFocusObject,
Imran Salam's avatar
Imran Salam committed
330
331
  changeDrawerView,
  closeDrawer,
Andrey Azov's avatar
Andrey Azov committed
332
  fetchGenomeData,
333
  replace,
334
  toggleDrawer,
335
336
  setDataFromUrlAndSave,
  restoreBrowserTrackStates
337
338
};

339
340
341
342
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Browser);