From cb680ec2b23a7280868e6c031ae9b861be8f12b8 Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Thu, 8 Jan 2026 23:09:09 -0300
Subject: [PATCH 01/16] Advances with Implement Modal to create a Biomarker
from a result of a correlation experiment
---
.../BiomarkerFromCorrelationModal.tsx | 1561 +++++++++++++++++
.../CreateBiomarkerButton.tsx | 90 +
.../ResultTableControlForm.tsx | 287 +--
src/frontend/static/frontend/src/css/gem.css | 5 +-
4 files changed, 1805 insertions(+), 138 deletions(-)
create mode 100644 src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
create mode 100644 src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
new file mode 100644
index 00000000..5fd6b445
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
@@ -0,0 +1,1561 @@
+import React from 'react'
+// Update the import path to the correct location of Base component
+import { Modal, DropdownItemProps, Icon, Form, Button } from 'semantic-ui-react'
+import { DjangoCGDSStudy, DjangoMRNAxGEMResultRow, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile } from '../../../utils/django_interfaces'
+import ky, { Options } from 'ky'
+import { getDjangoHeader, cleanRef, getFilenameFromSource, getDefaultSource } from '../../../utils/util_functions'
+import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, ConfirmModal, ExperimentInfo, ExperimentResultTableControl } from '../../../utils/interfaces'
+import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from '../../biomarkers/types'
+import { ManualForm } from '../../biomarkers/modalContentBiomarker/manualForm/ManualForm'
+import { PaginationCustomFilter } from '../../common/PaginatedTable'
+import { isEqual } from 'lodash'
+import { getDefaultClusteringParameters, getDefaultRFParameters, getDefaultSvmParameters } from '../../biomarkers/utils'
+
+// URLs defined in biomarkers.html
+declare const urlBiomarkersCRUD: string
+declare const urlBiomarkersSimpleUpdate: string
+declare const urlBiomarkersCreate: string
+declare const urlMiRNACodes: string
+declare const urlGeneSymbols: string
+declare const urlMethylationSites: string
+declare const urlMiRNACodesFinder: string
+declare const urlMethylationSitesFinder: string
+declare const urlGeneSymbolsFinder: string
+
+const REQUEST_TIMEOUT = 120000 // 2 minutes in milliseconds
+type SelectedOption = 'selectAll' | 'selectWithFilters'
+
+/** A matched molecule with the search query and the validated alias. */
+type MoleculeFinderResult = { molecule: string, standard: string }
+
+/** Extremely simple struct of a Biomarker (useful for simple updates). */
+type BiomarkerNameAndDesc = {
+ name: string,
+ description: string
+}
+
+/** Some flags to validate the Biomarkers form. */
+type ValidationForm = {
+ haveAmbiguous: boolean,
+ haveInvalid: boolean
+}
+
+interface BiomarkerFromCorrelationModalProps {
+ experimentInfo: ExperimentInfo;
+ tableControl: ExperimentResultTableControl;
+ onCreateBiomarker: (options: {
+ experimentInfo: ExperimentInfo;
+ selectAll: boolean;
+ tableControl?: ExperimentResultTableControl;
+ }) => void;
+}
+
+/** BiomarkersPanel's state */
+interface BiomarkerFromCorrelationModalState {
+ biomarkers: BiomarkerSimple[],
+ newBiomarker: Biomarker,
+ /** PK of the Biomarker that's being loaded. */
+ loadingFullBiomarkerId: Nullable,
+ selectedBiomarkerToDeleteOrSync: Nullable,
+ checkedIgnoreProposedAlias: boolean,
+ showDeleteBiomarkerModal: boolean,
+ /** Indicates if there's a Biomarker being deleted. */
+ deletingBiomarker: boolean,
+ /** Indicates if there's a Biomarker being stopped. */
+ stoppingExperiment: boolean,
+ /** Biomarker to stop. */
+ biomarkerToStop: Nullable,
+ addingOrEditingBiomarker: boolean,
+ biomarkerTypeSelected: BiomarkerOrigin,
+ formBiomarker: FormBiomarkerData,
+ confirmModal: ConfirmModal
+ tags: DjangoTag[],
+ /** Indicates if the modal to create or edit a Biomarker is open. */
+ openCreateEditBiomarkerModal: boolean,
+ /** Indicates if the modal to clone a Biomarker is open. Contains the pk of the Biomarker to clone. */
+ biomarkerToClone: Nullable,
+ /** Indicates if there's a Biomarker being cloned. */
+ cloningBiomarker: boolean,
+ /** Indicates if the modal to get the details of a Biomarker is open. */
+ openDetailsModal: boolean,
+ /** Selected Biomarker instance to show its details. */
+ selectedBiomarker: Nullable,
+ alert: CustomAlert,
+ featureSelection: FeatureSelectionPanelData,
+ submittingFSExperiment: boolean,
+ openDetailsModal2: boolean,
+ selectedOption: SelectedOption,
+ openSelectOptionModal: boolean
+}
+
+/**
+ * Renders a CRUD panel for a Biomarker.
+ */
+export class BiomarkerFromCorrelationModal extends React.Component {
+ abortController = new AbortController()
+ constructor (props) {
+ super(props)
+ this.state = {
+ biomarkers: [],
+ newBiomarker: this.getDefaultNewBiomarker(),
+ loadingFullBiomarkerId: null,
+ biomarkerTypeSelected: BiomarkerOrigin.BASE,
+ checkedIgnoreProposedAlias: false,
+ showDeleteBiomarkerModal: false,
+ stoppingExperiment: false,
+ biomarkerToStop: null,
+ selectedBiomarkerToDeleteOrSync: null,
+ deletingBiomarker: false,
+ addingOrEditingBiomarker: false,
+ formBiomarker: this.getDefaultFormBiomarker(),
+ confirmModal: this.getDefaultConfirmModal(),
+ tags: [],
+ openCreateEditBiomarkerModal: false,
+ cloningBiomarker: false,
+ biomarkerToClone: null,
+ openDetailsModal: false,
+ selectedBiomarker: null,
+ alert: this.getDefaultAlertProps(),
+ featureSelection: this.getDefaultFeatureSelectionProps(),
+ submittingFSExperiment: false,
+ openDetailsModal2: false,
+ selectedOption: 'selectAll',
+ openSelectOptionModal: false
+ }
+ }
+
+ /**
+ * Abort controller if component is render
+ */
+
+ componentWillUnmount () {
+ this.abortController.abort()
+ }
+
+ /**
+ * Generates default feature selection creation structure
+ * @returns Default the default Alert
+ */
+ getDefaultFeatureSelectionProps = (): FeatureSelectionPanelData => {
+ return {
+ step: 1,
+ biomarker: null,
+ selectedBiomarker: null,
+ clinicalSource: getDefaultSource(),
+ mRNASource: getDefaultSource(),
+ mirnaSource: getDefaultSource(),
+ methylationSource: getDefaultSource(),
+ cnaSource: getDefaultSource(),
+ algorithm: FeatureSelectionAlgorithm.BLIND_SEARCH,
+ fitnessFunction: FitnessFunction.CLUSTERING,
+ fitnessFunctionParameters: this.getDefaultFitnessFunctionParameters(),
+ advancedAlgorithmParameters: this.getDefaultAdvancedAlgorithmParameters(),
+ crossValidationParameters: { folds: 10 }
+ }
+ }
+
+ /**
+ * Handle changes in the checkedIgnoreProposedAlias value.
+ * @param checkedIgnoreProposedAlias New checkedIgnoreProposedAlias value.
+ */
+ handleChangeIgnoreProposedAlias = (checkedIgnoreProposedAlias: boolean) => {
+ // Clear all the proposed molecules as they are not valid anymore (they are computed on search only)
+ const formBiomarker = this.state.formBiomarker
+ formBiomarker.moleculesSymbolsFinder.data = []
+
+ this.setState({ checkedIgnoreProposedAlias, formBiomarker })
+ }
+
+ /**
+ * Generates default settings for advance Algorithm data.
+ * @returns Default structure of all advance algorithms.
+ */
+ getDefaultAdvancedAlgorithmParameters = (): AdvancedAlgorithmParameters => ({
+ isActive: false,
+ BBHA: {
+ useSpark: true,
+ numberOfStars: 60,
+ numberOfIterations: 10,
+ BBHAVersion: BBHAVersion.ORIGINAL,
+ coeff1: 2.2,
+ coeff2: 0.1
+ },
+ GA: {
+ useSpark: true,
+ numberOfIterations: 10,
+ populationSize: 50,
+ mutationRate: 0.01
+ },
+ coxRegression: {
+ useSpark: true,
+ topN: 5
+ }
+ })
+
+ /**
+ * Generates default settings for all the fitness functions.
+ * @returns Default structure for all the fitness functions.
+ */
+ getDefaultFitnessFunctionParameters = (): FitnessFunctionParameters => ({
+ clusteringParameters: { ...getDefaultClusteringParameters(), lookForOptimalNClusters: false }, // TODO: Change to default when implemented in backend
+ svmParameters: getDefaultSvmParameters(),
+ rfParameters: getDefaultRFParameters()
+ })
+
+ /**
+ * Generates a default alert structure
+ * @returns Default the default Alert
+ */
+ getDefaultAlertProps = (): CustomAlert => {
+ return {
+ message: '', // This have to change during cycle of component
+ isOpen: false,
+ type: CustomAlertTypes.SUCCESS,
+ duration: 500
+ }
+ }
+
+ /**
+ * Generates a default confirm modal structure
+ * @returns Default confirmModal object
+ */
+ getDefaultConfirmModal = (): ConfirmModal => {
+ return {
+ confirmModal: false,
+ headerText: '',
+ contentText: '',
+ onConfirm: () => console.log('DefaultConfirmModalFunction, this should change during cycle of component')
+ }
+ }
+
+ handleCloseStopFSExperiment = () => {
+ this.setState({ biomarkerToStop: null })
+ }
+
+ /**
+ * Reset the confirm modal, to be used again
+ */
+ handleCloseAlert = () => {
+ const alert = this.state.alert
+ alert.isOpen = false
+ this.setState({ alert })
+ }
+
+ /**
+ * Reset the confirm modal, to be used again
+ */
+ handleCancelConfirmModalState () {
+ this.setState({ confirmModal: this.getDefaultConfirmModal() })
+ }
+
+ /**
+ * Toggle if advance is active or not
+ */
+ handleSwitchAdvanceAlgorithm = () => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.advancedAlgorithmParameters.isActive = !featureSelection.advancedAlgorithmParameters.isActive
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Change the value of the advance algorithm and prop selected
+ * @param advanceAlgorithm Advance algorithm selected
+ * @param name name of prop to change
+ * @param value value to set
+ */
+ handleChangeAdvanceAlgorithm = (advanceAlgorithm: string, name: string, value: any) => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.advancedAlgorithmParameters[advanceAlgorithm][name] = value
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Callback when a new file is selected in the uncontrolled component
+ * (input type=file)
+ */
+ selectNewFile = () => { this.updateSourceFilenamesAndCommonSamples() }
+
+ /**
+ * Select the algorithm, initialize the state of the selected and clean the others states
+ * @param algorithm algorithm selected
+ */
+ handleChangeAlgorithm = (algorithm: FeatureSelectionAlgorithm) => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.algorithm = algorithm
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Select the algorithm, initialize the state of the selected and clean the others states
+ * @param fitnessFunction Fitness function selected
+ */
+ handleChangeFitnessFunction = (fitnessFunction: FitnessFunction) => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.fitnessFunction = fitnessFunction
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Manage changes of Feature Selection process parameters.
+ * @param fitnessFunction name of fitness function to change
+ * @param key name of the fitnessFunction object that have changed
+ * @param value value selected typed depends of what fitness function and key is being changing
+ */
+ handleChangeFitnessFunctionOption = (fitnessFunction: T, key: M, value: FitnessFunctionParameters[T][M]) => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.fitnessFunctionParameters[fitnessFunction][key] = value
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Manage changes of CrossValidation parameters.
+ * @param key name of the crossValidationParameters object that have changed.
+ * @param value value selected.
+ */
+ handleChangeCrossValidation = (key: T, value: any) => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.crossValidationParameters[key] = value
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Selects a CGDS Study as a source
+ * @param selectedStudy Selected Study as Source
+ * @param sourceStateName Source's name in state object to update
+ */
+ selectStudy = (selectedStudy: DjangoCGDSStudy, sourceStateName: SourceStateBiomarker) => {
+ // Selects source to update
+ const featureSelection = this.state.featureSelection
+ const source = featureSelection[sourceStateName]
+
+ source.type = SourceType.CGDS
+ source.CGDSStudy = selectedStudy
+ this.setState({ featureSelection }, this.updateSourceFilenamesAndCommonSamples)
+ }
+
+ /**
+ * Selects a User's file as a source
+ * @param selectedFile Selected file as Source
+ * @param sourceStateName Source's name in state object to update
+ */
+ selectUploadedFile = (selectedFile: DjangoUserFile, sourceStateName: SourceStateBiomarker) => {
+ // Selects source to update
+ const featureSelection = this.state.featureSelection
+ const source = featureSelection[sourceStateName]
+
+ source.type = SourceType.UPLOADED_DATASETS
+ source.selectedExistingFile = selectedFile
+ this.setState({ featureSelection }, this.updateSourceFilenamesAndCommonSamples)
+ }
+
+ /**
+ * Change the source state to submit a pipeline
+ * @param sourceType New selected Source
+ * @param sourceStateName Source's name in state object to update
+ */
+ handleChangeSourceType = (sourceType: SourceType, sourceStateName: SourceStateBiomarker) => {
+ // Selects source to update
+ const featureSelection = this.state.featureSelection
+ const source = featureSelection[sourceStateName]
+ // Change source type
+ source.type = sourceType
+
+ // Resets all source values
+ source.selectedExistingFile = null
+ source.CGDSStudy = null
+ cleanRef(source.newUploadedFileRef)
+
+ // After update state
+ this.setState({ featureSelection }, this.updateSourceFilenamesAndCommonSamples)
+ }
+
+ /**
+ * Updates Sources' filenames and common examples counter
+ */
+ updateSourceFilenamesAndCommonSamples = () => {
+ this.updateSourceFilenames()
+ // this.checkCommonSamples() TODO: check function and dependencies functions in file Pipeline.tsx
+ }
+
+ /**
+ * Handles file input changes to set data to show in form
+ * IMPORTANT: this is necessary because the file inputs are uncontrolled components
+ * and doesn't trigger an update of the state fields
+ */
+ updateSourceFilenames = () => {
+ // Updates state filenames
+ const featureSelection = this.state.featureSelection
+ featureSelection.mRNASource.filename = getFilenameFromSource(featureSelection.mRNASource)
+ featureSelection.clinicalSource.filename = getFilenameFromSource(featureSelection.clinicalSource)
+ featureSelection.cnaSource.filename = getFilenameFromSource(featureSelection.cnaSource)
+ featureSelection.methylationSource.filename = getFilenameFromSource(featureSelection.methylationSource)
+ featureSelection.mirnaSource.filename = getFilenameFromSource(featureSelection.mirnaSource)
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Changes confirm modal state
+ * @param setOption New state of option
+ * @param headerText Optional text of header in confirm modal, by default will be empty
+ * @param contentText optional text of content in confirm modal, by default will be empty
+ * @param onConfirm Modal onConfirm callback
+ */
+ handleChangeConfirmModalState = (setOption: boolean, headerText: string, contentText: string, onConfirm: () => void) => {
+ const confirmModal = this.state.confirmModal
+ confirmModal.confirmModal = setOption
+ confirmModal.headerText = headerText
+ confirmModal.contentText = contentText
+ confirmModal.onConfirm = onConfirm
+ this.setState({ confirmModal })
+ }
+
+ /**
+ * Disambiguate the selected molecule for the yellow buttons.
+ * @param moleculeToDisambiguate Molecule to disambiguate.
+ * @param section Molecule section.
+ * @param selectedOption Selected option.
+ */
+ handleSelectOptionMolecule = (moleculeToDisambiguate: MoleculesSectionData, section: BiomarkerType, selectedOption: string) => {
+ const formBiomarker = this.state.formBiomarker
+ const indexToSelect = formBiomarker.moleculesSection[section].data.findIndex((item) => isEqual(item.value, moleculeToDisambiguate.value))
+
+ // Checks if the molecule is already a valid one
+ const exists = formBiomarker.moleculesSection[section].data.some((item) => item.value === selectedOption)
+ const data: MoleculesSectionData[] = formBiomarker.moleculesSection[section].data
+ data.splice(indexToSelect, 1)
+
+ if (!exists) {
+ data.push({
+ isValid: true,
+ value: selectedOption
+ })
+ }
+
+ this.setState({
+ ...this.state,
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesSection: {
+ ...this.state.formBiomarker.moleculesSection,
+ [section]: {
+ ...this.state.formBiomarker.moleculesSection[section],
+ data
+ }
+ }
+ }
+ })
+ }
+
+ /**
+ * Method that select how the user is going to create a Biomarker
+ * @param type Select the way to create a Biomarker
+ */
+ handleSelectModal = (type: BiomarkerOrigin) => {
+ this.setState({ biomarkerTypeSelected: type })
+ }
+
+ getBiomarkerFullInstance = (biomarkerSimple: BiomarkerSimple): Promise => {
+ return new Promise((resolve, reject) => {
+ this.setState({ loadingFullBiomarkerId: biomarkerSimple.id })
+ ky.get(urlBiomarkersCRUD + '/' + biomarkerSimple.id + '/', { signal: this.abortController.signal }).then((response) => {
+ response.json().then((jsonResponse: Biomarker | PromiseLike) => {
+ resolve(jsonResponse)
+ }).catch((err) => {
+ console.error('Error parsing JSON on Biomarker retrieval:', err)
+ reject(err)
+ })
+ }).catch((err) => {
+ console.error('Error getting Biomarker:', err)
+
+ if (!this.abortController.signal.aborted) {
+ reject(err)
+ }
+ }).finally(() => {
+ if (!this.abortController.signal.aborted) {
+ this.setState({ loadingFullBiomarkerId: null })
+ }
+ })
+ })
+ }
+
+ /**
+ * Opens the modal to show all the Biomarker details.
+ * @param selectedBiomarker Selected Biomarker instance.
+ */
+ openBiomarkerDetailsModal = (selectedBiomarker: BiomarkerSimple) => {
+ this.getBiomarkerFullInstance(selectedBiomarker).then((biomarker) => {
+ this.setState({
+ selectedBiomarker: {
+ ...selectedBiomarker,
+ cnas: biomarker.cnas,
+ mirnas: biomarker.mirnas,
+ methylations: biomarker.methylations,
+ mrnas: biomarker.mrnas
+ },
+ openDetailsModal: true
+ })
+ })
+ }
+
+ /** Closes the modal of Biomarker's details. */
+ closeBiomarkerDetailsModal = () => {
+ this.setState({
+ selectedBiomarker: null,
+ openDetailsModal: false
+ })
+ }
+
+ /**
+ * Checks if the user can edit the Biomarker (i.e. It was not used for an Inference experiment, Statistical Validation or Trained Model).
+ * @param biomarker Biomarker to check.
+ * @returns True if the Biomarker can be edited, false otherwise.
+ */
+ canEditBiomarker = (biomarker: BiomarkerSimple): boolean => !biomarker.was_already_used && biomarker.state === BiomarkerState.COMPLETED
+
+ /**
+ * Method that select how the user is going to create a Biomarker
+ * @param selectedBiomarker Biomarker selected to update
+ */
+ handleOpenEditBiomarker = (selectedBiomarker: BiomarkerSimple) => {
+ this.getBiomarkerFullInstance(selectedBiomarker).then((biomarker) => {
+ this.setState({
+ biomarkerTypeSelected: BiomarkerOrigin.MANUAL,
+ openCreateEditBiomarkerModal: true,
+ formBiomarker: {
+ id: biomarker.id,
+ canEditMolecules: this.canEditBiomarker(biomarker),
+ biomarkerName: biomarker.name,
+ biomarkerDescription: biomarker.description,
+ tag: biomarker.tag,
+ moleculeSelected: BiomarkerType.MRNA,
+ moleculesTypeOfSelection: MoleculesTypeOfSelection.INPUT,
+ moleculesSection: {
+ [BiomarkerType.CNA]: {
+ isLoading: false,
+ data: biomarker.cnas.map(item => ({ isValid: true, value: item.identifier }))
+ },
+ [BiomarkerType.MIRNA]: {
+ isLoading: false,
+ data: biomarker.mirnas.map(item => ({ isValid: true, value: item.identifier }))
+ },
+ [BiomarkerType.METHYLATION]: {
+ isLoading: false,
+ data: biomarker.methylations.map(item => ({ isValid: true, value: item.identifier }))
+ },
+ [BiomarkerType.MRNA]: {
+ isLoading: false,
+ data: biomarker.mrnas.map(item => ({ isValid: true, value: item.identifier }))
+ }
+ },
+ validation: {
+ haveAmbiguous: false,
+ haveInvalid: false,
+ isLoading: false,
+ checkBox: false
+ },
+ moleculesSymbolsFinder: {
+ isLoading: false,
+ data: []
+ }
+ }
+ })
+ })
+ }
+
+ handleSelectAllBiomarker = () => {
+ const { experimentInfo } = this.props
+
+ const allMolecules = experimentInfo.rows
+
+ this.setState({
+ biomarkerTypeSelected: BiomarkerOrigin.MANUAL,
+ openCreateEditBiomarkerModal: true,
+ formBiomarker: {
+ id: null,
+ canEditMolecules: true,
+ biomarkerName: '',
+ biomarkerDescription: '',
+ tag: '',
+ moleculeSelected: BiomarkerType.MRNA,
+ moleculesTypeOfSelection: MoleculesTypeOfSelection.INPUT,
+ moleculesSection: {
+ [BiomarkerType.CNA]: { isLoading: false, data: [] },
+ [BiomarkerType.MIRNA]: {
+ isLoading: false,
+ data: allMolecules.map(item => ({
+ isValid: true,
+ value: item.gem
+ }))
+ },
+ [BiomarkerType.METHYLATION]: { isLoading: false, data: [] },
+ [BiomarkerType.MRNA]: {
+ isLoading: false,
+ data: allMolecules.map(item => ({
+ isValid: true,
+ value: item.gene
+ }))
+ }
+ },
+ validation: {
+ haveAmbiguous: false,
+ haveInvalid: false,
+ isLoading: false,
+ checkBox: false
+ },
+ moleculesSymbolsFinder: {
+ isLoading: false,
+ data: []
+ }
+ }
+ })
+ }
+
+ buildFormBiomarkerFromCurrentPageFilteredRows = () => {
+ const filteredRows = this.getFilteredMoleculesCurrentPage()
+ const { experimentInfo, tableControl } = this.props
+
+ // Verify if there are filters applied
+ const hasFilters = Object.values(tableControl.filters).some(f => f.value !== null && f.value !== undefined && f.value !== '')
+
+ // If there are filters applied, use the filtered rows, otherwise use all the rows
+ const rowsToUse = hasFilters ? filteredRows : experimentInfo.rows
+
+ const genes = rowsToUse.map(r => r.gene)
+ const gems = rowsToUse.map(r => r.gem)
+
+ return {
+ ...this.getDefaultFormBiomarker(),
+ moleculesSection: {
+ [BiomarkerType.MRNA]: { isLoading: false, data: genes.map(g => ({ value: g, isValid: true })) },
+ [BiomarkerType.MIRNA]: { isLoading: false, data: gems.map(g => ({ value: g, isValid: true })) },
+ [BiomarkerType.CNA]: { isLoading: false, data: [] },
+ [BiomarkerType.METHYLATION]: { isLoading: false, data: [] }
+ }
+ }
+ }
+
+ getFilteredMoleculesCurrentPage = (): DjangoMRNAxGEMResultRow[] => {
+ const { experimentInfo, tableControl } = this.props
+ const rows = experimentInfo.rows
+
+ return rows.filter(row => {
+ return Object.entries(tableControl.filters).every(([key, filter]) => {
+ const filterValue = filter.value
+
+ if (filterValue === null || filterValue === undefined || filterValue === '') { return true }
+
+ return row[key as keyof DjangoMRNAxGEMResultRow] === filterValue
+ })
+ })
+ }
+
+ /**
+ * Method that get symbols while user is writing in Select molecules input
+ * @param query string that is sending to the api
+ */
+ handleGenesSymbolsFinder = (query: string): void => {
+ // loading aca
+ const formBiomarkerPreLoad = this.state.formBiomarker
+ formBiomarkerPreLoad.moleculesSymbolsFinder.isLoading = true
+ let urlToFind = urlGeneSymbolsFinder
+
+ switch (this.state.formBiomarker.moleculeSelected) {
+ case BiomarkerType.MIRNA:
+ urlToFind = urlMiRNACodesFinder
+ break
+ case BiomarkerType.METHYLATION:
+ urlToFind = urlMethylationSitesFinder
+ break
+ default:
+ break
+ }
+
+ this.setState({ formBiomarker: formBiomarkerPreLoad })
+ ky.get(urlToFind, { searchParams: { query, limit: 5 }, signal: this.abortController.signal, timeout: REQUEST_TIMEOUT }).then((response) => {
+ response.json().then((jsonResponse: MoleculeFinderResult[]) => {
+ const formBiomarker = this.state.formBiomarker
+ const checkedIgnoreProposedAlias = this.state.checkedIgnoreProposedAlias // For short
+
+ formBiomarker.moleculesSymbolsFinder.data = jsonResponse.map(molecule => {
+ const text = checkedIgnoreProposedAlias || molecule.molecule === molecule.standard
+ ? molecule.molecule
+ : `${molecule.molecule} (${molecule.standard})`
+
+ return {
+ key: molecule.molecule,
+ text,
+ value: checkedIgnoreProposedAlias ? molecule.molecule : molecule.standard
+ }
+ })
+ this.setState({ formBiomarker })
+ }).catch((err) => {
+ console.error('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ console.error('Error getting genes ->', err)
+ }).finally(() => {
+ if (!this.abortController.signal.aborted) {
+ const formBiomarker = this.state.formBiomarker
+ formBiomarker.moleculesSymbolsFinder.isLoading = false
+ this.setState({ formBiomarker })
+ }
+ })
+ }
+
+ /**
+ * Method that removes invalid genes of the sector selected
+ * @param sector string of the sector selected to change state
+ */
+ handleRemoveInvalidGenes = (sector: BiomarkerType): void => {
+ this.setState({
+ ...this.state,
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesSection: {
+ ...this.state.formBiomarker.moleculesSection,
+ [sector]: {
+ ...this.state.formBiomarker.moleculesSection[sector],
+ data: this.state.formBiomarker.moleculesSection[sector].data.filter(gen => gen.isValid || Array.isArray(gen.value))
+ }
+ }
+ }
+ })
+ }
+
+ /**
+ * Method that removes all the molecules of the sector selecterd
+ * @param sector string of the sector selected to change state
+ */
+ handleRestartSection = (sector: BiomarkerType): void => {
+ this.setState({
+ ...this.state,
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesSection: {
+ ...this.state.formBiomarker.moleculesSection,
+ [sector]: {
+ ...this.state.formBiomarker.moleculesSection[sector],
+ data: []
+ }
+ }
+ }
+ })
+ }
+
+ /**
+ * Order data to show in the section.
+ * @param data Data to order.
+ * @returns Ordered data.
+ */
+ orderData = (data: MoleculesSectionData[]): MoleculesSectionData[] => {
+ return data.sort((a, b) => {
+ const cond = Number(a.isValid) - Number(b.isValid)
+
+ if (cond !== 0) {
+ return cond
+ }
+
+ return Array.isArray(a.value) ? 1 : -1
+ })
+ }
+
+ /**
+ * Sets a list of molecules to the current selected section.
+ * @param moleculesList List of molecules to set.
+ */
+ setMoleculesToSelectedSection = (moleculesList: MoleculesSectionData[]) => {
+ const moleculeTypeSelected = this.state.formBiomarker.moleculeSelected
+
+ // Sets loading in false
+ const moleculesSection = {
+ ...this.state.formBiomarker.moleculesSection,
+ [moleculeTypeSelected]: {
+ isLoading: false,
+ data: this.orderData([...this.state.formBiomarker.moleculesSection[moleculeTypeSelected].data].concat(moleculesList))
+ }
+ }
+
+ const newFormBiomarker: FormBiomarkerData = {
+ ...this.state.formBiomarker,
+ moleculesSection
+ }
+
+ newFormBiomarker.moleculesSymbolsFinder.isLoading = false
+ newFormBiomarker.moleculesSection[moleculeTypeSelected].isLoading = false
+
+ this.setState({ formBiomarker: newFormBiomarker })
+ }
+
+ /**
+ * Method that gets symbols while user is writing in Select molecules input
+ * @param molecules array of strings that is sending to the api
+ */
+ handleGeneSymbols = async (molecules: string[]): Promise => {
+ const moleculesSectionPreload = {
+ ...this.state.formBiomarker.moleculesSection,
+ [this.state.formBiomarker.moleculeSelected]: {
+ isLoading: true,
+ data: [...this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data]
+ }
+ }
+ this.setState({
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesSymbolsFinder: {
+ ...this.state.formBiomarker.moleculesSymbolsFinder,
+ isLoading: true
+ },
+ moleculesSection: moleculesSectionPreload
+ }
+ })
+ let urlToFind: string
+ let json: { [key: string]: string[] }
+ let keyMolecules: string
+
+ switch (this.state.formBiomarker.moleculeSelected) {
+ case BiomarkerType.MIRNA:
+ urlToFind = urlMiRNACodes
+ json = { mirna_codes: molecules }
+ keyMolecules = 'mirna_codes'
+ break
+ case BiomarkerType.METHYLATION:
+ urlToFind = urlMethylationSites
+ json = { methylation_sites: molecules }
+ keyMolecules = 'methylation_sites'
+ break
+ default:
+ urlToFind = urlGeneSymbols
+ json = { gene_ids: molecules }
+ keyMolecules = 'gene_ids'
+ break
+ }
+
+ const genesArray: MoleculesSectionData[] = []
+ ky.post(urlToFind, { headers: getDjangoHeader(), json, timeout: REQUEST_TIMEOUT }).then((response) => {
+ response.json().then((jsonResponse: { [key: string]: string[] }) => {
+ const genes = Object.entries(jsonResponse)
+
+ for (const gene of genes) {
+ let condition
+
+ switch (gene[1].length) {
+ case 0:
+ condition = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.concat(genesArray).filter(item => item.value === gene[0])
+
+ if (!condition.length) {
+ genesArray.push({
+ isValid: false,
+ value: gene[0]
+ })
+ }
+
+ break
+ case 1:
+ condition = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.concat(genesArray).filter(item => item.value === gene[1][0])
+
+ if (!condition.length) {
+ genesArray.push({
+ isValid: true,
+ value: gene[1][0]
+ })
+ }
+
+ break
+ default:
+ condition = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.concat(genesArray).filter(
+ item => isEqual(item.value, gene[1])
+ )
+
+ if (!condition.length) {
+ genesArray.push({
+ isValid: false,
+ value: gene[1]
+ })
+ }
+
+ break
+ }
+ }
+ }).catch((err) => {
+ console.error('Error parsing JSON ->', err)
+ console.warn('Setting all molecules as invalid to show warning')
+
+ json[keyMolecules].forEach(molecule => {
+ genesArray.push({
+ isValid: false,
+ value: molecule
+ })
+ })
+ }).finally(() => {
+ this.setMoleculesToSelectedSection(genesArray)
+ })
+ }).catch((err) => {
+ console.error('Error getting molecules ->', err)
+ console.warn('Setting all molecules as invalid to show warning')
+
+ json[keyMolecules].forEach(molecule => {
+ genesArray.push({
+ isValid: false,
+ value: molecule
+ })
+ })
+ }).finally(() => {
+ this.setMoleculesToSelectedSection(genesArray)
+ })
+ }
+
+ /**
+ * Generates a default formBiomarker
+ * @returns Default FormBiomarkerData object
+ */
+ getDefaultFormBiomarker (): FormBiomarkerData {
+ return {
+ id: null,
+ biomarkerName: '',
+ biomarkerDescription: '',
+ canEditMolecules: true,
+ tag: null,
+ moleculeSelected: BiomarkerType.MRNA,
+ moleculesTypeOfSelection: MoleculesTypeOfSelection.INPUT,
+ validation: {
+ haveAmbiguous: false,
+ haveInvalid: false,
+ isLoading: false,
+ checkBox: false
+ },
+ moleculesSection: {
+ [BiomarkerType.CNA]: {
+ isLoading: false,
+ data: []
+ },
+ [BiomarkerType.MIRNA]: {
+ isLoading: false,
+ data: []
+ },
+ [BiomarkerType.METHYLATION]: {
+ isLoading: false,
+ data: []
+ },
+ [BiomarkerType.MRNA]: {
+ isLoading: false,
+ data: []
+ }
+ },
+ moleculesSymbolsFinder: {
+ isLoading: false,
+ data: []
+ }
+ }
+ }
+
+ /**
+ * Updates checkbox status
+ * @param value new value to set
+ */
+ handleChangeCheckBox = (value: boolean) => {
+ const formBiomarker = this.state.formBiomarker
+ formBiomarker.validation.checkBox = value
+ this.setState({ formBiomarker })
+ }
+
+ /**
+ * Validates if the form is correct, if not change state of labels alerts bars
+ * @returns Some flags indicating if the form is valid or not
+ */
+ handleValidateForm = (): ValidationForm => {
+ let haveAmbiguous = false
+ let haveInvalid = false
+
+ for (const option of Object.values(BiomarkerType)) {
+ if (!haveAmbiguous) {
+ const indexOfAmbiguous = this.state.formBiomarker.moleculesSection[option].data.findIndex(item => !item.isValid && Array.isArray(item.value))
+
+ if (indexOfAmbiguous >= 0) {
+ haveAmbiguous = true
+ }
+ }
+
+ if (!haveInvalid && !this.state.formBiomarker.validation.checkBox) {
+ const indexOfInvalid = this.state.formBiomarker.moleculesSection[option].data.findIndex(item => !item.isValid && !Array.isArray(item.value))
+
+ if (indexOfInvalid >= 0) {
+ haveInvalid = true
+ }
+ }
+ }
+
+ return {
+ haveAmbiguous,
+ haveInvalid
+ }
+ }
+
+ /**
+ * Checks if it's a valid structure to send the molecule to backend and create the Biomarker.
+ * @param item Molecule to send.
+ * @returns True if it's valid, false if not.
+ */
+ moleculeIdentifierIsValid = (item: MoleculesSectionData): boolean => !Array.isArray(item.value) && item.isValid
+
+ /**
+ * Generates a valid structure to send the molecule to backend and create the Biomarker
+ * @param item Molecule to send
+ * @returns Correct structure to send
+ */
+ moleculeIdentified = (item: MoleculesSectionData): SaveMoleculeStructure => ({
+ identifier: item.value as string
+ })
+
+ /**
+ * Generates a valid structure to send the molecules to backend and create the Biomarker checking if the
+ * "Ignore errors" checkbox is checked or not.
+ * @param molecules Molecules to send.
+ * @returns Correct structure to send.
+ */
+ getMoleculesData = (molecules: MoleculesSectionData[]): SaveMoleculeStructure[] => {
+ const ignoreErrors = this.state.formBiomarker.validation.checkBox
+
+ if (ignoreErrors) {
+ return molecules.map(this.moleculeIdentified)
+ } else {
+ return molecules.filter(this.moleculeIdentifierIsValid).map(this.moleculeIdentified)
+ }
+ }
+
+ /**
+ * Makes the request to create a Biomarker
+ */
+ handleSendForm = () => {
+ const formBiomarker = this.state.formBiomarker
+ formBiomarker.validation.isLoading = true
+ this.setState({ formBiomarker })
+
+ // Gets name and description
+ const simpleBiomarker: BiomarkerNameAndDesc = {
+ name: formBiomarker.biomarkerName,
+ description: formBiomarker.biomarkerDescription
+ }
+
+ // Adds molecules if needed
+ const biomarkerToSend: SaveBiomarkerStructure | BiomarkerNameAndDesc = formBiomarker.canEditMolecules
+ ? {
+ ...simpleBiomarker,
+ mrnas: this.getMoleculesData(formBiomarker.moleculesSection.mRNA.data),
+ mirnas: this.getMoleculesData(formBiomarker.moleculesSection.miRNA.data),
+ cnas: this.getMoleculesData(formBiomarker.moleculesSection.CNA.data),
+ methylations: this.getMoleculesData(formBiomarker.moleculesSection.Methylation.data)
+ }
+ : simpleBiomarker
+
+ const settings: Options = {
+ headers: getDjangoHeader(),
+ json: biomarkerToSend,
+ timeout: REQUEST_TIMEOUT
+ }
+
+ // Checks if it's a creation or an update
+ if (!formBiomarker.id) {
+ ky.post(urlBiomarkersCreate, settings).then((response) => {
+ response.json().then((_jsonResponse: Biomarker) => {
+ this.closeModalWithSuccessMsg('Biomarker created successfully')
+ }).catch((err) => {
+ console.log('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ console.log('Error adding Biomarker ->', err)
+ const alert = this.state.alert
+ alert.isOpen = true
+ alert.type = CustomAlertTypes.ERROR
+ alert.message = 'Error creating biomarker!'
+ this.setState({ alert })
+ }).finally(() => {
+ formBiomarker.validation.isLoading = false
+ this.setState({ formBiomarker })
+ })
+ } else {
+ const url = formBiomarker.canEditMolecules ? urlBiomarkersCRUD : urlBiomarkersSimpleUpdate
+ ky.patch(`${url}/${formBiomarker.id}/`, settings).then((response) => {
+ response.json().then((_jsonResponse: Biomarker) => {
+ this.closeModalWithSuccessMsg('Biomarker edited successfully')
+ }).catch((err) => {
+ console.log('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ console.log('Error getting genes ->', err)
+ const alert = this.state.alert
+ alert.isOpen = true
+ alert.type = CustomAlertTypes.ERROR
+ alert.message = 'Error editing biomarker!'
+ this.setState({ alert })
+ }).finally(() => {
+ formBiomarker.validation.isLoading = false
+ this.setState({ formBiomarker })
+ })
+ }
+ }
+
+ /**
+ * change name or description of manual form
+ * @param value new value for input form
+ * @param name type of input to change
+ */
+ handleChangeInputForm = (value: string, name: 'biomarkerName' | 'biomarkerDescription') => {
+ const formBiomarker = this.state.formBiomarker
+ formBiomarker[name] = value
+ this.setState({ formBiomarker })
+ }
+
+ /**
+ * Handles the table's control filters, select, etc changes
+ * @param value Value to set to the state moleculeSelected in formBiomarkerState
+ */
+ handleChangeMoleculeSelected = (value: BiomarkerType) => {
+ const formBiomarker = this.state.formBiomarker
+ formBiomarker.moleculeSelected = value
+ formBiomarker.moleculesSymbolsFinder.data = []
+ this.setState({
+ formBiomarker
+ })
+ }
+
+ /**
+ * Handles the table's control filters, select, etc changes
+ * @param value Value to set to the state moleculesTypeOfSelection in formBiomarkerState
+ */
+ handleChangeMoleculeInputSelected = (value: MoleculesTypeOfSelection) => {
+ this.setState({
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesTypeOfSelection: value
+ }
+ })
+ }
+
+ /**
+ * Handles the table's control filters, select, etc changes
+ * @param value Value to add to the molecules section that is selected
+ */
+ handleAddMoleculeToSection = (value: MoleculesSectionData) => {
+ const genesSymbolsFinder = this.state.formBiomarker.moleculesSymbolsFinder
+ genesSymbolsFinder.data = []
+ this.setState({
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesSymbolsFinder: genesSymbolsFinder
+ }
+ })
+
+ const sectionFound = this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data.find((item: MoleculesSectionData) => value.value === item.value)
+
+ if (sectionFound !== undefined) {
+ return
+ }
+
+ const moleculesSection = {
+ ...this.state.formBiomarker.moleculesSection,
+ [this.state.formBiomarker.moleculeSelected]: {
+ isLoading: false,
+ data: [...this.state.formBiomarker.moleculesSection[this.state.formBiomarker.moleculeSelected].data, value]
+ }
+ }
+ this.setState({
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesSection
+ }
+ })
+ }
+
+ /**
+ * Handles the table's control filters, select, etc changes
+ * @param section Value to add to the molecules section that is selected
+ * @param molecule molecule to remove of the array
+ */
+ handleRemoveMolecule = (section: BiomarkerType, molecule: MoleculesSectionData) => {
+ // keeps the molecules that are not the one that is going to be removed
+ const data = this.state.formBiomarker.moleculesSection[section].data.filter((item: MoleculesSectionData) => {
+ return item.value !== molecule.value
+ })
+
+ this.setState({
+ ...this.state,
+ formBiomarker: {
+ ...this.state.formBiomarker,
+ moleculesSection: {
+ ...this.state.formBiomarker.moleculesSection,
+ [section]: {
+ isLoading: false,
+ data
+ }
+ }
+ }
+ })
+ }
+
+ /**
+ * Generates a default new file form
+ * @returns An object with all the field with default values
+ */
+ getDefaultNewBiomarker (): Biomarker {
+ return {
+ id: null,
+ name: '',
+ description: '',
+ tag: null,
+ number_of_mrnas: 0,
+ number_of_mirnas: 0,
+ number_of_cnas: 0,
+ number_of_methylations: 0,
+ has_fs_experiment: false,
+ was_already_used: false,
+ origin: BiomarkerOrigin.BASE,
+ state: BiomarkerState.COMPLETED,
+ contains_nan_values: false,
+ column_used_as_index: '',
+ methylations: [],
+ mirnas: [],
+ cnas: [],
+ mrnas: [],
+ user: {
+ id: 0,
+ username: ''
+ },
+ is_public: false,
+ }
+ }
+
+ /**
+ * Cleans the new/edit biomarker form
+ */
+ cleanForm = () => {
+ this.setState({
+ openCreateEditBiomarkerModal: true,
+ formBiomarker: this.getDefaultFormBiomarker(),
+ confirmModal: this.getDefaultConfirmModal()
+ })
+ }
+
+ /**
+ * Show a modal to confirm a Biomarker deletion
+ * @param biomarker Selected Biomarker to delete
+ */
+ confirmBiomarkerDeletion = (biomarker: BiomarkerSimple) => {
+ this.setState({
+ selectedBiomarkerToDeleteOrSync: biomarker,
+ showDeleteBiomarkerModal: true
+ })
+ }
+
+ /** Closes the deletion confirm modals. */
+ handleClose = () => {
+ this.setState({ showDeleteBiomarkerModal: false })
+ }
+
+ /**
+ * Check if can submit the new Biomarker form
+ * @returns True if everything is OK, false otherwise
+ */
+ canSubmitBiomarkerForm = (): boolean => {
+ return !this.state.addingOrEditingBiomarker &&
+ this.state.newBiomarker.name.trim().length > 0
+ }
+
+ /**
+ * Handles Biomarker form changes
+ * @param name Name of the state field to modify
+ * @param value Value to set to the state field
+ */
+ handleFormChanges = (name: string, value) => {
+ const newBiomarker = this.state.newBiomarker
+ newBiomarker[name] = value
+ this.setState({ newBiomarker })
+ }
+
+ /**
+ * TODO: Check if needed
+ * Adds a Survival data tuple for a CGDSDataset
+ * @param datasetName Name of the edited CGDS dataset
+ */
+ addSurvivalFormTuple = (datasetName: NameOfCGDSDataset) => {
+ const newBiomarker = this.state.newBiomarker
+ const dataset = newBiomarker[datasetName]
+
+ if (dataset !== null) {
+ const newElement: DjangoSurvivalColumnsTupleSimple = { event_column: '', time_column: '' }
+
+ if (dataset.survival_columns === undefined) {
+ dataset.survival_columns = []
+ }
+
+ dataset.survival_columns.push(newElement)
+ this.setState({ newBiomarker })
+ }
+ }
+
+ /**
+ * TODO: Check if needed
+ * Removes a Survival data tuple for a CGDSDataset
+ * @param datasetName Name of the edited CGDS dataset
+ * @param idxSurvivalTuple Index in survival tuple
+ */
+ removeSurvivalFormTuple = (datasetName: NameOfCGDSDataset, idxSurvivalTuple: number) => {
+ const newBiomarker = this.state.newBiomarker
+ const dataset = newBiomarker[datasetName]
+
+ if (dataset !== null && dataset.survival_columns !== undefined) {
+ dataset.survival_columns.splice(idxSurvivalTuple, 1)
+ this.setState({ newBiomarker })
+ }
+ }
+
+ /**
+ * TODO: Check if needed
+ * Handles CGDS Dataset form changes in fields of Survival data tuples
+ * @param datasetName Name of the edited CGDS dataset
+ * @param idxSurvivalTuple Index in survival tuple
+ * @param name Field of the CGDS dataset to change
+ * @param value Value to assign to the specified field
+ */
+ handleSurvivalFormDatasetChanges = (
+ datasetName: NameOfCGDSDataset,
+ idxSurvivalTuple: number,
+ name: string,
+ value: any
+ ) => {
+ const newBiomarker = this.state.newBiomarker
+ const dataset = newBiomarker[datasetName]
+
+ if (dataset !== null && dataset.survival_columns !== undefined) {
+ dataset.survival_columns[idxSurvivalTuple][name] = value
+ this.setState({ newBiomarker })
+ }
+ }
+
+ /**
+ * Checks if the form is entirely empty. Useful to enable 'Cancel' button
+ * @returns True is any of the form's field contains any data. False otherwise
+ */
+ isFormEmpty = (): boolean => isEqual(this.state.formBiomarker, this.getDefaultFormBiomarker())
+
+ /**
+ * Callback to mark a Biomarker as selected
+ * @param biomarker Selected biomarker to mark
+ */
+ markBiomarkerAsSelected = (biomarker: Biomarker) => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.selectedBiomarker = biomarker
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Function to complete step 2
+ */
+ handleCompleteStep2 = () => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.step = 3
+ this.setState({ featureSelection })
+ }
+
+ /**
+ * Function to go back to step 1
+ */
+ handleGoBackStep1 = () => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.clinicalSource = getDefaultSource()
+ featureSelection.mRNASource = getDefaultSource()
+ featureSelection.mirnaSource = getDefaultSource()
+ featureSelection.methylationSource = getDefaultSource()
+ featureSelection.cnaSource = getDefaultSource()
+ featureSelection.step = 1
+ this.setState({ featureSelection })
+ }
+
+ /** Closes the modal to confirm a Biomarker cloning. */
+ closeModalToClone = () => { this.setState({ biomarkerToClone: null }) }
+
+ /**
+ * Function to go back to step 2
+ */
+ handleGoBackStep2 = () => {
+ const featureSelection = this.state.featureSelection
+ featureSelection.step = 2
+ featureSelection.algorithm = FeatureSelectionAlgorithm.BLIND_SEARCH
+ this.setState({ featureSelection })
+ }
+
+ handleConfirm = () => {
+ const { selectedOption } = this.state
+
+ this.setState({ openSelectOptionModal: false }, () => {
+ if (selectedOption === 'selectAll') {
+ // ver de si hacer un endpoint que traiga todos los genes y no solo los de la pagina
+ this.handleSelectAllBiomarker()
+ } else {
+ // Select with Filters: usamos solo la página actual y filtros
+ this.setState({
+ openCreateEditBiomarkerModal: true,
+ formBiomarker: this.buildFormBiomarkerFromCurrentPageFilteredRows()
+ })
+ }
+ })
+ }
+
+ /**
+ * Closes the modal and shows a successful Semantic-UI Alert message.
+ * @param msg Message to show.
+ */
+ closeModalWithSuccessMsg = (msg: string) => {
+ const alert = this.state.alert
+ alert.isOpen = true
+ alert.type = CustomAlertTypes.SUCCESS
+ alert.message = msg
+ this.setState({
+ alert,
+ formBiomarker: this.getDefaultFormBiomarker(),
+ openCreateEditBiomarkerModal: false,
+ confirmModal: this.getDefaultConfirmModal(),
+ biomarkerTypeSelected: BiomarkerOrigin.BASE
+ })
+ }
+
+ /**
+ * Generates default table's Filters.
+ * @returns Default object for table's Filters
+ */
+ getDefaultFilters (): PaginationCustomFilter[] {
+ const tagOptions: DropdownItemProps[] = this.state.tags.map((tag) => {
+ const id = tag.id as number
+ return { key: id, value: id, text: tag.name }
+ })
+
+ tagOptions.unshift({ key: 'no_tag', text: 'No tag' })
+
+ // TODO: refactor Tag key as it's the same as AllExperimentsView.tsx and UserFilesView.tsx
+ return [
+ { label: 'Tag', keyForServer: 'tag', defaultValue: '', placeholder: 'Select an existing Tag', options: tagOptions, width: 3 }
+ ]
+ }
+
+ closeBiomarkerModal = () => {
+ this.setState({
+ formBiomarker: this.getDefaultFormBiomarker(),
+ featureSelection: this.getDefaultFeatureSelectionProps(),
+ openCreateEditBiomarkerModal: false,
+ confirmModal: this.getDefaultConfirmModal(),
+ biomarkerTypeSelected: BiomarkerOrigin.BASE
+ })
+ }
+
+ buildFormBiomarkerFromFilteredRows = () => {
+ const { rows } = this.props.experimentInfo
+
+ return {
+ ...this.getDefaultFormBiomarker(),
+ moleculesSection: {
+ [BiomarkerType.MRNA]: {
+ isLoading: false,
+ data: rows.map(item => ({ value: item.gene, isValid: true }))
+ },
+ [BiomarkerType.MIRNA]: {
+ isLoading: false,
+ data: rows.map(item => ({ value: item.gem, isValid: true }))
+ },
+ // los otros tipos vacíos
+ [BiomarkerType.CNA]: { isLoading: false, data: [] },
+ [BiomarkerType.METHYLATION]: { isLoading: false, data: [] }
+ }
+ }
+ }
+
+ render () {
+ const { openSelectOptionModal, selectedOption } = this.state
+ return (
+ <>
+ this.setState({
+ formBiomarker: this.getDefaultFormBiomarker(),
+ openSelectOptionModal: true
+ })}
+ />
+
+ {/* Modal con opciones */}
+ this.setState({ openSelectOptionModal: false })}
+ >
+ Create Biomarker
+
+ Select how you want to build the biomarker:
+
+ this.setState({ selectedOption: value as SelectedOption })}
+ />
+ this.setState({ selectedOption: value as SelectedOption })}
+ />
+
+
+
+
+
+
+
+
+
+ {/* Modal con ManualForm */}
+ }
+ closeOnEscape={false}
+ closeOnDimmerClick={false}
+ closeOnDocumentClick={false}
+ className='space-modal large-modal'
+ onClose={() => this.closeBiomarkerModal()}
+ >
+
+
+ >
+ )
+ }
+}
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
new file mode 100644
index 00000000..f04f7445
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
@@ -0,0 +1,90 @@
+import React, { useState } from 'react'
+import { Form, Modal, Button, Icon, ButtonProps } from 'semantic-ui-react'
+import { ExperimentInfo } from '../../../utils/interfaces'
+
+type BiomarkerOption = 'selectAll' | 'selectWithFilters'
+
+interface CreateBiomarkerButtonProps extends Partial {
+ experimentInfo: ExperimentInfo
+ onCreateBiomarker: (options: {
+ experimentInfo: ExperimentInfo;
+ selectAll: boolean;
+ }) => void;
+}
+
+const CreateBiomarkerButton: React.FC = ({
+ experimentInfo,
+ onCreateBiomarker,
+ ...buttonProps // <-- todas las props extra van acá
+}) => {
+ const [open, setOpen] = useState(false)
+ const [selectedOption, setSelectedOption] = useState('selectAll')
+
+ const handleConfirm = () => {
+ onCreateBiomarker({
+ experimentInfo,
+ selectAll: selectedOption === 'selectAll',
+ })
+ setOpen(false)
+ }
+
+ return (
+ <>
+ {/* Botón que abre el modal */}
+
+
+ {/* Modal con opciones */}
+ setOpen(false)}
+ >
+ Create Biomarker
+
+ Select how you want to build the biomarker:
+
+ setSelectedOption(value as BiomarkerOption)}
+ />
+ setSelectedOption(value as BiomarkerOption)}
+ />
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default CreateBiomarkerButton
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
index 3f1fd12c..d9cbcba7 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
@@ -5,6 +5,7 @@ import { InfoPopup } from './gene-gem-details/InfoPopup'
import { generatesOrderingQueryMultiField } from '../../../utils/util_functions'
import { DjangoExperiment } from '../../../utils/django_interfaces'
import { SingleRangeSlider } from 'neo-react-semantic-ui-range'
+import { BiomarkerFromCorrelationModal } from './BiomarkerFromCorrelationModal'
declare const urlDownloadResultWithFilters: string
@@ -29,9 +30,9 @@ interface ResultTableControlFormProps {
/** Callback for changes in (adjusted) p-values precision only */
changePrecisionState: (showHighPrecision: boolean) => void,
/** Callback for reset filters and sorting */
- resetFiltersAndSorting: (experiment: DjangoExperiment) => void
+ resetFiltersAndSorting: (experiment: DjangoExperiment) => void,
/** Callback for reset only filters */
- resetFilters: (experiment: ExperimentInfo) => void
+ resetFilters: (experiment: ExperimentInfo) => void,
}
/**
@@ -70,140 +71,152 @@ export const ResultTableControlForm = (props: ResultTableControlFormProps) => {
}
return (
-
- {/* Number of showing/total combinations */}
-
-
-
-
-
- {props.numberOfShowingCombinations} / {props.totalNumberOfCombinations}
-
-
- SHOWING/TOTAL
-
-
-
-
-
- {/* mRNA/MiRNA search */}
- props.onHandleTableControlChanges(name, value)}
- />
-
- {/* Correlation threshold */}
-
-
-
- props.onHandleTableControlChanges('coefficientThreshold', value)}
- />
-
-
-
-
-
- {/* Correlation type */}
- props.onHandleTableControlChanges(name, value)}
- />
-
- {/* Page size */}
- props.onHandleTableControlChanges(name, value)}
- />
-
- props.changePrecisionState(!isShowingHighPrecision)}
- />
-
- props.resetFilters(props.experimentInfo)}
- />
-
- props.resetFiltersAndSorting(props.experimentInfo.experiment)}
- />
-
- window.open(generateDownloadWithFiltersQuery(), '_blank')}
- disabled={!props.experimentInfo.rows.length}
- />
-
-
-
- )}
- />
-
-
-
+ <>
+
+
+
+ {/* Number of showing/total combinations */}
+
+
+
+
+
+ {props.numberOfShowingCombinations} / {props.totalNumberOfCombinations}
+
+
+ SHOWING/TOTAL
+
+
+
+
+
+ {/* mRNA/MiRNA search */}
+ props.onHandleTableControlChanges(name, value)}
+ className='no-margin-right-form-field'
+ />
+
+ {/* Correlation threshold */}
+
+
+
+ props.onHandleTableControlChanges('coefficientThreshold', value)}
+ />
+
+
+
+
+
+ {/* Correlation type */}
+ props.onHandleTableControlChanges(name, value)}
+ className='no-margin-right-form-field'
+ />
+
+ {/* Page size */}
+ props.onHandleTableControlChanges(name, value)}
+ className='no-margin-right-form-field'
+ />
+
+ props.changePrecisionState(!isShowingHighPrecision)}
+ />
+
+ {
+ console.log('filtros del biomarker', {
+ selectAll,
+ experimentInfo,
+ tableControl
+ })
+ }}
+ />
+
+ props.resetFiltersAndSorting(props.experimentInfo.experiment)}
+ />
+
+ window.open(generateDownloadWithFiltersQuery(), '_blank')}
+ disabled={!props.experimentInfo.rows.length}
+ />
+
+
+
+
+
+
+ >
+
)
}
diff --git a/src/frontend/static/frontend/src/css/gem.css b/src/frontend/static/frontend/src/css/gem.css
index 27c3edfb..9ba6b3b1 100644
--- a/src/frontend/static/frontend/src/css/gem.css
+++ b/src/frontend/static/frontend/src/css/gem.css
@@ -123,7 +123,10 @@
}
.no-margin-right-form-field * {
- margin-right: 0 !important;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ align-items: center;
}
#sorting-order-data {
From ec71d8d1254079e6669e219d52901ec652fb2a85 Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Sun, 15 Feb 2026 20:22:38 -0300
Subject: [PATCH 02/16] Fix bugs in obtaining filtered and unfiltered results
---
.../BiomarkerFromCorrelationModal.tsx | 27 ++++++++++---------
1 file changed, 15 insertions(+), 12 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
index 5fd6b445..9453b86b 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
@@ -85,7 +85,8 @@ interface BiomarkerFromCorrelationModalState {
submittingFSExperiment: boolean,
openDetailsModal2: boolean,
selectedOption: SelectedOption,
- openSelectOptionModal: boolean
+ openSelectOptionModal: boolean,
+ experimentInfoWithoutFilters: ExperimentInfo
}
/**
@@ -120,7 +121,11 @@ export class BiomarkerFromCorrelationModal extends React.Component {
- const { experimentInfo } = this.props
-
- const allMolecules = experimentInfo.rows
+ const allMolecules = this.state.experimentInfoWithoutFilters.rows
+ console.log('handleSelectAllBiomarker Filtrado', this.props.experimentInfo)
+ console.log('handleSelectAllBiomarker Sin filtrar', allMolecules)
this.setState({
biomarkerTypeSelected: BiomarkerOrigin.MANUAL,
openCreateEditBiomarkerModal: true,
@@ -1215,12 +1220,7 @@ export class BiomarkerFromCorrelationModal extends React.Component this.closeBiomarkerModal()}
>
+
>
)
}
From 72a7c1d4ea44d64630d8dba297147352dbabdad4 Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Sun, 15 Feb 2026 20:23:40 -0300
Subject: [PATCH 03/16] Normalization
---
.../pipeline/experiment-result/CreateBiomarkerButton.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
index f04f7445..a7a29ae7 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
@@ -40,7 +40,7 @@ const CreateBiomarkerButton: React.FC = ({
className='space-modal large-modal'
labelPosition='left'
onClick={() => setOpen(true)}
- {...buttonProps} // <-- se propagan las props aquí
+ {...buttonProps}
>
Create Biomarker
@@ -49,7 +49,8 @@ const CreateBiomarkerButton: React.FC = ({
{/* Modal con opciones */}
setOpen(false)}
>
Create Biomarker
From 39f052827c459b5c48e8f25c70dcd2fc11fe1e0f Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Sun, 15 Feb 2026 20:28:58 -0300
Subject: [PATCH 04/16] Normalization
---
.../experiment-result/BiomarkerFromCorrelationModal.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
index 9453b86b..2189b025 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
@@ -570,8 +570,6 @@ export class BiomarkerFromCorrelationModal extends React.Component {
const allMolecules = this.state.experimentInfoWithoutFilters.rows
- console.log('handleSelectAllBiomarker Filtrado', this.props.experimentInfo)
- console.log('handleSelectAllBiomarker Sin filtrar', allMolecules)
this.setState({
biomarkerTypeSelected: BiomarkerOrigin.MANUAL,
openCreateEditBiomarkerModal: true,
From 7fb322167a2b5c15bee7917bb2e2c7c89fad6e4f Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Sun, 15 Feb 2026 22:13:31 -0300
Subject: [PATCH 05/16] fix bugs in biomarker insertions
---
.../BiomarkerFromCorrelationModal.tsx | 41 +++++++++++++++++--
.../CreateBiomarkerButton.tsx | 5 +--
.../ResultTableControlForm.tsx | 8 +---
src/frontend/templates/frontend/gem.html | 9 ++++
src/frontend/views.py | 24 ++++++++++-
5 files changed, 73 insertions(+), 14 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
index 2189b025..e11a536d 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
@@ -1,6 +1,6 @@
import React from 'react'
// Update the import path to the correct location of Base component
-import { Modal, DropdownItemProps, Icon, Form, Button } from 'semantic-ui-react'
+import { Modal, DropdownItemProps, Icon, Form, Button, Confirm } from 'semantic-ui-react'
import { DjangoCGDSStudy, DjangoMRNAxGEMResultRow, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile } from '../../../utils/django_interfaces'
import ky, { Options } from 'ky'
import { getDjangoHeader, cleanRef, getFilenameFromSource, getDefaultSource } from '../../../utils/util_functions'
@@ -10,8 +10,10 @@ import { ManualForm } from '../../biomarkers/modalContentBiomarker/manualForm/Ma
import { PaginationCustomFilter } from '../../common/PaginatedTable'
import { isEqual } from 'lodash'
import { getDefaultClusteringParameters, getDefaultRFParameters, getDefaultSvmParameters } from '../../biomarkers/utils'
+import { BiomarkerDetailsModal } from '../../biomarkers/BiomarkerDetailsModal'
+import { Alert } from '../../common/Alert'
-// URLs defined in biomarkers.html
+// URLs defined in gem.html
declare const urlBiomarkersCRUD: string
declare const urlBiomarkersSimpleUpdate: string
declare const urlBiomarkersCreate: string
@@ -1139,6 +1141,7 @@ export class BiomarkerFromCorrelationModal extends React.Component {
+ console.debug('handleAddMoleculeToSection', value)
const genesSymbolsFinder = this.state.formBiomarker.moleculesSymbolsFinder
genesSymbolsFinder.data = []
this.setState({
@@ -1528,7 +1531,8 @@ export class BiomarkerFromCorrelationModal extends React.Component this.closeBiomarkerModal()}
>
+ {/* Biomarker details modal. */}
+ }
+ closeOnEscape={false}
+ closeOnDimmerClick={false}
+ closeOnDocumentClick={false}
+ centered={false}
+ onClose={this.closeBiomarkerDetailsModal}
+ open={this.state.openDetailsModal}
+ >
+
+
+
+ this.handleCancelConfirmModalState()}
+ onConfirm={() => this.state.confirmModal.onConfirm()}
+ />
+
+
+
>
)
}
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
index a7a29ae7..3967f448 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
@@ -15,7 +15,7 @@ interface CreateBiomarkerButtonProps extends Partial {
const CreateBiomarkerButton: React.FC = ({
experimentInfo,
onCreateBiomarker,
- ...buttonProps // <-- todas las props extra van acá
+ ...buttonProps
}) => {
const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState('selectAll')
@@ -49,8 +49,7 @@ const CreateBiomarkerButton: React.FC = ({
{/* Modal con opciones */}
setOpen(false)}
>
Create Biomarker
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
index d9cbcba7..ff9fe3d3 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
@@ -173,13 +173,7 @@ export const ResultTableControlForm = (props: ResultTableControlFormProps) => {
{
- console.log('filtros del biomarker', {
- selectAll,
- experimentInfo,
- tableControl
- })
- }}
+ onCreateBiomarker={() => { }}
/>
Date: Fri, 20 Feb 2026 12:32:13 -0300
Subject: [PATCH 06/16] Normalization parameters of
BiomarkerFromCorrelationModel
---
.../pipeline/experiment-result/ResultTableControlForm.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
index ff9fe3d3..7602e80f 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
@@ -173,7 +173,6 @@ export const ResultTableControlForm = (props: ResultTableControlFormProps) => {
{ }}
/>
Date: Fri, 20 Feb 2026 12:32:43 -0300
Subject: [PATCH 07/16] Normalization BiomarkerFromCorrelationModal parameters
---
.../experiment-result/BiomarkerFromCorrelationModal.tsx | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
index e11a536d..8ea6fd4a 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
@@ -45,11 +45,6 @@ type ValidationForm = {
interface BiomarkerFromCorrelationModalProps {
experimentInfo: ExperimentInfo;
tableControl: ExperimentResultTableControl;
- onCreateBiomarker: (options: {
- experimentInfo: ExperimentInfo;
- selectAll: boolean;
- tableControl?: ExperimentResultTableControl;
- }) => void;
}
/** BiomarkersPanel's state */
From c218f9f790597b7782fe11210390e4cdc3294aed Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Fri, 20 Feb 2026 13:11:07 -0300
Subject: [PATCH 08/16] Fix spelling mistake
---
.../pipeline/experiment-result/ResultTableControlForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
index 7602e80f..651c5cf1 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/ResultTableControlForm.tsx
@@ -166,7 +166,7 @@ export const ResultTableControlForm = (props: ResultTableControlFormProps) => {
width={2}
label={isShowingHighPrecision ? '1.234e-5' : 'p < .001'}
icon={isShowingHighPrecision ? 'eye slash' : 'eye'}
- title={`${isShowingHighPrecision ? 'Les s' : 'More'} precise p-value`}
+ title={`${isShowingHighPrecision ? 'Less' : 'More'} precise p-value`}
onClick={() => props.changePrecisionState(!isShowingHighPrecision)}
/>
From 14acdf828500c0a2a2aed1d75ec68b1391bf0666 Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Fri, 20 Feb 2026 13:20:57 -0300
Subject: [PATCH 09/16] Remove unused imports
---
src/frontend/views.py | 12 +++---------
1 file changed, 3 insertions(+), 9 deletions(-)
diff --git a/src/frontend/views.py b/src/frontend/views.py
index 40378178..eaed2acf 100644
--- a/src/frontend/views.py
+++ b/src/frontend/views.py
@@ -4,26 +4,20 @@
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework import permissions
-from copy import deepcopy
from typing import List, Optional, Dict
from django.conf import settings
from django.contrib.auth.decorators import login_required
-from django.db import transaction
from django.shortcuts import render
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions, filters
-from rest_framework.generics import get_object_or_404
from rest_framework.request import Request
-from rest_framework.response import Response
from rest_framework.views import APIView
-from api_service.models import Experiment
from api_service.mrna_service import global_mrna_service
-from biomarkers.models import Biomarker, BiomarkerState, BiomarkerOrigin, MoleculeIdentifier
-from biomarkers.serializers import BiomarkerFromCorrelationAnalysisSerializer, BiomarkerSerializer, MoleculeIdentifierSerializer, \
- BiomarkerSimpleSerializer, BiomarkerSimpleUpdateSerializer
+from biomarkers.models import Biomarker, BiomarkerState, BiomarkerOrigin
+from biomarkers.serializers import BiomarkerSerializer, BiomarkerSimpleSerializer, BiomarkerSimpleUpdateSerializer
from common.pagination import StandardResultsSetPagination
from common.response import generate_json_response_or_404
-from django.db.models import QuerySet, Q
+from django.db.models import Q
def index_action(request):
"""Index view"""
From c7e2de33fdc6f7629ec4ad55bff1963c6e8a97e9 Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Thu, 12 Mar 2026 20:35:33 -0300
Subject: [PATCH 10/16] Fix some bugs
---
.../pipeline/experiment-result/CreateBiomarkerButton.tsx | 5 ++---
src/frontend/static/frontend/src/css/gem.css | 5 +----
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
index 3967f448..79e21bfb 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/CreateBiomarkerButton.tsx
@@ -30,11 +30,10 @@ const CreateBiomarkerButton: React.FC = ({
return (
<>
- {/* Botón que abre el modal */}
+ {/* Button that opens the modal */}
- {/* Modal con opciones */}
+ {/* Modal with options */}
Date: Fri, 20 Mar 2026 13:02:28 -0300
Subject: [PATCH 11/16] Save 20.3 cytoscape graph
---
.../static/frontend/package-lock.json | 10 +-
src/frontend/static/frontend/package.json | 2 +-
.../molecules/CurrentMoleculeDetails.tsx | 3 +
.../molecules/MoleculesDetailsMenu.tsx | 7 +
.../GeneOntologyCytoscapeChart.tsx | 2 +-
.../genes/GeneAssociationsNetwork.tsx | 341 +++++++++++++
.../genes/GeneAssociationsNetworkPanel.tsx | 479 +++++++++++-------
.../src/components/biomarkers/types.ts | 1 +
8 files changed, 645 insertions(+), 200 deletions(-)
create mode 100644 src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx
diff --git a/src/frontend/static/frontend/package-lock.json b/src/frontend/static/frontend/package-lock.json
index b6e3a75c..bbca754f 100644
--- a/src/frontend/static/frontend/package-lock.json
+++ b/src/frontend/static/frontend/package-lock.json
@@ -16,7 +16,7 @@
"@visx/tooltip": "^3.3.0",
"apexcharts": "^3.52.0",
"axios": "^1.8.4",
- "cytoscape": "^3.30.2",
+ "cytoscape": "^3.33.1",
"d3": "^7.9.0",
"dayjs": "^1.11.13",
"fomantic-ui-css": "^2.9.4",
@@ -10122,9 +10122,10 @@
"integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A=="
},
"node_modules/cytoscape": {
- "version": "3.30.2",
- "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.2.tgz",
- "integrity": "sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw==",
+ "version": "3.33.1",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
+ "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
+ "license": "MIT",
"engines": {
"node": ">=0.10"
}
@@ -23101,7 +23102,6 @@
"resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-3.3.0.tgz",
"integrity": "sha512-3PT9dW7IbIfN7JWGr4YxxFQnbN5MRaB36qIKF/eF0iC9l0/MuGSlMlgRgI7Uu8vYuGxX6AjLwsBBRYTPG7NFSA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@plotly/d3": "3.8.2",
"@plotly/d3-sankey": "0.7.2",
diff --git a/src/frontend/static/frontend/package.json b/src/frontend/static/frontend/package.json
index fd73478c..76dd01fa 100644
--- a/src/frontend/static/frontend/package.json
+++ b/src/frontend/static/frontend/package.json
@@ -59,7 +59,7 @@
"@visx/tooltip": "^3.3.0",
"apexcharts": "^3.52.0",
"axios": "^1.8.4",
- "cytoscape": "^3.30.2",
+ "cytoscape": "^3.33.1",
"d3": "^7.9.0",
"dayjs": "^1.11.13",
"fomantic-ui-css": "^2.9.4",
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
index 4f463eeb..1f173352 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
@@ -11,6 +11,7 @@ import { GeneAssociationsNetworkPanel } from './genes/GeneAssociationsNetworkPan
import { MiRNADrugsPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADrugsPanel'
import { MiRNADiseasesPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADiseasesPanel'
import { ActionableCancerGenesPanel } from './genes/ActionableCancerGenesPanel'
+import { GeneExpressionRegulationNetworkPanel } from './genes/GeneAssociationsNetwork'
// const MENU_DEFAULT: ActiveBiomarkerMoleculeItemMenu = ActiveBiomarkerMoleculeItemMenu.DETAILS // TODO: use this
const MENU_DEFAULT: ActiveBiomarkerMoleculeItemMenu = ActiveBiomarkerMoleculeItemMenu.DETAILS
@@ -52,6 +53,8 @@ export const CurrentMoleculeDetails = (props: CurrentMoleculeDetailsProps) => {
return
case ActiveBiomarkerMoleculeItemMenu.GENE_ONTOLOGY:
return
+ case ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS:
+ return
case ActiveBiomarkerMoleculeItemMenu.DISEASES:
return
case ActiveBiomarkerMoleculeItemMenu.DRUGS:
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx
index 5a98008b..ec0b914c 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx
@@ -63,6 +63,13 @@ export const MoleculesDetailsMenu = (props: MoleculesDetailsMenuProps) => {
popupInfo: 'Gene Ontology (GO) is a powerful tool for understanding the biological processes, molecular functions, and cellular components associated with a gene',
isVisible: isGene
},
+ {
+ name: 'Gene regulation associations',
+ onClick: () => props.setActiveItem(ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS),
+ isActive: props.activeItem === ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS,
+ popupInfo: 'Gene regulation associations provide insights into the regulatory relationships between genes, helping to unravel the complex mechanisms that control gene expression and cellular function',
+ isVisible: isGene
+ },
// TODO: implement
// {
// name: 'Actionable/Cancer genes',
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx
index 9b277bba..af7df28e 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/gene-ontology/GeneOntologyCytoscapeChart.tsx
@@ -78,7 +78,7 @@ export const GeneOntologyCytoscapeChart = (props: GeneOntologyCytoscapeChartProp
style: {
'curve-style': 'bezier',
'target-arrow-shape': 'triangle',
- 'line-color': function (edge) {
+ 'line-color': function (edge): any {
// Sets the color of the edge depending on the relation_type attribute
const relationType: OntologyRelationTermToTermFilter = edge.data('relation_type')
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx
new file mode 100644
index 00000000..ad7f0e0d
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx
@@ -0,0 +1,341 @@
+import React, { useEffect, useMemo, useRef } from 'react'
+import cytoscape, { Core, ElementDefinition } from 'cytoscape'
+
+type NodeKind = 'mRNA' | 'miRNA' | 'CNA' | 'Methylation' | 'Drug'
+
+type Props = {
+ height?: number | string;
+ width?: number | string;
+}
+
+const NODE_COLORS: Record = {
+ mRNA: '#4f46e5',
+ miRNA: '#db2777',
+ CNA: '#f59e0b',
+ Methylation: '#10b981',
+ Drug: '#64748b',
+}
+
+const getEdgeColor = (correlation: number) => {
+ if (correlation <= -0.5) { return '#dc2626' }
+
+ if (correlation >= 0.5) { return '#2563eb' }
+
+ return '#cbd5e1'
+}
+
+const getEdgeOpacity = (correlation: number) => {
+ const abs = Math.abs(correlation)
+
+ if (abs >= 0.8) { return 0.95 }
+ if (abs >= 0.6) { return 0.8 }
+ if (abs >= 0.5) { return 0.65 }
+
+ return 0.22
+}
+
+const getEdgeWidth = (correlation: number) => {
+ const abs = Math.abs(correlation)
+
+ if (abs >= 0.9) { return 6 }
+ if (abs >= 0.8) { return 5 }
+ if (abs >= 0.7) { return 4 }
+ if (abs >= 0.5) { return 3 }
+
+ return 1.5
+}
+
+export const GeneExpressionRegulationNetworkPanel = ({
+ height = 650,
+ width = '100%',
+}: Props) => {
+ const containerRef = useRef(null)
+ const cyRef = useRef(null)
+
+ const elements = useMemo(() => {
+ const nodes: ElementDefinition[] = [
+ { data: { id: 'mrna_pcna', label: 'PCNA', type: 'mRNA', size: 58 } },
+ { data: { id: 'mrna_fen1', label: 'FEN1', type: 'mRNA', size: 54 } },
+ { data: { id: 'mrna_rad51', label: 'RAD51', type: 'mRNA', size: 36 } },
+ { data: { id: 'mrna_pold1', label: 'POLD1', type: 'mRNA', size: 32 } },
+ { data: { id: 'mrna_lig1', label: 'LIG1', type: 'mRNA', size: 30 } },
+ { data: { id: 'mir_21', label: 'miR-21', type: 'miRNA', size: 28 } },
+ { data: { id: 'mir_34a', label: 'miR-34a', type: 'miRNA', size: 28 } },
+ { data: { id: 'mir_155', label: 'miR-155', type: 'miRNA', size: 28 } },
+ { data: { id: 'mir_200c', label: 'miR-200c', type: 'miRNA', size: 28 } },
+ { data: { id: 'cna_8q24', label: 'CNA 8q24', type: 'CNA', size: 34 } },
+ { data: { id: 'cna_17p_loss', label: '17p loss', type: 'CNA', size: 34 } },
+ { data: { id: 'cna_1q_gain', label: '1q gain', type: 'CNA', size: 34 } },
+ { data: { id: 'meth_mlh1', label: 'MLH1 meth', type: 'Methylation', size: 32 } },
+ { data: { id: 'meth_mgmt', label: 'MGMT meth', type: 'Methylation', size: 32 } },
+ { data: { id: 'drug_olaparib', label: 'Olaparib', type: 'Drug', size: 34 } },
+ { data: { id: 'drug_cisplatin', label: 'Cisplatin', type: 'Drug', size: 34 } },
+ { data: { id: 'drug_temozolomide', label: 'Temozolomide', type: 'Drug', size: 34 } },
+ { data: { id: 'mrna_apex2', label: 'APEX2', type: 'mRNA', size: 32 } },
+ ]
+
+ const rawEdges = [
+ ['mrna_pcna', 'mrna_fen1', 0.92],
+ ['mrna_pcna', 'mrna_rad51', 0.87],
+ ['mrna_pcna', 'mrna_pold1', 0.83],
+ ['mrna_pcna', 'mrna_lig1', 0.79],
+ ['mrna_pcna', 'mrna_apex2', 0.72],
+ ['mrna_fen1', 'mrna_rad51', 0.76],
+ ['mrna_fen1', 'mrna_pold1', 0.81],
+ ['mrna_fen1', 'mrna_apex2', 0.69],
+ ['mrna_rad51', 'mrna_apex2', 0.64],
+ ['mrna_pold1', 'mrna_lig1', 0.71],
+ ['mrna_lig1', 'mrna_apex2', 0.58],
+
+ ['mir_21', 'mrna_pcna', -0.74],
+ ['mir_21', 'mrna_fen1', -0.68],
+ ['mir_21', 'mrna_rad51', -0.61],
+ ['mir_34a', 'mrna_pcna', -0.71],
+ ['mir_34a', 'mrna_pold1', -0.66],
+ ['mir_155', 'mrna_rad51', -0.78],
+ ['mir_155', 'mrna_apex2', -0.57],
+ ['mir_200c', 'mrna_lig1', -0.63],
+ ['mir_200c', 'mrna_fen1', -0.55],
+
+ ['cna_8q24', 'mrna_pcna', 0.67],
+ ['cna_8q24', 'mrna_fen1', 0.54],
+ ['cna_17p_loss', 'mrna_rad51', -0.58],
+ ['cna_17p_loss', 'mrna_apex2', -0.52],
+ ['cna_1q_gain', 'mrna_pold1', 0.62],
+ ['cna_1q_gain', 'mrna_lig1', 0.57],
+ ['meth_mlh1', 'mrna_pcna', -0.53],
+ ['meth_mlh1', 'mrna_apex2', -0.64],
+ ['meth_mgmt', 'mrna_rad51', -0.59],
+ ['meth_mgmt', 'mrna_fen1', -0.51],
+
+ ['drug_olaparib', 'mrna_rad51', -0.69],
+ ['drug_olaparib', 'mrna_fen1', -0.56],
+ ['drug_cisplatin', 'mrna_pcna', -0.52],
+ ['drug_cisplatin', 'mrna_lig1', -0.55],
+ ['drug_temozolomide', 'meth_mgmt', 0.73],
+ ['drug_temozolomide', 'mrna_apex2', -0.51],
+
+ ['mir_21', 'cna_8q24', 0.22],
+ ['mir_34a', 'meth_mlh1', -0.18],
+ ['drug_olaparib', 'drug_cisplatin', 0.31],
+ ['mrna_pcna', 'drug_temozolomide', -0.27],
+ ['cna_1q_gain', 'mrna_apex2', 0.33],
+ ] as const
+
+ const edges: ElementDefinition[] = rawEdges.map(([source, target, correlation], index) => ({
+ data: {
+ id: `e_${index + 1}`,
+ source,
+ target,
+ correlation,
+ edgeColor: getEdgeColor(correlation),
+ edgeOpacity: getEdgeOpacity(correlation),
+ edgeWidth: getEdgeWidth(correlation),
+ },
+ classes: Math.abs(correlation) < 0.5 ? 'weak-edge' : '',
+ }))
+
+ return [...nodes, ...edges]
+ }, [])
+
+ useEffect(() => {
+ if (!containerRef.current) { return }
+
+ const cy = cytoscape({
+ container: containerRef.current,
+ elements,
+ wheelSensitivity: 0.18,
+ minZoom: 0.4,
+ maxZoom: 2,
+ style: [
+ {
+ selector: 'node',
+ style: {
+ label: 'data(label)',
+ width: 'data(size)',
+ height: 'data(size)',
+ shape: 'ellipse',
+ 'background-color': '#64748b',
+ color: '#ffffff',
+ 'font-size': 12,
+ 'font-weight': 600,
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ 'text-wrap': 'wrap',
+ 'text-max-width': 90 as any,
+ 'text-outline-color': '#475569',
+ 'text-outline-width': 3,
+ 'border-width': 2,
+ 'border-color': '#ffffff',
+ },
+ },
+ {
+ selector: 'node[type = "mRNA"]',
+ style: {
+ 'background-color': NODE_COLORS.mRNA,
+ 'text-outline-color': NODE_COLORS.mRNA,
+ },
+ },
+ {
+ selector: 'node[type = "miRNA"]',
+ style: {
+ 'background-color': NODE_COLORS.miRNA,
+ 'text-outline-color': NODE_COLORS.miRNA,
+ },
+ },
+ {
+ selector: 'node[type = "CNA"]',
+ style: {
+ 'background-color': NODE_COLORS.CNA,
+ 'text-outline-color': NODE_COLORS.CNA,
+ },
+ },
+ {
+ selector: 'node[type = "Methylation"]',
+ style: {
+ 'background-color': NODE_COLORS.Methylation,
+ 'text-outline-color': NODE_COLORS.Methylation,
+ },
+ },
+ {
+ selector: 'node[type = "Drug"]',
+ style: {
+ 'background-color': NODE_COLORS.Drug,
+ 'text-outline-color': NODE_COLORS.Drug,
+ },
+ },
+ {
+ selector: 'edge',
+ style: {
+ width: 'data(edgeWidth)',
+ 'line-color': 'data(edgeColor)',
+ opacity: 'data(edgeOpacity)' as any,
+ 'curve-style': 'bezier',
+ },
+ },
+ {
+ selector: 'edge.weak-edge',
+ style: {
+ 'line-style': 'dashed',
+ },
+ },
+ {
+ selector: ':selected',
+ style: {
+ 'border-color': '#f8fafc',
+ 'border-width': 5,
+ 'line-color': '#0f172a',
+ 'target-arrow-color': '#0f172a',
+ 'source-arrow-color': '#0f172a',
+ },
+ },
+ {
+ selector: '.faded',
+ style: {
+ opacity: 0.12,
+ },
+ },
+ ],
+ layout: {
+ name: 'cose',
+ animate: true,
+ randomize: true,
+ fit: true,
+ padding: 40,
+ gravity: 1,
+ nodeRepulsion: 9000,
+ idealEdgeLength: 90,
+ componentSpacing: 80,
+ },
+ })
+
+ cyRef.current = cy
+
+ cy.on('tap', 'node', (evt) => {
+ const node = evt.target
+ const neighborhood = node.closedNeighborhood()
+
+ cy.elements().addClass('faded')
+ neighborhood.removeClass('faded')
+ })
+
+ cy.on('tap', (evt) => {
+ if (evt.target === cy) {
+ cy.elements().removeClass('faded')
+ cy.elements().unselect()
+ }
+ })
+
+ return () => {
+ cy.destroy()
+ cyRef.current = null
+ }
+ }, [elements])
+
+ return (
+
+ )
+}
+
+const LegendDot = ({ color, label }: { color: string; label: string }) => (
+
+
+ {label}
+
+)
+
+const LegendLine = ({ color, label }: { color: string; label: string }) => (
+
+
+ {label}
+
+)
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx
index a7d5db85..4a4e00ab 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetworkPanel.tsx
@@ -1,6 +1,11 @@
-import React, { useEffect, useRef } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import ky from 'ky'
-import cytoscape from 'cytoscape'
+import cytoscape, {
+ Core,
+ ElementDefinition,
+ EventObjectNode,
+ NodeSingular
+} from 'cytoscape'
import { BiomarkerMolecule } from '../../../types'
import { Form, Grid, Input } from 'semantic-ui-react'
import { alertGeneralError } from '../../../../../utils/util_functions'
@@ -11,240 +16,314 @@ import '../../../../../css/cytoscape.css'
// Defined in biomarkers.html
declare const urlGeneAssociationsNetwork: string
-/** Colors for the String standard: https://string-db.org/cgi/help ("Network" section). */
const COLORS_BY_STRING_RELATION = {
fusion: ['Fusion', '#d90429'],
coOccurrence: ['Co-occurrence', '#3a86ff'],
experimental: ['Experimental', '#7209b7'],
textMining: ['Text mining', '#d5ae5d'],
database: ['Database', '#4cc9f0'],
- coExpression: ['Co-expression', '#10002b'] // In the FAQ says 'white', but we have a white background, so it's not visible
-}
+ coExpression: ['Co-expression', '#10002b']
+} as const
-/**
- * Renders the legends for the Cytoscape instance.
- * @returns Component.
- */
const CytoscapeLegends = () => (
Relations
{Object.entries(COLORS_BY_STRING_RELATION).map(([_, [relationDescription, color]]) => (
- - {relationDescription}
+ -
+
+ {relationDescription}
+
))}
)
-/** GeneAssociationsNetworkPanel props. */
interface GeneAssociationsNetworkPanelProps {
- /** Selected BiomarkerMolecule instance to show the options. */
- selectedGene: BiomarkerMolecule,
+ selectedGene: BiomarkerMolecule
}
-export const GeneAssociationsNetworkPanel = (props: GeneAssociationsNetworkPanelProps) => {
- const abortControllerTerm = useRef(new AbortController())
- const [minCombinedScore, setMinCombinedScore] = React.useState(950)
+type CytoscapeResponseData =
+ | ElementDefinition[]
+ | {
+ nodes?: ElementDefinition[]
+ edges?: ElementDefinition[]
+ }
+
+export const GeneAssociationsNetworkPanel = ({ selectedGene }: GeneAssociationsNetworkPanelProps) => {
+ const cyContainerRef = useRef(null)
+ const cyRef = useRef(null)
+ const abortControllerRef = useRef(null)
+ const positionsRef = useRef>({})
+ const [minCombinedScore, setMinCombinedScore] = useState(950)
+
+ const cytoscapeStyles = [
+ {
+ selector: 'core',
+ style: {
+ 'selection-box-color': '#AAD8FF',
+ 'selection-box-border-color': '#8BB0D0',
+ 'selection-box-opacity': 0.5
+ } as any
+ },
+ {
+ selector: 'node',
+ style: {
+ width: 'mapData(score, 0, 0.006769776522008331, 20, 60)',
+ height: 'mapData(score, 0, 0.006769776522008331, 20, 60)',
+ content: 'data(name)',
+ 'font-size': '12px',
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ 'background-color': '#555',
+ 'text-outline-color': '#555',
+ 'text-outline-width': '2px',
+ color: '#fff',
+ 'overlay-padding': '6px',
+ 'z-index': 10
+ }
+ },
+ {
+ selector: 'node[?attr]',
+ style: {
+ shape: 'rectangle',
+ 'background-color': '#aaa',
+ 'text-outline-color': '#aaa',
+ width: '16px',
+ height: '16px',
+ 'font-size': '6px',
+ 'z-index': 1
+ }
+ },
+ {
+ selector: 'node[?query]',
+ style: {
+ 'background-clip': 'none',
+ 'background-fit': 'contain'
+ }
+ },
+ {
+ selector: 'node:selected',
+ style: {
+ 'border-width': '6px',
+ 'border-color': '#AAD8FF',
+ 'border-opacity': 0.5,
+ 'background-color': '#77828C',
+ 'text-outline-color': '#77828C'
+ }
+ },
+ {
+ selector: 'edge',
+ style: {
+ 'curve-style': 'haystack',
+ 'haystack-radius': 0.5,
+ opacity: 0.4,
+ 'line-color': '#bbb',
+ width: 'mapData(weight, 0, 1, 1, 8)',
+ 'overlay-padding': '3px'
+ }
+ },
+ {
+ selector: 'node.unhighlighted',
+ style: {
+ opacity: 0.2
+ }
+ },
+ {
+ selector: 'edge.unhighlighted',
+ style: {
+ opacity: 0.05
+ }
+ },
+ {
+ selector: '.highlighted',
+ style: {
+ 'z-index': 999999
+ }
+ },
+ {
+ selector: 'node.highlighted',
+ style: {
+ 'border-width': '6px',
+ 'border-color': '#AAD8FF',
+ 'border-opacity': 0.5,
+ 'background-color': '#394855',
+ 'text-outline-color': '#394855'
+ }
+ },
+ {
+ selector: 'edge.filtered',
+ style: {
+ opacity: 0
+ }
+ },
+ {
+ selector: 'edge[group="fusion"]',
+ style: {
+ 'line-color': COLORS_BY_STRING_RELATION.fusion[1]
+ }
+ },
+ {
+ selector: 'edge[group="coOccurrence"]',
+ style: {
+ 'line-color': COLORS_BY_STRING_RELATION.coOccurrence[1]
+ }
+ },
+ {
+ selector: 'edge[group="experimental"]',
+ style: {
+ 'line-color': COLORS_BY_STRING_RELATION.experimental[1]
+ }
+ },
+ {
+ selector: 'edge[group="textMining"]',
+ style: {
+ 'line-color': COLORS_BY_STRING_RELATION.textMining[1]
+ }
+ },
+ {
+ selector: 'edge[group="database"]',
+ style: {
+ 'line-color': COLORS_BY_STRING_RELATION.database[1]
+ }
+ },
+ {
+ selector: 'edge[group="coExpression"]',
+ style: {
+ 'line-color': COLORS_BY_STRING_RELATION.coExpression[1]
+ }
+ }
+ ]
+
+ const normalizeElements = (elements: CytoscapeResponseData): ElementDefinition[] => {
+ if (Array.isArray(elements)) {
+ return elements
+ }
+
+ return [...(elements.nodes || []), ...(elements.edges || [])]
+ }
+
+ const createCy = () => {
+ if (cyRef.current || !cyContainerRef.current) { return }
- /**
- * Initializes the Cytoscape instance with the given elements.
- * @param elements Cytoscape elements to initialize the instance.
- */
- const initCytoscape = (elements: any /* TODO: type */) => {
const cy = cytoscape({
- container: document.getElementById('cy'),
+ container: cyContainerRef.current,
minZoom: 0.5,
maxZoom: 1.5,
- randomize: true,
- animate: true,
- nodeSpacing: 15,
- edgeLengthVal: 45,
- style: [
- {
- selector: 'core',
- style: {
- 'selection-box-color': '#AAD8FF',
- 'selection-box-border-color': '#8BB0D0',
- 'selection-box-opacity': '0.5'
- }
- },
- {
- selector: 'node',
- style: {
- width: 'mapData(score, 0, 0.006769776522008331, 20, 60)',
- height: 'mapData(score, 0, 0.006769776522008331, 20, 60)',
- content: 'data(name)',
- 'font-size': '12px',
- 'text-valign': 'center',
- 'text-halign': 'center',
- 'background-color': '#555',
- 'text-outline-color': '#555',
- 'text-outline-width': '2px',
- color: '#fff',
- 'overlay-padding': '6px',
- 'z-index': '10'
- }
- },
- {
- selector: 'node[?attr]',
- style: {
- shape: 'rectangle',
- 'background-color': '#aaa',
- 'text-outline-color': '#aaa',
- width: '16px',
- height: '16px',
- 'font-size': '6px',
- 'z-index': '1'
- }
- },
- {
- selector: 'node[?query]',
- style: {
- 'background-clip': 'none',
- 'background-fit': 'contain'
- }
- },
- {
- selector: 'node:selected',
- style: {
- 'border-width': '6px',
- 'border-color': '#AAD8FF',
- 'border-opacity': '0.5',
- 'background-color': '#77828C',
- 'text-outline-color': '#77828C'
- }
- },
- {
- selector: 'edge',
- style: {
- 'curve-style': 'haystack',
- 'haystack-radius': '0.5',
- opacity: '0.4',
- 'line-color': '#bbb',
- width: 'mapData(weight, 0, 1, 1, 8)',
- 'overlay-padding': '3px'
- }
- },
- {
- selector: 'node.unhighlighted',
- style: {
- opacity: '0.2'
- }
- },
- {
- selector: 'edge.unhighlighted',
- style: {
- opacity: '0.05'
- }
- },
- {
- selector: '.highlighted',
- style: {
- 'z-index': '999999'
- }
- },
- {
- selector: 'node.highlighted',
- style: {
- 'border-width': '6px',
- 'border-color': '#AAD8FF',
- 'border-opacity': '0.5',
- 'background-color': '#394855',
- 'text-outline-color': '#394855'
- }
- },
- {
- selector: 'edge.filtered',
- style: {
- opacity: '0'
- }
- },
- // Types of relations
- {
- selector: 'edge[group="fusion"]',
- style: {
- 'line-color': COLORS_BY_STRING_RELATION.fusion[1]
- }
- },
- {
- selector: 'edge[group="coOccurrence"]',
- style: {
- 'line-color': COLORS_BY_STRING_RELATION.coOccurrence[1]
- }
- },
- {
- selector: 'edge[group="experimental"]',
- style: {
- 'line-color': COLORS_BY_STRING_RELATION.experimental[1]
- }
- },
- {
- selector: 'edge[group="textMining"]',
- style: {
- 'line-color': COLORS_BY_STRING_RELATION.textMining[1]
- }
- },
- {
- selector: 'edge[group="database"]',
- style: {
- 'line-color': COLORS_BY_STRING_RELATION.database[1]
- }
- },
- {
- selector: 'edge[group="coExpression"]',
- style: {
- 'line-color': COLORS_BY_STRING_RELATION.coExpression[1]
- }
+ elements: [],
+ style: cytoscapeStyles,
+ layout: {
+ name: 'preset'
+ }
+ })
+
+ cy.on('dragfreeon', 'node', (evt: EventObjectNode) => {
+ const node = evt.target
+ positionsRef.current[node.id()] = node.position()
+ })
+
+ cyRef.current = cy
+ }
+
+ const applyElements = (rawElements: CytoscapeResponseData) => {
+ const cy = cyRef.current
+
+ if (!cy) { return }
+
+ const incomingElements = normalizeElements(rawElements)
+
+ cy.nodes().forEach((node: NodeSingular) => {
+ positionsRef.current[node.id()] = node.position()
+ })
+
+ const mergedElements = incomingElements.map((element) => {
+ const elementId = element?.data?.id as string | undefined
+ const savedPosition = elementId ? positionsRef.current[elementId] : undefined
+ const isNode = !('source' in (element.data || {})) && !('target' in (element.data || {}))
+
+ if (isNode && savedPosition) {
+ return {
+ ...element,
+ position: savedPosition
}
- ],
- elements
+ }
+
+ return element
})
- const layout = cy.elements().layout({
- name: 'random'
+ cy.batch(() => {
+ cy.elements().remove()
+ cy.add(mergedElements)
})
- layout.run()
+ const hasSavedPositions = cy
+ .nodes()
+ .toArray()
+ .some((node: NodeSingular) => Boolean(positionsRef.current[node.id()]))
+
+ if (hasSavedPositions) {
+ cy.layout({
+ name: 'preset',
+ fit: true,
+ padding: 30,
+ animate: false
+ }).run()
+ } else {
+ cy.layout({
+ name: 'cose',
+ fit: true,
+ padding: 30,
+ animate: false,
+ randomize: true
+ }).run()
+ }
+
+ cy.resize()
+ cy.fit(undefined, 30)
}
- /**
- * Gets all the related genes to the given gene.
- * @param selectedGene Gene object.
- */
- const getRelatedGenes = (selectedGene: BiomarkerMolecule) => {
- const searchParams = { gene_id: selectedGene.identifier, min_combined_score: minCombinedScore }
-
- ky.get(urlGeneAssociationsNetwork, { searchParams: searchParams as any, signal: abortControllerTerm.current.signal }).then((response) => {
- response.json().then((data: { data: any /* TODO: type */ }) => {
- // The response is a CytoscapeElements object already
- initCytoscape(data.data)
- }).catch((err) => {
- alertGeneralError()
- console.log('Error parsing JSON ->', err)
+ const getRelatedGenes = async (gene: BiomarkerMolecule, score: number) => {
+ abortControllerRef.current?.abort()
+ abortControllerRef.current = new AbortController()
+
+ try {
+ const response = await ky.get(urlGeneAssociationsNetwork, {
+ searchParams: {
+ gene_id: gene.identifier,
+ min_combined_score: score
+ } as any,
+ signal: abortControllerRef.current.signal
})
- }).catch((err) => {
- if (!abortControllerTerm.current.signal.aborted) {
+
+ const data = await response.json<{ data: CytoscapeResponseData }>()
+ applyElements(data.data)
+ } catch (err) {
+ if (!abortControllerRef.current?.signal.aborted) {
alertGeneralError()
}
console.log('Error getting experiment', err)
- })
+ }
}
useEffect(() => {
- getRelatedGenes(props.selectedGene)
- }, [props.selectedGene])
+ createCy()
- /** On unmount, cancels the request */
- useEffect(() => {
return () => {
- // Cleanup: cancel the ongoing request when component unmounts
- abortControllerTerm.current.abort()
+ abortControllerRef.current?.abort()
+ cyRef.current?.destroy()
+ cyRef.current = null
}
}, [])
+ useEffect(() => {
+ if (!cyRef.current) { return }
+
+ getRelatedGenes(selectedGene, minCombinedScore)
+ }, [selectedGene, minCombinedScore])
+
return (
@@ -257,27 +336,41 @@ export const GeneAssociationsNetworkPanel = (props: GeneAssociationsNetworkPanel
value={minCombinedScore}
min={900}
max={1000}
- // TODO: implement with debounce
onChange={(_e, { value }) => setMinCombinedScore(Number(value))}
/>
+
- The combined score is computed by combining the probabilities from the different evidence channels and corrected for the probability of randomly observing an interaction. For a more detailed description please see von Mering, et al. Nucleic Acids Res. 2005
+ The combined score is computed by combining the probabilities from the different evidence channels and corrected for the probability of randomly observing an interaction. For a more detailed description please see{' '}
+
+ von Mering, et al. Nucleic Acids Res. 2005
+
)}
onTop={false}
onEvent='click'
/>
+
-
+
diff --git a/src/frontend/static/frontend/src/components/biomarkers/types.ts b/src/frontend/static/frontend/src/components/biomarkers/types.ts
index 131ae12d..98d2b66f 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/types.ts
+++ b/src/frontend/static/frontend/src/components/biomarkers/types.ts
@@ -389,6 +389,7 @@ enum ActiveBiomarkerMoleculeItemMenu {
PATHWAYS,
GENE_ASSOCIATIONS_NETWORK,
GENE_ONTOLOGY,
+ GENE_REGULATION_ASSOCIATIONS,
INFERENCE,
DISEASES,
DRUGS,
From fc69571fe79f91b22b60d23cbdef47b5d3157f6a Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Wed, 8 Apr 2026 13:43:22 -0300
Subject: [PATCH 12/16] TODO Fix
---
.../differential-expression/VolcanoPlot.tsx | 118 +++++++++---------
1 file changed, 59 insertions(+), 59 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx b/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx
index f1a666c9..5be970cd 100644
--- a/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx
+++ b/src/frontend/static/frontend/src/components/differential-expression/VolcanoPlot.tsx
@@ -74,64 +74,64 @@ export const VolcanoPlot = ({
},
]
: []
+ return null
+ // return (
+ // d.log2FC),
+ // y: significant.map((d) => transformP(d.pValue)),
+ // text: significant.map((d) => d.label ?? d.id),
+ // mode: 'markers',
+ // type: 'scattergl',
+ // name: 'Significant',
+ // marker: { size: 6 },
+ // hovertemplate:
+ // 'Significant
' +
+ // 'log2FC: %{x:.2f}
' +
+ // '-log10(p): %{y:.2f}
' +
+ // '%{text}' +
+ // '',
+ // },
+ // {
+ // x: nonsignificant.map((d) => d.log2FC),
+ // y: nonsignificant.map((d) => transformP(d.pValue)),
+ // text: nonsignificant.map((d) => d.label ?? d.id),
+ // mode: 'markers',
+ // type: 'scattergl',
+ // name: 'Not significant',
+ // marker: { size: 4, opacity: 0.6 },
+ // hovertemplate:
+ // 'Not significant
' +
+ // 'log2FC: %{x:.2f}
' +
+ // '-log10(p): %{y:.2f}
' +
+ // '%{text}' +
+ // '',
+ // },
+ // ]}
+ // layout={{
+ // title: 'Volcano Plot',
+ // hovermode: 'closest',
+ // showlegend: true,
+ // margin: { l: 90, r: 40, t: 50, b: 70 },
- return (
- d.log2FC),
- y: significant.map((d) => transformP(d.pValue)),
- text: significant.map((d) => d.label ?? d.id),
- mode: 'markers',
- type: 'scattergl',
- name: 'Significant',
- marker: { size: 6 },
- hovertemplate:
- 'Significant
' +
- 'log2FC: %{x:.2f}
' +
- '-log10(p): %{y:.2f}
' +
- '%{text}' +
- '',
- },
- {
- x: nonsignificant.map((d) => d.log2FC),
- y: nonsignificant.map((d) => transformP(d.pValue)),
- text: nonsignificant.map((d) => d.label ?? d.id),
- mode: 'markers',
- type: 'scattergl',
- name: 'Not significant',
- marker: { size: 4, opacity: 0.6 },
- hovertemplate:
- 'Not significant
' +
- 'log2FC: %{x:.2f}
' +
- '-log10(p): %{y:.2f}
' +
- '%{text}' +
- '',
- },
- ]}
- layout={{
- title: 'Volcano Plot',
- hovermode: 'closest',
- showlegend: true,
- margin: { l: 90, r: 40, t: 50, b: 70 },
-
- // Axis labels (normal, not floating)
- xaxis: {
- title: { text: 'log2(Fold Change)', standoff: 20 },
- zeroline: false,
- },
- yaxis: {
- title: { text: '-log10(p-value)', standoff: 10 },
- zeroline: false,
- },
- shapes: thresholdShapes,
- }}
- config={{
- responsive: true,
- displayModeBar: true,
- }}
- style={{ width: '100%', height: '500px' }}
- />
- )
+ // // Axis labels (normal, not floating)
+ // xaxis: {
+ // title: { text: 'log2(Fold Change)', standoff: 20 },
+ // zeroline: false,
+ // },
+ // yaxis: {
+ // title: { text: '-log10(p-value)', standoff: 10 },
+ // zeroline: false,
+ // },
+ // shapes: thresholdShapes,
+ // }}
+ // config={{
+ // responsive: true,
+ // displayModeBar: true,
+ // }}
+ // style={{ width: '100%', height: '500px' }}
+ // />
+ // )
}
From 8896267de034dadadf3a934977e874dff5209896 Mon Sep 17 00:00:00 2001
From: Hernan
Date: Thu, 26 Feb 2026 20:39:08 -0300
Subject: [PATCH 13/16] Add tissues app and tissue FK integration
Introduce a new read-only tissues app (model, admin, serializer, views, URLs, app config) with initial migration and a data migration loading a list of tissue names. Add tissue ForeignKey fields to CGDSStudy and UserFile with corresponding migrations. Update serializers to accept tissue_id (writable PK) and include nested tissue representation on retrieve; persist tissue on updates/creates. Expose tissues in the REST API and enable filtering by tissue for CGDSStudy and UserFile (DjangoFilterBackend imports and filterset_fields). Register the app in settings and project URLs, and add a frontend URL constant for tissues. Admin/UI prevents adding/changing/deleting tissues (read-only).
---
src/datasets_synchronization/models.py | 2 +-
src/datasets_synchronization/serializers.py | 16 ++++++
src/datasets_synchronization/views.py | 3 +-
.../migrations/0002_load_initial_tissues.py | 50 +++++++++++++++++++
src/tissues/models.py | 1 -
src/tissues/serializers.py | 2 +-
src/user_files/admin.py | 5 +-
src/user_files/models.py | 3 +-
src/user_files/serializers.py | 7 ++-
src/user_files/views.py | 2 +-
10 files changed, 80 insertions(+), 11 deletions(-)
create mode 100644 src/tissues/migrations/0002_load_initial_tissues.py
diff --git a/src/datasets_synchronization/models.py b/src/datasets_synchronization/models.py
index 43a7b472..45166fd6 100644
--- a/src/datasets_synchronization/models.py
+++ b/src/datasets_synchronization/models.py
@@ -289,7 +289,7 @@ class CGDSStudy(models.Model):
null=True,
related_name='cgds_studies_as_clinical_sample_dataset'
)
- tissues = models.ManyToManyField(Tissue, blank=True)
+ tissue = models.ForeignKey(Tissue, on_delete=models.SET_NULL, blank=True, null=True)
task_id: Optional[str] = models.CharField(max_length=100, blank=True, null=True) # Celery Task ID
def __str__(self) -> str:
diff --git a/src/datasets_synchronization/serializers.py b/src/datasets_synchronization/serializers.py
index 3f78d88f..73a0cac8 100644
--- a/src/datasets_synchronization/serializers.py
+++ b/src/datasets_synchronization/serializers.py
@@ -8,6 +8,8 @@
from common.response import ResponseStatus
from .enums import CreateCGDSStudyResponseCode
from .models import CGDSStudy, CGDSDataset, SurvivalColumnsTupleCGDSDataset
+from tissues.models import Tissue
+from tissues.serializers import TissueSerializer
from django.db.models import Q
@@ -54,6 +56,13 @@ class CGDSStudySerializer(serializers.ModelSerializer):
clinical_sample_dataset = CGDSDatasetSerializer(required=False, allow_null=True)
version = serializers.IntegerField(read_only=True)
is_last_version = serializers.SerializerMethodField(method_name='get_is_last_version')
+ # tissue_id: writable PK field; tissue: read-only nested (set in to_representation)
+ tissue_id = serializers.PrimaryKeyRelatedField(
+ queryset=Tissue.objects.all(),
+ source='tissue',
+ allow_null=True,
+ required=False
+ )
class Meta:
model = CGDSStudy
@@ -63,6 +72,12 @@ class Meta:
def get_is_last_version(study: CGDSStudy) -> bool:
return study.version == study.get_last_version()
+ def to_representation(self, instance: CGDSStudy):
+ """Makes a nested representation of the tissue field on GET requests."""
+ data = super().to_representation(instance)
+ data['tissue'] = TissueSerializer(instance.tissue).data if instance.tissue else None
+ return data
+
def __create_cgds_dataset(self, validated_data_pop: OrderedDict) -> Optional[CGDSDataset]:
"""
Creates a CGDSDataset instance from a request data.
@@ -296,6 +311,7 @@ def update(self, instance: CGDSStudy, validated_data):
instance.description = validated_data.get('description', instance.description)
instance.url = validated_data.get('url', instance.url)
instance.url_study_info = validated_data.get('url_study_info', instance.url_study_info)
+ instance.tissue = validated_data.get('tissue', instance.tissue)
# Updates datasets
instance.mrna_dataset = mrna_dataset
diff --git a/src/datasets_synchronization/views.py b/src/datasets_synchronization/views.py
index 3e7097f7..ec5997be 100644
--- a/src/datasets_synchronization/views.py
+++ b/src/datasets_synchronization/views.py
@@ -14,6 +14,7 @@
from .models import CGDSStudy, CGDSDatasetSynchronizationState, CGDSStudySynchronizationState, CGDSDataset
from rest_framework import generics, permissions, filters
from django_filters.rest_framework import DjangoFilterBackend
+from django_filters.rest_framework import DjangoFilterBackend
from user_files.models_choices import FileType
from .serializers import CGDSStudySerializer
from django.shortcuts import render, get_object_or_404
@@ -84,7 +85,7 @@ def get_queryset(self):
permission_classes = [permissions.IsAuthenticated]
pagination_class = StandardResultsSetPagination
filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend]
- filterset_fields = ['tissues']
+ filterset_fields = ['tissue']
search_fields = ['name', 'description']
ordering_fields = '__all__'
diff --git a/src/tissues/migrations/0002_load_initial_tissues.py b/src/tissues/migrations/0002_load_initial_tissues.py
new file mode 100644
index 00000000..f181bf15
--- /dev/null
+++ b/src/tissues/migrations/0002_load_initial_tissues.py
@@ -0,0 +1,50 @@
+# Generated by Django 4.2.19 on 2026-02-26 22:58
+
+from django.db import migrations
+
+TISSUES = [
+ 'Cerebro (GBM)',
+ 'Cerebro (Glioma)',
+ 'Colon',
+ 'Cérvix',
+ 'Estómago',
+ 'Esófago',
+ 'Glándula Adrenal',
+ 'Hígado',
+ 'Mama',
+ 'Ovario',
+ 'Piel',
+ 'Próstata',
+ 'Pulmón',
+ 'Pulmón (Escamoso)',
+ 'Páncreas',
+ 'Recto',
+ 'Riñón (KIRC)',
+ 'Riñón (KIRP)',
+ 'Sangre (Leucemia)',
+ 'Testículo',
+ 'Tiroides',
+ 'Vejiga',
+ 'Útero',
+]
+
+
+def load_tissues(apps, schema_editor):
+ Tissue = apps.get_model('tissues', 'Tissue')
+ Tissue.objects.bulk_create([Tissue(name=name) for name in TISSUES])
+
+
+def unload_tissues(apps, schema_editor):
+ Tissue = apps.get_model('tissues', 'Tissue')
+ Tissue.objects.filter(name__in=TISSUES).delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tissues', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(load_tissues, reverse_code=unload_tissues),
+ ]
\ No newline at end of file
diff --git a/src/tissues/models.py b/src/tissues/models.py
index d8fe7f7a..9b769759 100644
--- a/src/tissues/models.py
+++ b/src/tissues/models.py
@@ -5,7 +5,6 @@
class Tissue(models.Model):
"""Reference model for tissue types. Tissues are read-only and cannot be deleted."""
name = models.CharField(max_length=100, unique=True)
- code = models.CharField(max_length=100, unique=True) # noqa: populated by migration 0003
class Meta:
ordering = ['name']
diff --git a/src/tissues/serializers.py b/src/tissues/serializers.py
index 9329da93..6a106382 100644
--- a/src/tissues/serializers.py
+++ b/src/tissues/serializers.py
@@ -5,4 +5,4 @@
class TissueSerializer(serializers.ModelSerializer):
class Meta:
model = Tissue
- fields = ['id', 'name', 'code']
+ fields = ['id', 'name']
diff --git a/src/user_files/admin.py b/src/user_files/admin.py
index 2e05957d..6a05a0b6 100644
--- a/src/user_files/admin.py
+++ b/src/user_files/admin.py
@@ -4,9 +4,8 @@
class UserFileAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'file_type', 'upload_date', 'contains_nan_values', 'number_of_rows',
- 'number_of_samples', 'decimal_separator', 'is_public', 'is_cpg_site_id', 'platform',
- 'tissue_list')
- list_filter = ('file_type', 'upload_date', 'is_public', 'is_cpg_site_id', 'tissues')
+ 'number_of_samples', 'decimal_separator', 'is_public', 'is_cpg_site_id', 'platform', 'tissue')
+ list_filter = ('file_type', 'upload_date', 'is_public', 'is_cpg_site_id', 'tissue')
search_fields = ('name', 'description', 'user__username')
filter_horizontal = ('tissues',)
diff --git a/src/user_files/models.py b/src/user_files/models.py
index 35f9ee75..46f1d5d7 100644
--- a/src/user_files/models.py
+++ b/src/user_files/models.py
@@ -15,6 +15,7 @@
from institutions.models import Institution
from tags.models import Tag
from tissues.models import Tissue
+from tissues.models import Tissue
from user_files.models_choices import FileType, FileDecimalSeparator
from user_files.utils import get_decimal_separator_and_numerical_data, read_excel_in_chunks
@@ -33,7 +34,7 @@ class UserFile(models.Model):
file_obj = models.FileField(upload_to=user_directory_path)
file_type = models.IntegerField(choices=FileType.choices)
tag = models.ForeignKey(Tag, on_delete=models.SET_NULL, blank=True, null=True)
- tissues = models.ManyToManyField(Tissue, blank=True)
+ tissue = models.ForeignKey(Tissue, on_delete=models.SET_NULL, blank=True, null=True)
upload_date = models.DateTimeField(auto_now_add=True, blank=False, null=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
institutions = models.ManyToManyField(Institution, blank=True)
diff --git a/src/user_files/serializers.py b/src/user_files/serializers.py
index 6e175ba4..50433fe0 100644
--- a/src/user_files/serializers.py
+++ b/src/user_files/serializers.py
@@ -10,6 +10,7 @@
from users.serializers import UserSimpleSerializer
from institutions.serializers import InstitutionSimpleSerializer
from tags.serializers import TagSerializer
+from tissues.serializers import TissueSerializer
from user_files.models import UserFile
@@ -39,7 +40,7 @@ class UserFileSerializer(serializers.ModelSerializer):
class Meta:
model = UserFile
- fields = ['id', 'name', 'description', 'file_obj', 'file_type', 'tag', 'tag_id', 'tissues',
+ fields = ['id', 'name', 'description', 'file_obj', 'file_type', 'tag', 'tag_id', 'tissue', 'tissue_id',
'upload_date', 'institutions', 'number_of_rows', 'number_of_samples', 'user', 'contains_nan_values',
'column_used_as_index', 'is_cpg_site_id', 'platform', 'survival_columns', 'is_public']
@@ -65,6 +66,8 @@ def to_representation(self, instance):
if instance.tag:
data['tag'] = TagSerializer(instance.tag).data
+ data['tissue'] = TissueSerializer(instance.tissue).data if instance.tissue else None
+
data['institutions'] = InstitutionSimpleSerializer(instance.institutions, many=True, read_only=True).data
data['survival_columns'] = SurvivalColumnsTupleUserFileSimpleSerializer(
instance.survival_columns,
@@ -124,7 +127,7 @@ def update(self, instance: UserFile, validated_data):
instance.file_type = validated_data.get('file_type', instance.file_type)
instance.description = validated_data.get('description', instance.description)
instance.tag = validated_data.get('tag')
- instance.tissues.set(validated_data.get('tissues', []))
+ instance.tissue = validated_data.get('tissue')
instance.institutions.set(validated_data.get('institutions', []))
instance.is_cpg_site_id = validated_data.get('is_cpg_site_id')
instance.platform = validated_data.get('platform')
diff --git a/src/user_files/views.py b/src/user_files/views.py
index f2af2d22..82098375 100644
--- a/src/user_files/views.py
+++ b/src/user_files/views.py
@@ -149,7 +149,7 @@ def get_queryset(self):
serializer_class = UserFileWithoutFileObjSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend]
- filterset_fields = ['tag', 'file_type', 'institutions', 'tissues']
+ filterset_fields = ['tag', 'file_type', 'institutions', 'tissue']
search_fields = ['name', 'description']
ordering_fields = ['name', 'description', 'upload_date', 'tag', 'user', 'file_type']
pagination_class = StandardResultsSetPagination
From 8742d95328524febd35391eb6605130ac7dbc337 Mon Sep 17 00:00:00 2001
From: Hernan
Date: Thu, 5 Mar 2026 19:51:46 -0300
Subject: [PATCH 14/16] Make tissue a ManyToMany on files and studies
Convert Tissue FK to ManyToMany on CGDSStudy and UserFile and update related code. Models updated to use tissues M2M; serializers changed (tissue_id -> tissue_ids, nested tissues in responses, create/update now set M2M relations); views' filterset_fields switched to 'tissues'; admin updated to filter and edit M2M with filter_horizontal and a user-facing tissue list column. Added migrations to replace FK with M2M for datasets_synchronization and user_files, and a new tissues migration that adds a unique code field and loads standardized initial tissue data (replacing the previous locale-specific seed). Run migrations after pulling these changes.
---
src/datasets_synchronization/models.py | 2 +-
src/datasets_synchronization/serializers.py | 13 +++--
src/datasets_synchronization/views.py | 2 +-
.../migrations/0002_load_initial_tissues.py | 50 -------------------
src/tissues/models.py | 1 +
src/tissues/serializers.py | 2 +-
src/user_files/admin.py | 7 +--
src/user_files/models.py | 2 +-
src/user_files/serializers.py | 13 +++--
src/user_files/views.py | 2 +-
10 files changed, 26 insertions(+), 68 deletions(-)
delete mode 100644 src/tissues/migrations/0002_load_initial_tissues.py
diff --git a/src/datasets_synchronization/models.py b/src/datasets_synchronization/models.py
index 45166fd6..43a7b472 100644
--- a/src/datasets_synchronization/models.py
+++ b/src/datasets_synchronization/models.py
@@ -289,7 +289,7 @@ class CGDSStudy(models.Model):
null=True,
related_name='cgds_studies_as_clinical_sample_dataset'
)
- tissue = models.ForeignKey(Tissue, on_delete=models.SET_NULL, blank=True, null=True)
+ tissues = models.ManyToManyField(Tissue, blank=True)
task_id: Optional[str] = models.CharField(max_length=100, blank=True, null=True) # Celery Task ID
def __str__(self) -> str:
diff --git a/src/datasets_synchronization/serializers.py b/src/datasets_synchronization/serializers.py
index 73a0cac8..bdc73ccc 100644
--- a/src/datasets_synchronization/serializers.py
+++ b/src/datasets_synchronization/serializers.py
@@ -56,11 +56,11 @@ class CGDSStudySerializer(serializers.ModelSerializer):
clinical_sample_dataset = CGDSDatasetSerializer(required=False, allow_null=True)
version = serializers.IntegerField(read_only=True)
is_last_version = serializers.SerializerMethodField(method_name='get_is_last_version')
- # tissue_id: writable PK field; tissue: read-only nested (set in to_representation)
- tissue_id = serializers.PrimaryKeyRelatedField(
+ # tissue_ids: writable list of PKs; tissues: read-only nested list (set in to_representation)
+ tissue_ids = serializers.PrimaryKeyRelatedField(
queryset=Tissue.objects.all(),
- source='tissue',
- allow_null=True,
+ source='tissues',
+ many=True,
required=False
)
@@ -73,9 +73,9 @@ def get_is_last_version(study: CGDSStudy) -> bool:
return study.version == study.get_last_version()
def to_representation(self, instance: CGDSStudy):
- """Makes a nested representation of the tissue field on GET requests."""
+ """Makes a nested representation of the tissues field on GET requests."""
data = super().to_representation(instance)
- data['tissue'] = TissueSerializer(instance.tissue).data if instance.tissue else None
+ data['tissues'] = TissueSerializer(instance.tissues.all(), many=True).data
return data
def __create_cgds_dataset(self, validated_data_pop: OrderedDict) -> Optional[CGDSDataset]:
@@ -311,7 +311,6 @@ def update(self, instance: CGDSStudy, validated_data):
instance.description = validated_data.get('description', instance.description)
instance.url = validated_data.get('url', instance.url)
instance.url_study_info = validated_data.get('url_study_info', instance.url_study_info)
- instance.tissue = validated_data.get('tissue', instance.tissue)
# Updates datasets
instance.mrna_dataset = mrna_dataset
diff --git a/src/datasets_synchronization/views.py b/src/datasets_synchronization/views.py
index ec5997be..ccfe9ab0 100644
--- a/src/datasets_synchronization/views.py
+++ b/src/datasets_synchronization/views.py
@@ -85,7 +85,7 @@ def get_queryset(self):
permission_classes = [permissions.IsAuthenticated]
pagination_class = StandardResultsSetPagination
filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend]
- filterset_fields = ['tissue']
+ filterset_fields = ['tissues']
search_fields = ['name', 'description']
ordering_fields = '__all__'
diff --git a/src/tissues/migrations/0002_load_initial_tissues.py b/src/tissues/migrations/0002_load_initial_tissues.py
deleted file mode 100644
index f181bf15..00000000
--- a/src/tissues/migrations/0002_load_initial_tissues.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Generated by Django 4.2.19 on 2026-02-26 22:58
-
-from django.db import migrations
-
-TISSUES = [
- 'Cerebro (GBM)',
- 'Cerebro (Glioma)',
- 'Colon',
- 'Cérvix',
- 'Estómago',
- 'Esófago',
- 'Glándula Adrenal',
- 'Hígado',
- 'Mama',
- 'Ovario',
- 'Piel',
- 'Próstata',
- 'Pulmón',
- 'Pulmón (Escamoso)',
- 'Páncreas',
- 'Recto',
- 'Riñón (KIRC)',
- 'Riñón (KIRP)',
- 'Sangre (Leucemia)',
- 'Testículo',
- 'Tiroides',
- 'Vejiga',
- 'Útero',
-]
-
-
-def load_tissues(apps, schema_editor):
- Tissue = apps.get_model('tissues', 'Tissue')
- Tissue.objects.bulk_create([Tissue(name=name) for name in TISSUES])
-
-
-def unload_tissues(apps, schema_editor):
- Tissue = apps.get_model('tissues', 'Tissue')
- Tissue.objects.filter(name__in=TISSUES).delete()
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('tissues', '0001_initial'),
- ]
-
- operations = [
- migrations.RunPython(load_tissues, reverse_code=unload_tissues),
- ]
\ No newline at end of file
diff --git a/src/tissues/models.py b/src/tissues/models.py
index 9b769759..d8fe7f7a 100644
--- a/src/tissues/models.py
+++ b/src/tissues/models.py
@@ -5,6 +5,7 @@
class Tissue(models.Model):
"""Reference model for tissue types. Tissues are read-only and cannot be deleted."""
name = models.CharField(max_length=100, unique=True)
+ code = models.CharField(max_length=100, unique=True) # noqa: populated by migration 0003
class Meta:
ordering = ['name']
diff --git a/src/tissues/serializers.py b/src/tissues/serializers.py
index 6a106382..9329da93 100644
--- a/src/tissues/serializers.py
+++ b/src/tissues/serializers.py
@@ -5,4 +5,4 @@
class TissueSerializer(serializers.ModelSerializer):
class Meta:
model = Tissue
- fields = ['id', 'name']
+ fields = ['id', 'name', 'code']
diff --git a/src/user_files/admin.py b/src/user_files/admin.py
index 6a05a0b6..40c90524 100644
--- a/src/user_files/admin.py
+++ b/src/user_files/admin.py
@@ -4,13 +4,14 @@
class UserFileAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'file_type', 'upload_date', 'contains_nan_values', 'number_of_rows',
- 'number_of_samples', 'decimal_separator', 'is_public', 'is_cpg_site_id', 'platform', 'tissue')
- list_filter = ('file_type', 'upload_date', 'is_public', 'is_cpg_site_id', 'tissue')
+ 'number_of_samples', 'decimal_separator', 'is_public', 'is_cpg_site_id', 'platform',
+ 'tissue_list')
+ list_filter = ('file_type', 'upload_date', 'is_public', 'is_cpg_site_id', 'tissues')
search_fields = ('name', 'description', 'user__username')
filter_horizontal = ('tissues',)
def tissue_list(self, obj):
- return ', '.join(obj.tissues.values_list('name', flat=True))
+ return ', '.join(t.name for t in obj.tissues.all())
tissue_list.short_description = 'Tissues'
diff --git a/src/user_files/models.py b/src/user_files/models.py
index 46f1d5d7..04cf1b24 100644
--- a/src/user_files/models.py
+++ b/src/user_files/models.py
@@ -34,7 +34,7 @@ class UserFile(models.Model):
file_obj = models.FileField(upload_to=user_directory_path)
file_type = models.IntegerField(choices=FileType.choices)
tag = models.ForeignKey(Tag, on_delete=models.SET_NULL, blank=True, null=True)
- tissue = models.ForeignKey(Tissue, on_delete=models.SET_NULL, blank=True, null=True)
+ tissues = models.ManyToManyField(Tissue, blank=True)
upload_date = models.DateTimeField(auto_now_add=True, blank=False, null=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
institutions = models.ManyToManyField(Institution, blank=True)
diff --git a/src/user_files/serializers.py b/src/user_files/serializers.py
index 50433fe0..8d086f1e 100644
--- a/src/user_files/serializers.py
+++ b/src/user_files/serializers.py
@@ -10,6 +10,7 @@
from users.serializers import UserSimpleSerializer
from institutions.serializers import InstitutionSimpleSerializer
from tags.serializers import TagSerializer
+from tissues.models import Tissue
from tissues.serializers import TissueSerializer
from user_files.models import UserFile
@@ -37,10 +38,16 @@ class Meta:
class UserFileSerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(many=False, read_only=True)
survival_columns = SurvivalColumnsTupleUserFileSimpleSerializer(many=True, read_only=True)
+ tissue_ids = serializers.PrimaryKeyRelatedField(
+ queryset=Tissue.objects.all(),
+ source='tissues',
+ many=True,
+ required=False
+ )
class Meta:
model = UserFile
- fields = ['id', 'name', 'description', 'file_obj', 'file_type', 'tag', 'tag_id', 'tissue', 'tissue_id',
+ fields = ['id', 'name', 'description', 'file_obj', 'file_type', 'tag', 'tag_id', 'tissues', 'tissue_ids',
'upload_date', 'institutions', 'number_of_rows', 'number_of_samples', 'user', 'contains_nan_values',
'column_used_as_index', 'is_cpg_site_id', 'platform', 'survival_columns', 'is_public']
@@ -66,7 +73,7 @@ def to_representation(self, instance):
if instance.tag:
data['tag'] = TagSerializer(instance.tag).data
- data['tissue'] = TissueSerializer(instance.tissue).data if instance.tissue else None
+ data['tissues'] = TissueSerializer(instance.tissues.all(), many=True).data
data['institutions'] = InstitutionSimpleSerializer(instance.institutions, many=True, read_only=True).data
data['survival_columns'] = SurvivalColumnsTupleUserFileSimpleSerializer(
@@ -127,7 +134,7 @@ def update(self, instance: UserFile, validated_data):
instance.file_type = validated_data.get('file_type', instance.file_type)
instance.description = validated_data.get('description', instance.description)
instance.tag = validated_data.get('tag')
- instance.tissue = validated_data.get('tissue')
+ instance.tissues.set(validated_data.get('tissues', []))
instance.institutions.set(validated_data.get('institutions', []))
instance.is_cpg_site_id = validated_data.get('is_cpg_site_id')
instance.platform = validated_data.get('platform')
diff --git a/src/user_files/views.py b/src/user_files/views.py
index 82098375..f2af2d22 100644
--- a/src/user_files/views.py
+++ b/src/user_files/views.py
@@ -149,7 +149,7 @@ def get_queryset(self):
serializer_class = UserFileWithoutFileObjSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend]
- filterset_fields = ['tag', 'file_type', 'institutions', 'tissue']
+ filterset_fields = ['tag', 'file_type', 'institutions', 'tissues']
search_fields = ['name', 'description']
ordering_fields = ['name', 'description', 'upload_date', 'tag', 'user', 'file_type']
pagination_class = StandardResultsSetPagination
From f1972c35fe9e765ba72b54c7cea44350b54a8753 Mon Sep 17 00:00:00 2001
From: Hernan
Date: Fri, 20 Mar 2026 11:36:07 -0300
Subject: [PATCH 15/16] Refactor tissue representation in serializers and
admin; remove unused fields
---
src/datasets_synchronization/serializers.py | 15 ---------------
src/user_files/admin.py | 2 +-
src/user_files/serializers.py | 12 +-----------
3 files changed, 2 insertions(+), 27 deletions(-)
diff --git a/src/datasets_synchronization/serializers.py b/src/datasets_synchronization/serializers.py
index bdc73ccc..3f78d88f 100644
--- a/src/datasets_synchronization/serializers.py
+++ b/src/datasets_synchronization/serializers.py
@@ -8,8 +8,6 @@
from common.response import ResponseStatus
from .enums import CreateCGDSStudyResponseCode
from .models import CGDSStudy, CGDSDataset, SurvivalColumnsTupleCGDSDataset
-from tissues.models import Tissue
-from tissues.serializers import TissueSerializer
from django.db.models import Q
@@ -56,13 +54,6 @@ class CGDSStudySerializer(serializers.ModelSerializer):
clinical_sample_dataset = CGDSDatasetSerializer(required=False, allow_null=True)
version = serializers.IntegerField(read_only=True)
is_last_version = serializers.SerializerMethodField(method_name='get_is_last_version')
- # tissue_ids: writable list of PKs; tissues: read-only nested list (set in to_representation)
- tissue_ids = serializers.PrimaryKeyRelatedField(
- queryset=Tissue.objects.all(),
- source='tissues',
- many=True,
- required=False
- )
class Meta:
model = CGDSStudy
@@ -72,12 +63,6 @@ class Meta:
def get_is_last_version(study: CGDSStudy) -> bool:
return study.version == study.get_last_version()
- def to_representation(self, instance: CGDSStudy):
- """Makes a nested representation of the tissues field on GET requests."""
- data = super().to_representation(instance)
- data['tissues'] = TissueSerializer(instance.tissues.all(), many=True).data
- return data
-
def __create_cgds_dataset(self, validated_data_pop: OrderedDict) -> Optional[CGDSDataset]:
"""
Creates a CGDSDataset instance from a request data.
diff --git a/src/user_files/admin.py b/src/user_files/admin.py
index 40c90524..2e05957d 100644
--- a/src/user_files/admin.py
+++ b/src/user_files/admin.py
@@ -11,7 +11,7 @@ class UserFileAdmin(admin.ModelAdmin):
filter_horizontal = ('tissues',)
def tissue_list(self, obj):
- return ', '.join(t.name for t in obj.tissues.all())
+ return ', '.join(obj.tissues.values_list('name', flat=True))
tissue_list.short_description = 'Tissues'
diff --git a/src/user_files/serializers.py b/src/user_files/serializers.py
index 8d086f1e..6e175ba4 100644
--- a/src/user_files/serializers.py
+++ b/src/user_files/serializers.py
@@ -10,8 +10,6 @@
from users.serializers import UserSimpleSerializer
from institutions.serializers import InstitutionSimpleSerializer
from tags.serializers import TagSerializer
-from tissues.models import Tissue
-from tissues.serializers import TissueSerializer
from user_files.models import UserFile
@@ -38,16 +36,10 @@ class Meta:
class UserFileSerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(many=False, read_only=True)
survival_columns = SurvivalColumnsTupleUserFileSimpleSerializer(many=True, read_only=True)
- tissue_ids = serializers.PrimaryKeyRelatedField(
- queryset=Tissue.objects.all(),
- source='tissues',
- many=True,
- required=False
- )
class Meta:
model = UserFile
- fields = ['id', 'name', 'description', 'file_obj', 'file_type', 'tag', 'tag_id', 'tissues', 'tissue_ids',
+ fields = ['id', 'name', 'description', 'file_obj', 'file_type', 'tag', 'tag_id', 'tissues',
'upload_date', 'institutions', 'number_of_rows', 'number_of_samples', 'user', 'contains_nan_values',
'column_used_as_index', 'is_cpg_site_id', 'platform', 'survival_columns', 'is_public']
@@ -73,8 +65,6 @@ def to_representation(self, instance):
if instance.tag:
data['tag'] = TagSerializer(instance.tag).data
- data['tissues'] = TissueSerializer(instance.tissues.all(), many=True).data
-
data['institutions'] = InstitutionSimpleSerializer(instance.institutions, many=True, read_only=True).data
data['survival_columns'] = SurvivalColumnsTupleUserFileSimpleSerializer(
instance.survival_columns,
From 77d3eef0dd57ee8d4c68aa283f5b7d41a04391d4 Mon Sep 17 00:00:00 2001
From: GonzaGomez
Date: Tue, 21 Apr 2026 20:08:45 -0300
Subject: [PATCH 16/16] Fixes styles in create Biomarker modal and remove
unused imports
---
.../BiomarkerFromCorrelationModal.tsx | 30 +++++--
src/frontend/static/frontend/src/css/gem.css | 87 +++++++++++++++++++
src/frontend/views.py | 14 ---
3 files changed, 109 insertions(+), 22 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
index 8ea6fd4a..a6123bb0 100644
--- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
+++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx
@@ -83,7 +83,8 @@ interface BiomarkerFromCorrelationModalState {
openDetailsModal2: boolean,
selectedOption: SelectedOption,
openSelectOptionModal: boolean,
- experimentInfoWithoutFilters: ExperimentInfo
+ experimentInfoWithoutFilters: ExperimentInfo,
+ modalReady: boolean,
}
/**
@@ -119,6 +120,7 @@ export class BiomarkerFromCorrelationModal extends React.Component {
this.setState({ loadingFullBiomarkerId: biomarkerSimple.id })
ky.get(urlBiomarkersCRUD + '/' + biomarkerSimple.id + '/', { signal: this.abortController.signal }).then((response) => {
- response.json().then((jsonResponse: Biomarker | PromiseLike) => {
+ response.json().then((jsonResponse) => {
resolve(jsonResponse)
}).catch((err) => {
console.error('Error parsing JSON on Biomarker retrieval:', err)
@@ -672,7 +674,7 @@ export class BiomarkerFromCorrelationModal extends React.Component {
- response.json().then((jsonResponse: MoleculeFinderResult[]) => {
+ response.json().then((jsonResponse) => {
const formBiomarker = this.state.formBiomarker
const checkedIgnoreProposedAlias = this.state.checkedIgnoreProposedAlias // For short
@@ -832,7 +834,7 @@ export class BiomarkerFromCorrelationModal extends React.Component {
- response.json().then((jsonResponse: { [key: string]: string[] }) => {
+ response.json<{ [key: string]: string[] }>().then((jsonResponse) => {
const genes = Object.entries(jsonResponse)
for (const gene of genes) {
@@ -1056,7 +1058,7 @@ export class BiomarkerFromCorrelationModal extends React.Component {
- response.json().then((_jsonResponse: Biomarker) => {
+ response.json().then((_jsonResponse) => {
this.closeModalWithSuccessMsg('Biomarker created successfully')
}).catch((err) => {
console.log('Error parsing JSON ->', err)
@@ -1075,7 +1077,7 @@ export class BiomarkerFromCorrelationModal extends React.Component {
- response.json().then((_jsonResponse: Biomarker) => {
+ response.json().then((_jsonResponse) => {
this.closeModalWithSuccessMsg('Biomarker edited successfully')
}).catch((err) => {
console.log('Error parsing JSON ->', err)
@@ -1216,7 +1218,12 @@ export class BiomarkerFromCorrelationModal extends React.Component this.closeBiomarkerModal()}
+ onOpen={() => {
+ this.setState({ modalReady: true }, () => {
+ window.dispatchEvent(new Event('resize'))
+ })
+ }}
>
+
{/* Biomarker details modal. */}
diff --git a/src/frontend/static/frontend/src/css/gem.css b/src/frontend/static/frontend/src/css/gem.css
index 27c3edfb..94bb6fb2 100644
--- a/src/frontend/static/frontend/src/css/gem.css
+++ b/src/frontend/static/frontend/src/css/gem.css
@@ -206,3 +206,90 @@
opacity: 1;
text-decoration: underline;
}
+
+.biomarkers--side--bar--box {
+ margin: 1rem 0;
+}
+
+.biomarkers--side--bar--buttons-group {
+ width: 100%;
+}
+
+.biomarkers--side--bar--input--selection {
+ margin: 0.5rem 0;
+ width: 100%;
+}
+
+.biomarkers--modal--container {
+ height: 100%;
+}
+
+.large-modal {
+ min-height: 92% !important;
+ width: 92% !important;
+}
+
+.space-modal:nth-child(1) {
+ display: flex !important;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.biomarkers--side--bar--container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+}
+
+.biomarkers--side--bar--container--item--margin {
+ margin: 0.5rem 0;
+}
+
+.biomarkers--side--bar--buttons--box {
+ align-self: flex-end;
+ display: flex;
+ justify-self: flex-end;
+ margin-top: 0.5rem;
+}
+
+.biomarkers--side--bar--validation--items {
+ margin: 8px 0px 8px 0px;
+ width: 100%;
+}
+
+.biomarkers--molecules--container {
+ max-width: 100%;
+ height: 32vh;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ margin-top: 0.5rem;
+}
+
+.biomarkers--molecules--container--item {
+ margin: 0.5rem;
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-self: center;
+}
+
+.biomarkers--molecules--container--grid {
+ width: 100%;
+}
+
+.biomarker--section--icon {
+ padding: 0 0.5rem;
+ cursor: pointer;
+}
+
+.biomarker--section--button {
+ cursor: default !important;
+}
+
+.row-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+}
\ No newline at end of file
diff --git a/src/frontend/views.py b/src/frontend/views.py
index eaed2acf..5a9a181f 100644
--- a/src/frontend/views.py
+++ b/src/frontend/views.py
@@ -1,23 +1,9 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.conf import settings
-from rest_framework.views import APIView
-from rest_framework.request import Request
-from rest_framework import permissions
-from typing import List, Optional, Dict
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
-from django_filters.rest_framework import DjangoFilterBackend
-from rest_framework import generics, permissions, filters
-from rest_framework.request import Request
-from rest_framework.views import APIView
-from api_service.mrna_service import global_mrna_service
-from biomarkers.models import Biomarker, BiomarkerState, BiomarkerOrigin
-from biomarkers.serializers import BiomarkerSerializer, BiomarkerSimpleSerializer, BiomarkerSimpleUpdateSerializer
-from common.pagination import StandardResultsSetPagination
-from common.response import generate_json_response_or_404
-from django.db.models import Q
def index_action(request):
"""Index view"""