Skip to content

Commit

Permalink
Markzhang/metadata sidebar (#1700)
Browse files Browse the repository at this point in the history
* Add Sidebar component.

Also tweak RemoveIcon props and do some CSS module conversions.

* Add MetadataEditor and SampleDetailsSidebar and backend endpoints.

Also tweak the API of some base ui components.

* Fix issue where some filters in PipelineSampleReport aren't working right.

Change Dropdown so first value isn't displayed even when it's not selected.

Change Dropdown so Menu is 100% width by default.

Fix bug where Dropdowns in MetadataEditor weren't working due to setState behavior.

* Check if metadata changed before saving.

* Fix some proptypes errors while I'm at it.
  • Loading branch information
MarkAZhang committed Nov 7, 2018
1 parent 87b60b4 commit 119e2c5
Show file tree
Hide file tree
Showing 41 changed files with 587 additions and 130 deletions.
32 changes: 32 additions & 0 deletions app/assets/src/api/index.js
@@ -0,0 +1,32 @@
// TODO(mark): Split this file up as more API methods get added.
import axios from "axios";

const postWithCSRF = async (url, params) => {
const resp = await axios.post(url, {
...params,
// Fetch the CSRF token from the DOM.
authenticity_token: document.getElementsByName("csrf-token")[0].content
});

// Just return the data.
// resp also contains headers, status, etc. that we might use later.
return resp.data;
};

const get = async url => {
const resp = await axios.get(url);

return resp.data;
};

const getSampleMetadata = id => get(`/samples/${id}/metadata`);

const saveSampleMetadata = (id, field, value) =>
postWithCSRF(`/samples/${id}/save_metadata_v2`, {
field,
value
});

const getMetadataTypes = () => get("/samples/metadata_types");

export { getSampleMetadata, saveSampleMetadata, getMetadataTypes };
37 changes: 31 additions & 6 deletions app/assets/src/components/PipelineSampleReads.jsx
Expand Up @@ -10,6 +10,7 @@ import ERCCScatterPlot from "./ERCCScatterPlot";
import PipelineSampleReport from "./PipelineSampleReport";
import AMRView from "./AMRView";
import BasicPopup from "./BasicPopup";
import SampleDetailsSidebar from "./views/report/SampleDetailsSidebar";
import { SAMPLE_FIELDS } from "./utils/SampleFields";
import PrimaryButton from "./ui/controls/buttons/PrimaryButton";
import ViewHeader from "./layout/ViewHeader";
Expand Down Expand Up @@ -60,7 +61,8 @@ class PipelineSampleReads extends React.Component {
confirmed_names: props.reportDetails
? props.reportDetails.confirmed_names
: [],
sample_name: props.sampleInfo.name
sample_name: props.sampleInfo.name,
sampleDetailsSidebarVisible: false
};
this.TYPE_PROMPT = "-";
this.NUCLEOTIDE_TYPES = ["Not set", "DNA", "RNA"];
Expand Down Expand Up @@ -150,6 +152,12 @@ class PipelineSampleReads extends React.Component {
.catch(err => {});
}

toggleSampleDetailsSidebar = () => {
this.setState({
sampleDetailsSidebarVisible: !this.state.sampleDetailsSidebarVisible
});
};

toggleHighlightTaxon(e) {
let taxid = e.target.getAttribute("data-tax-id");
let name = e.target.getAttribute("data-tax-name");
Expand Down Expand Up @@ -179,7 +187,7 @@ class PipelineSampleReads extends React.Component {
let dropdown_options = this.DROPDOWN_OPTIONS[field];
let display_value = this.sampleInfo[field] ? this.sampleInfo[field] : "-";
return (
<div className="row detail-row">
<div className="row detail-row" key={`${label}_${field}`}>
<div className="col s6 label">{label}</div>
<div className="col s6">
<div className="sample-notes">
Expand Down Expand Up @@ -217,7 +225,7 @@ class PipelineSampleReads extends React.Component {
let value = hash[field];
if (hash[field] instanceof Array) value = hash[field].join("; ");
return (
<div className="details-container col s12">
<div className="details-container col s12" key={`${label}_${field}`}>
<div className="details-title note">{label}</div>
<div className={"sample-notes note " + (editable ? "edit-wide" : "")}>
<pre
Expand All @@ -241,7 +249,7 @@ class PipelineSampleReads extends React.Component {
if (popupContent)
labelElem = <BasicPopup trigger={labelElem} content={popupContent} />;
return (
<div className="row detail-row">
<div className="row detail-row" key={`${label}_${field}`}>
{labelElem}
<div className="col s6">
<div
Expand All @@ -265,7 +273,7 @@ class PipelineSampleReads extends React.Component {
render_metadata_numfield(label, field) {
let display_value = this.sampleInfo[field] || this.TYPE_PROMPT;
return (
<div className="row detail-row">
<div className="row detail-row" key={`${label}_${field}`}>
<div className="col s6 label">{label}</div>
<div className="col s6">
<div
Expand Down Expand Up @@ -574,6 +582,7 @@ class PipelineSampleReads extends React.Component {
onClick={() => {
this.refreshPage(phash);
}}
key={version}
>
{"Pipeline v" + version}
</Dropdown.Item>
Expand Down Expand Up @@ -830,7 +839,6 @@ class PipelineSampleReads extends React.Component {
<AMRView amr={this.amr} />
</div>
) : null;

return (
<div>
<ViewHeader className={cs.viewHeader}>
Expand All @@ -852,6 +860,16 @@ class PipelineSampleReads extends React.Component {
onClick: () => window.open(`/samples/${sampleId}`, "_self")
}))}
/>
{this.props.admin && (
<div className={cs.sampleDetailsLinkContainer}>
<span
className={cs.sampleDetailsLink}
onClick={this.toggleSampleDetailsSidebar}
>
Sample Details
</span>
</div>
)}
</ViewHeader.Content>
<ViewHeader.Controls>{report_buttons}</ViewHeader.Controls>
</ViewHeader>
Expand Down Expand Up @@ -1000,6 +1018,13 @@ class PipelineSampleReads extends React.Component {
>
{d_report}
</div>
{this.props.admin && (
<SampleDetailsSidebar
visible={this.state.sampleDetailsSidebarVisible}
onClose={this.toggleSampleDetailsSidebar}
sample={this.props.sampleInfo}
/>
)}
</div>
);
}
Expand Down
32 changes: 14 additions & 18 deletions app/assets/src/components/PipelineSampleReport.jsx
Expand Up @@ -620,45 +620,41 @@ class PipelineSampleReport extends React.Component {
this.handleThresholdFiltersChange(activeThresholds);
};

handleBackgroundModelChange = (_, data) => {
if (data.value === this.state.backgroundData.id) {
handleBackgroundModelChange = (backgroundId, backgroundName) => {
if (backgroundId === this.state.backgroundData.id) {
// Skip if no change
return;
}

const backgroundName = data.options.find(function(option) {
return option.value === data.value;
}).text;

Cookies.set("background_id", data.value);
Cookies.set("background_id", backgroundId);
this.setState(
{
backgroundData: {
name: backgroundName,
id: data.value
id: backgroundId
}
},
() => {
this.props.refreshPage({ background_id: data.value });
this.props.refreshPage({ background_id: backgroundId });
}
);
};

handleNameTypeChange = (_, data) => {
Cookies.set("name_type", data.value);
this.setState({ name_type: data.value });
handleNameTypeChange = nameType => {
Cookies.set("name_type", nameType);
this.setState({ name_type: nameType });
};

handleSpecificityChange = (_, data) => {
Cookies.set("readSpecificity", data.value);
this.setState({ readSpecificity: data.value }, () => {
handleSpecificityChange = specificity => {
Cookies.set("readSpecificity", specificity);
this.setState({ readSpecificity: specificity }, () => {
this.applyFilters();
});
};

handleTreeMetricChange = (_, data) => {
Cookies.set("treeMetric", data.value);
this.setState({ treeMetric: data.value });
handleTreeMetricChange = treeMetric => {
Cookies.set("treeMetric", treeMetric);
this.setState({ treeMetric });
};

handleViewClicked = (_, data) => {
Expand Down
55 changes: 27 additions & 28 deletions app/assets/src/components/SamplesHeatmap.jsx
Expand Up @@ -99,16 +99,10 @@ class SamplesHeatmap extends React.Component {
this.getTaxonFor = this.getTaxonFor.bind(this);
this.getTooltip = this.getTooltip.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
this.onBackgroundChanged = this.onBackgroundChanged.bind(this);
this.onCategoryChange = this.onCategoryChange.bind(this);
this.onCellClick = this.onCellClick.bind(this);
this.onDataScaleChange = this.onDataScaleChange.bind(this);
this.onMetricChange = this.onMetricChange.bind(this);
this.onRemoveRow = this.onRemoveRow.bind(this);
this.onSampleLabelClick = this.onSampleLabelClick.bind(this);
this.onShareClick = this.onShareClick.bind(this);
this.onSpecificityChange = this.onSpecificityChange.bind(this);
this.onTaxonLevelChange = this.onTaxonLevelChange.bind(this);
this.onTaxonsPerSampleEnd = this.onTaxonsPerSampleEnd.bind(this);
this.onThresholdFilterApply = this.onThresholdFilterApply.bind(this);
}
Expand Down Expand Up @@ -492,22 +486,23 @@ class SamplesHeatmap extends React.Component {
);
}

onMetricChange(_, metric) {
if (metric.value == this.state.selectedOptions.metric) {
onMetricChange = metric => {
if (metric == this.state.selectedOptions.metric) {
return;
}

this.optionsChanged = true;
this.setSelectedOptionsState(
{ metric: metric.value },
{ metric },
this.explicitApply ? undefined : this.updateHeatmap
);
}
};

renderMetricPicker() {
return (
<Dropdown
fluid
rounded
options={this.state.availableOptions.metrics}
onChange={this.onMetricChange}
value={this.state.selectedOptions.metric}
Expand Down Expand Up @@ -540,22 +535,23 @@ class SamplesHeatmap extends React.Component {
);
}

onTaxonLevelChange(_, taxonLevel) {
if (this.state.selectedOptions.species == taxonLevel.value) {
onTaxonLevelChange = taxonLevel => {
if (this.state.selectedOptions.species == taxonLevel) {
return;
}

this.optionsChanged = true;
this.setSelectedOptionsState(
{ species: taxonLevel.value },
{ species: taxonLevel },
this.explicitApply ? undefined : this.updateHeatmap
);
}
};

renderTaxonLevelPicker() {
return (
<Dropdown
fluid
rounded
options={this.state.availableOptions.taxonLevels}
value={this.state.selectedOptions.species}
onChange={this.onTaxonLevelChange}
Expand All @@ -565,14 +561,14 @@ class SamplesHeatmap extends React.Component {
);
}

onDataScaleChange(_, scaleIdx) {
if (scaleIdx.value == this.state.selectedOptions.dataScaleIdx) {
onDataScaleChange = scaleIdx => {
if (scaleIdx == this.state.selectedOptions.dataScaleIdx) {
return;
}

this.recluster = true;
this.setSelectedOptionsState({ dataScaleIdx: scaleIdx.value });
}
this.setSelectedOptionsState({ dataScaleIdx: scaleIdx });
};

renderScalePicker() {
let options = this.state.availableOptions.scales.map(function(
Expand All @@ -585,6 +581,7 @@ class SamplesHeatmap extends React.Component {
return (
<Dropdown
fluid
rounded
value={this.state.selectedOptions.dataScaleIdx}
onChange={this.onDataScaleChange}
options={options}
Expand Down Expand Up @@ -624,13 +621,13 @@ class SamplesHeatmap extends React.Component {
);
}

onCategoryChange(categories, subcategories) {
onCategoryChange = (categories, subcategories) => {
this.optionsChanged = true;
this.setSelectedOptionsState(
{ categories, subcategories },
this.explicitApply ? undefined : this.updateHeatmap
);
}
};

renderCategoryFilter() {
let options = this.state.availableOptions.categories.map(category => {
Expand All @@ -657,17 +654,17 @@ class SamplesHeatmap extends React.Component {
);
}

onBackgroundChanged(_, background) {
if (background.value == this.state.selectedOptions.background) {
onBackgroundChanged = background => {
if (background == this.state.selectedOptions.background) {
return;
}

this.optionsChanged = true;
this.setSelectedOptionsState(
{ background: background.value },
{ background },
this.explicitApply ? undefined : this.updateHeatmap
);
}
};

renderBackgroundPicker() {
let options = this.state.availableOptions.backgrounds.map(function(
Expand All @@ -679,6 +676,7 @@ class SamplesHeatmap extends React.Component {
return (
<Dropdown
fluid
rounded
options={options}
onChange={this.onBackgroundChanged}
value={this.state.selectedOptions.background}
Expand Down Expand Up @@ -709,22 +707,23 @@ class SamplesHeatmap extends React.Component {
);
}

onSpecificityChange(_, specificity) {
if (specificity.value === this.state.selectedOptions.readSpecificity) {
onSpecificityChange = specificity => {
if (specificity === this.state.selectedOptions.readSpecificity) {
return;
}

this.optionsChanged = true;
this.setSelectedOptionsState(
{ readSpecificity: specificity.value },
{ readSpecificity: specificity },
this.explicitApply ? undefined : this.updateHeatmap
);
}
};

renderSpecificityFilter() {
return (
<Dropdown
fluid
rounded
options={this.availableOptions.specificityOptions}
value={this.state.selectedOptions.readSpecificity}
label="Read Specificity: "
Expand Down
2 changes: 1 addition & 1 deletion app/assets/src/components/layout/ViewHeader/Title.jsx
Expand Up @@ -95,7 +95,7 @@ Title.propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
id: PropTypes.string,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onClick: PropTypes.func.isRequired
})
),
Expand Down

0 comments on commit 119e2c5

Please sign in to comment.