import * as React from 'react'; import { SyntheticEvent } from 'react'; import { observer } from 'mobx-react'; import { StudyViewPageStore } from '../../studyView/StudyViewPageStore'; import { action, computed, makeObservable, observable } from 'mobx'; import autobind from 'autobind-decorator'; import { DUPLICATE_GROUP_NAME_MSG, getDefaultGroupName, MAX_GROUPS_IN_SESSION, StudyViewComparisonGroup, } from '../GroupComparisonUtils'; import { getComparisonLoadingUrl, redirectToComparisonPage, } from '../../../shared/api/urls'; import styles, { sharedGroup } from '../styles.module.scss'; import { DefaultTooltip, remoteData } from 'cbioportal-frontend-commons'; import { getGroupParameters, getSelectedGroups, } from './ComparisonGroupManagerUtils'; import comparisonClient from '../../../shared/api/comparisonGroupClientInstance'; import { MakeMobxView } from '../../../shared/components/MobxView'; import LoadingIndicator from '../../../shared/components/loadingIndicator/LoadingIndicator'; import ErrorMessage from '../../../shared/components/ErrorMessage'; import GroupCheckbox from './GroupCheckbox'; import { sleepUntil } from '../../../shared/lib/TimeUtils'; import { LoadingPhase } from '../GroupComparisonLoading'; import _ from 'lodash'; import { serializeEvent } from 'shared/lib/tracking'; import { openSocialAuthWindow } from 'shared/lib/openSocialAuthWindow'; import classnames from 'classnames'; import { getReservedGroupColor } from 'shared/lib/Colors'; export interface IComparisonGroupManagerProps { store: StudyViewPageStore; shareGroups: (groups: StudyViewComparisonGroup[]) => void; onGroupColorChange?: (groupId: string, color: string) => void; } @observer export default class ComparisonGroupManager extends React.Component< IComparisonGroupManagerProps, {} > { @observable groupNameFilter: string = ''; @observable addGroupPanelOpen = false; @observable _inputGroupName: string = ''; @computed get inputGroupName() { return this._inputGroupName; } @observable addSamplesTargetGroupId: string = ''; constructor(props: IComparisonGroupManagerProps) { super(props); makeObservable(this); } @action.bound private onChangeGroupNameFilter(e: SyntheticEvent) { this.groupNameFilter = (e.target as HTMLInputElement).value; } @action.bound private onChangeInputGroupName(e: SyntheticEvent) { this._inputGroupName = (e.target as HTMLInputElement).value; } readonly filteredGroups = remoteData({ await: () => [this.props.store.comparisonGroups], invoke: () => Promise.resolve( // TODO: fuzzy string search? _.sortBy( this.props.store.comparisonGroups.result!.filter(group => new RegExp(this.groupNameFilter, 'i').test(group.name) ), group => group.name.toLowerCase() ) ), }); @action.bound private showAddGroupPanel() { this.addGroupPanelOpen = true; const preDefinedCustomChartFilterSet = _.reduce( Array.from( this.props.store.preDefinedCustomChartFilterSet.entries() ), (acc, [chartUniqueKey, clinicalDataFilter]) => { acc[chartUniqueKey] = clinicalDataFilter.values.map( datum => datum.value ); return acc; }, {} as { [id: string]: string[] } ); this._inputGroupName = getDefaultGroupName( this.props.store.filters, preDefinedCustomChartFilterSet, this.props.store.clinicalAttributeIdToDataType.result! ); } @action.bound private cancelAddGroup() { this.addGroupPanelOpen = false; this._inputGroupName = ''; this.addSamplesTargetGroupId = ''; } @action.bound private deleteGroup(group: StudyViewComparisonGroup) { this.props.store.toggleComparisonGroupMarkedForDeletion(group.uid); } @action.bound private selectAllFiltered() { for (const group of this.filteredGroups.result!) { this.props.store.setComparisonGroupSelected(group.uid, true); } this.props.store.flagDuplicateColorsForSelectedGroups( undefined!, undefined! ); } @action.bound private deselectAllFiltered() { for (const group of this.filteredGroups.result!) { this.props.store.setComparisonGroupSelected(group.uid, false); this.props.store.showComparisonGroupWarningSign(group.uid, false); } } private get header() { return (
); } @autobind private restoreGroup(group: StudyViewComparisonGroup) { this.props.store.toggleComparisonGroupMarkedForDeletion(group.uid); } @autobind private shareSingleGroup(group: StudyViewComparisonGroup) { this.props.shareGroups([group]); } @autobind private shareGroups() { const selectedGroups = getSelectedGroups( this.props.store.comparisonGroups.result || [], this.props.store ); this.props.shareGroups(selectedGroups); } private readonly groupsSection = MakeMobxView({ await: () => [ this.props.store.comparisonGroups, this.filteredGroups, this.props.store.selectedSamples, ], render: () => { if (this.props.store.comparisonGroups.result!.length > 0) { // show this component if there are groups, and if filteredGroups is complete return (
{this.filteredGroups.result!.length > 0 ? ( this.filteredGroups.result!.map(group => ( )) ) : (
No results for your current search.
)}
); } else { return (
Group comparison allows you to create custom groups and compare their clinical and genomic features. Use the button below to create groups based on selections.
); } }, renderPending: () => , renderError: () => (
), }); private get viewButton() { if ( this.props.store.comparisonGroups.isComplete && this.props.store.comparisonGroups.result.length > 0 ) { // only show select button if there are any groups return ( ); } else { return null; } } private get shareButton() { if ( this.props.store.comparisonGroups.isComplete && this.props.store.comparisonGroups.result.length > 0 ) { // only show select button if there are any groups return ( ); } else { return null; } } @computed get allGroupNames() { return this.props.store.comparisonGroups.isComplete ? this.props.store.comparisonGroups.result!.map(group => group.name) : undefined; } @computed get submitNewGroupDisabled() { const selectedSamples = this.props.store.selectedSamples.isComplete ? this.props.store.selectedSamples.result : undefined; return ( !selectedSamples || this.inputGroupName.length === 0 || !this.allGroupNames || this.allGroupNames.includes(this.inputGroupName) ); } @action.bound private async onSubmitNewGroup() { const selectedSamples = this.props.store.selectedSamples.isComplete ? this.props.store.selectedSamples.result : undefined; const { id } = await comparisonClient.addGroup( getGroupParameters( this.inputGroupName, selectedSamples!, this.props.store.studyIds ) ); // check if there is any predefined color for this group and set the color const reservedColor = getReservedGroupColor(this.inputGroupName); if (reservedColor != undefined) this.props.store.onGroupColorChange(id, reservedColor); this.props.store.notifyComparisonGroupsChange(); if (!this.props.store.isLoggedIn) { //save group id to page session only for non-logged in user. // this is to keep track of what groups are created by the user this.props.store.trackNewlyCreatedGroup(id); } this.cancelAddGroup(); } @action.bound private async onGroupColorChange( group: StudyViewComparisonGroup, newColor: string ) { this.props.onGroupColorChange && this.props.onGroupColorChange(group.uid, newColor); } private get compareButton() { if (this.props.store.comparisonGroups.isComplete) { // only show if there are enough groups to possibly compare (i.e. 2) const numSelectedGroups = getSelectedGroups( this.props.store.comparisonGroups.result, this.props.store ).length; const wrongNumberOfGroups = numSelectedGroups > MAX_GROUPS_IN_SESSION || numSelectedGroups < 2; let tooltipText = ''; if (this.props.store.comparisonGroups.result.length >= 2) { if (wrongNumberOfGroups) { tooltipText = `Select between 2 and ${MAX_GROUPS_IN_SESSION} groups to enable comparison.`; } else { tooltipText = 'Open a comparison session with selected groups'; } } else { tooltipText = 'Create at least two groups to open a comparison session'; } return ( ); } else { return null; } } private readonly actionButtons = MakeMobxView({ await: () => [this.props.store.comparisonGroups], render: () => { if (this.props.store.comparisonGroups.result!.length > 0) { return (
{this.compareButton} {this.viewButton} {this.shareButton}
); } else { return null; } }, }); private get addGroupPanel() { let contents: any; const createOrAddButtonWidth = 58; const selectedSamples = this.props.store.selectedSamples.isComplete ? this.props.store.selectedSamples.result : undefined; if (this.addGroupPanelOpen) { contents = [
Add{selectedSamples ? ` ${selectedSamples.length}` : ''}{' '} selected samples
,
{DUPLICATE_GROUP_NAME_MSG}
} > { if ( event.key == 'Enter' && !this.submitNewGroupDisabled ) { this.onSubmitNewGroup(); } }} />
, ]; } else { contents = ( ); } return (
{contents}
); } componentWillUnmount() { this.props.store.deleteMarkedComparisonGroups(); } @computed get existSharedGroups() { return ( this.props.store.comparisonGroups.result.filter( group => group.isSharedGroup ).length > 0 ); } @computed get notificationMessages() { let notificationMessages: JSX.Element[] = []; if (this.props.store.comparisonGroups.result.length > 0) { // Notify if there any shared groups if (this.existSharedGroups) { notificationMessages.push( <> Highlighted rows {' '} are shared groups. ); } // Notify that shared and page-session groups are not saved for non-logged users if ( !this.props.store.isLoggedIn && this.props.store.appStore.isSocialAuthenticated ) { notificationMessages.push( <>
The groups above will not be saved without  
); } else if (this.existSharedGroups) { // Notify if shared groups are saved to user profile notificationMessages.push( <>They have been saved to your profile. ); } } return notificationMessages; } render() { return (
{this.header} {this.groupsSection.component} {this.notificationMessages.length > 0 && ( <>
Note: {this.notificationMessages} )} {this.actionButtons.component}
{this.addGroupPanel}
); } }