#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

#66 🪟 Add Visualizer Modal - #248

Merged
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal
Jun 26, 2025
Merged

#66 🪟 Add Visualizer Modal#248
demariadaniel merged 23 commits into
iobiofrom
66/feat-add-visualizer-modal

Conversation

@demariadaniel

@demariadanieldemariadaniel commented Jun 17, 2025

Copy link
Copy Markdown

Summary

Adds Visualizer Modal to select between JBrowse, Iobio & cBio tables

Issues

Description of Changes

Creates VisualizerModal with updated Table Header navigation
Adds base logic for enabling/disabling different visualizer tables
Adds environment variables to feature flag JBrowse, Iobio & CBio

Special Instructions

Env requires new variable for Iobio testing: NEXT_PUBLIC_IOBIO_ENABLED=true

Env variables are also added for NEXT_PUBLIC_JBROWSE_ENABLED & NEXT_PUBLIC_CBIOPORTAL_ENABLED but can be left disabled

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadanieldemariadaniel self-assigned this Jun 18, 2025
@demariadaniel
demariadaniel changed the base branch from main to iobioJune 18, 2025 15:01
const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData is unknown[] | [] here so type guard isn't working as expected without casting. Open to suggestions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

@demariadaniel
demariadaniel marked this pull request as ready for review June 18, 2025 17:07

@joneubankjoneubank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the Visualizer Button interface.

Comment thread.env.schema Outdated
isBamFileSelected: boolean;
isFileTableActive: boolean;
switchTable: (t: string) => void;
openModal: () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openModal is required property but is not used if visualizersEnabled is false... so now we require someone using this header button to provide a openModal value even if it is not used.

We should make sure we don't require unused props, can use discriminated union as our props based on visualizersEnabled, or move the visualizers properties to an optional visualizersConfig prop and then make that config prop's existence be used to enable/disable the button.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a good point about using visualizersConfig; the two boolean props are ONLY used for disabled={!visualizersEnabled && isFileTableActive} so I'm going to combine them as a disabled prop

And then openModal can be made optional as well

}
`}
onClick={() => switchTable(tableTypes['BAM_TABLE'])}
onClick={openModal}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, all of the behaviour of this button has been moved outside of this component. The button does whatever openModal specifies. Even a decision about whether it is enabled is based on props.

Perhaps its just a case where we need to rename the props to onClick instead of openModal and replace visualizerEnabled and isFileTableActive to a single disabled or enabled prop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into how I can organize this better. The intent was to have openModal / closeModal as fixed utilities which always do the same thing, but because they use state you can't export them that way, and have to be scoped within a component render/props pattern (which is intended to be mutable). So you get this sort of organizational conflict.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use disabled

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a consequence of using an older version of TS, since I believe it should properly update the type on the latest TS versions. The filter is protecting things well so this is fine enough.

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments. Unclear what level of "correct" we're going for here based on the future of "stage", so I'll leave that decision (pr approval) up to the architects 👍 (I think @joneubank has eyes on this)

The overall page content component feels like it's doing too much, and some of this code could be placed into it's own component and there's a lot of mixed up styling paradigms.

Comment threadcomponents/pages/explorer/HeaderButtons.tsx Outdated
Comment threadcomponents/pages/explorer/PageContent.tsx Outdated
Comment on lines +31 to +117
const getCardContainerStyle = (theme: Theme) => css`
display: flex;
height: 100%;

.file-container {
bottom: 10px;
position: absolute;

.badge {
display: inline-flex;
border: none;
border-radius: 20px;
margin: 5px;
min-width: fit-content;
padding: 3px 10px;
background-color: ${theme.colors.accent};
color: ${theme.colors.white};

&.disabled {
background-color: ${theme.colors.grey_6};
}

&.format {
background-color: ${theme.colors.accent_light};
&.disabled {
background-color: ${theme.colors.grey_5};
}
}
}
}

.visualizer-card {
background: unset;
border: 1px solid ${theme.colors.grey_5};
border-radius: 16px;
cursor: pointer;
display: inline-flex;
font-family: 'Lato', sans-serif;
margin: 0 0.5rem;
padding: 10px;
position: relative;
width: 33%;

:disabled {
cursor: not-allowed;
}

p {
font-weight: 400;
font-size: 16px;
line-height: 16px;
margin-top: 0px;
text-align: left;
height: 30%;
}

h4 {
display: inline-block;
font-size: 18px;
margin: 0.5rem;
}

h5 {
font-weight: 700;
font-size: 16px;
margin: 0.25rem 0;
text-align: left;
}
}

.logo {
height: 18px;
vertical-align: text-bottom;
width: 18px;
}

.preview {
max-height: 28%;
overflow-y: hidden;
width: 100%;

img {
width: 100%;
}
}
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermixing classic CSS classname styling with CSS-in-JS code style established in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've moved use of classes into a CSS-in-JS pattern

Comment on lines +259 to +261
<div className={`badge ${isJbrowseEnabled ? '' : 'disabled'}`}>5 Max</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.VCF</div>
<div className={`badge format ${isJbrowseEnabled ? '' : 'disabled'}`}>.BAM</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this has something to do with usage of ReactModal.... but using the established CSS patterns, in this case Emotion CSS-in-JS, is usually a workable solution. Emotion specifically works with the standard className prop.

Comment on lines +195 to +197
style={css`
vertical-align: middle;
`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works - css call from Emotion needs the className prop or the css prop, as style works completely differently

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does work correctly, you can inspect the Modal X icon in browser and edit the vertical-align property

@ciaranschutteciaranschutteJun 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is confusing.

style is already a html attribute and we're overloading the meaning in Dismiss component.
So it's a custom prop called 'style' that then uses the Emotion css prop internally.

So we have both className and style in use ... sometimes they're html attributes.. other times they're React props, and sometimes they're strings and styled as selectors, and sometimes they're styled using Emotion functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the Icon components are all using this style prop convention

Comment threadcomponents/pages/explorer/VisualizerModal.tsx Outdated
Comment on lines 76 to 82
useEffect(() => {
if (firstRender) {
currentFilters && setSQON(currentFilters);
ReactModal.setAppElement('#pageContent');
setFirstRender(false);
}
}, [currentFilters, firstRender, setSQON]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the firstRender logic here. Usually a code smell having state to check the first render.
Shouldn't this just be on mount?

then this:
currentFilters && setSQON(currentFilters);
is manually syncing syncing state between them when they should share a single source of state?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look like this actually using url state either? Nothing updates in the url bar on filter selection.
I thought Stage was using all Overture work, where as the "currentFilter" stuff is ARGO code.
Basically the thing I mentioned in the demo about everything reading from sqon state

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not create the firstRender useEffect here, you'd have to ask @justincorrigible, but I am leveraging it here because ReactModal crashes if the targeted app element isn't rendered on the page.
So useEffect manages Next/Arranger/ReactModal setup here, other approaches weren't working for me.

re: fileData derived state, that is a good call, useEffect was added for early dev states setting up the Iobio Components but is no longer required

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sounds like a "onMount" using useEffect.
You may not have added "firstRender" but I don't understand why setting ReactModal.setAppElement('#pageContent'); depends on "firstRender" state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not explaining the need for on mount behaviour... but the linter will require the firstRender in the dependency array because its used in the function. Leads to some strange behaviour where setting firstRender to true will repeat the effect.

const isBamFile = Boolean(file_type && BamFileExtensions.includes(file_type));
return idMatch && isBamFile;
});
const fileData = tableData.filter(rowIsFileData) as FileTableData[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can't this just be derived state instead of additional useEffect?

Comment on lines +233 to +237
<div
css={css`
width: 100%;
`}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels hacky, div should be full width by default, it's a fundamental diff between div and span blocks.
I removed it and on a quick glance everything looks ok, but please verify

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is definitely overkill, removed + a few more instances of width: 100%. CSS is where the hacking can go into overdrive some times ;)

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the use of css unset vs none?

@ciaranschutte

ciaranschutte commented Jun 21, 2025

Copy link
Copy Markdown
Contributor

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

typeVizDetailProps={title: string,description: string,previewImage: string,logoImage: string,};constVizDetail=({
title,
description,
previewImage,
logoImage,}: VizDetailProps)=>{return(<><divcss={css`max-height:28%;overflow-y: hidden;img {width:100%; } `}><imgsrc={previewImage}/></div><divcss={css`text-align: left; `}><imgcss={css`height:18px;vertical-align: text-bottom;width:18px; `}src={logoImage}/><h4css={css`font-size:18px;margin:0.5rem; `}>{title}</h4></div><pcss={css`font-weight:400;font-size:16px;line-height:16px;margin-top:0px;text-align: left;height:30%; `}>{description}</p></>);};constBadges=({ isEnabled, badges })=>{returnbadges.map((badge)=>{return<divcss={badgeStyle({isDisabled: !isEnabled})}>{badge}</div>;});};exportconstVizOption=({
onClick,
isEnabled,
viz,}: {onClick: ()=>void,isEnabled: boolean,viz: VizDetailProps,})=>{return(<buttoncss={css`background: unset;border:1px solid ${theme.colors.grey_5};border-radius:16px;cursor: pointer;display: inline-flex;margin:00.5rem;padding:10px;position: relative;:disabled {cursor: not-allowed; } `}disabled={!isEnabled}onClick={onClick}><div><VizDetailviz={viz}/><divcss={css`bottom:10px; `}><h5css={css`font-weight:700;font-size:16px;margin:0.25rem0; `}>
Files:
</h5><divcss={css`display: flex; `}><BadgesisEnabled={isEnabled}badges={viz.badges}/></div></div></div></button>);};// Encapsulates the 3rd party component, some static, and if modal is openconstVizModal=({ children, appRootId, isModalOpen })=>{useEffect(()=>{ReactModal.setAppElement(appRootId);},[]);return(<ReactModal/** * removed ariaHideApp={!!firstRender} * it's defined in its type as "Boolean indicating if the appElement should be hidden. Defaults to true." * so this is only setting to hide app (which is set elsewhere) when firstRender is true? */isOpen={isModalOpen}style={{overlay: {zIndex: 10,backgroundColor: "rgba(0,0,0,0.7)",},content: {top: "15%",left: "20%",width: "60%",height: "70%",padding: "12px",},}}><divcss={css`display: flex;flex-direction: column;font-family:"Lato", sans-serif;height:100%; `}><divcss={css`display: flex;justify-content: end; `}><buttoncss={css`background: unset;border: unset;cursor: pointer;:hover {svg {path {fill:${theme.colors.grey_5}; } } } `}onClick={closeModal}><Dismissstyle={css`vertical-align: middle; `}height={12}width={12}fill={theme.colors.accent_dark}/></button></div><h3css={css`font-size:24px;font-weight:700;line-height:38px;margin:00.5rem; `}>
Select Visualization App:
</h3><pcss={css`font-size:16px;font-weight:400;line-height:26px;margin:0.5rem; `}>
Choose the appropriate app to analyze your selected data.
</p><divcss={css`display: flex;height:100%; `}>{children}</div></div></ReactModal>);};/** * * Responsibility is rendering the options available * ie. read configs, gather data etc then render each option with data * */exportconstVisualizerModal=({
closeModal,
setTable,
isModalOpen,
currentFiles,
appRootId,})=>{const{NEXT_PUBLIC_BASE_PATH,NEXT_PUBLIC_IOBIO_ENABLED,NEXT_PUBLIC_JBROWSE_ENABLED,NEXT_PUBLIC_CBIOPORTAL_ENABLED,}=getConfig();constisJbrowseEnabled=NEXT_PUBLIC_JBROWSE_ENABLED&&currentFiles.length<=5;constisCBioEnabled=NEXT_PUBLIC_CBIOPORTAL_ENABLED&&currentFiles.length<=2;constisIobioEnabled=NEXT_PUBLIC_IOBIO_ENABLED&&currentFiles.length===1&&currentFiles[0].file_type&&BamFileExtensions.includes(currentFiles[0].file_type);// onclick always 1) sets a table option an 2) close modal, we can encapsulate all this in a single functionconstselectViz=(tableType)=>()=>{setTable(tableType);closeModal();};return(<VizModalisModalOpen={isModalOpen}appRootId={appRootId}><VizOptiononClick={selectViz(tableTypes.JBROWSE_TABLE)}isEnabled={isJbrowseEnabled}viz={{title: "JBrowse",description:
"A fully featured genome browser that is capable of visualizing diverse types of genome-located data.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/jBrowse_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.BAM_TABLE)}isEnabled={isIobioEnabled}viz={{title: "IOBIO",description: "Examine your sequence alignment file in seconds",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/IOBIO_Logo.png"),}}/><VizOptiononClick={selectViz(tableTypes.CBIO_TABLE)}isEnabled={isCBioEnabled}viz={{title: "cBioPortal",description:
"Provides visualization, analysis and download of large-scale cancer genomics data sets.",previewImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Preview.png"),logoImage: urlJoin(NEXT_PUBLIC_BASE_PATH,"/images/cBioPortal_Logo.png"),}}/></VizModal>);};

@demariadaniel

Copy link
Copy Markdown
Author

curious about the use of css unset vs none?

unset and none could be interchangeable here. I wanted to revert to default html styles, but on second inspection this is reverting user-agent style, so none is applicable

@demariadaniel

Copy link
Copy Markdown
Author

I had a second swing at some of my wordy suggestions. Obviously this is a quick pass, and ignoring css stuff mostly. Just trying to align with React a bit better - making a component hierarchy, limiting interfaces for components, correct lifecycle usage (render => useEffect => render?). needless to say this should also be put into seperate files/better naming etc and it's not fully complete eg. badges needs more work.

OK this was a necessary & major improvement. I've made the changes as recommended, w/ CSS & functioning: Break Modal into components

Could potentially break components into more atomic files or other small improvements, let me know what you think

@ciaranschutteciaranschutte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 18 TS errors from these changes

  • let's name consistently viz vs visualization

  • general pattern is to colocate styles and types with their components. I appreciate the cleanliness of your approach, unfortunately it's not the established pattern in this codebase.

  • ReactModal.setAppElement('#pageContent') is still in that "firstRender" useEffect dependency as well as in the ModalContainer. I think it should be scoped to the modal content. or at the very least be used in a onMount hook with an empty dependency array. If it does actually depend on "firstRender" please add a comment, very much sounds like it should work with a useEffect onMount scoped to the modal content somewhere. Not sure if using in both places intentionally.

Comment on lines +84 to +98
export const Badges = ({ isEnabled, badges }: { isEnabled: boolean; badges: BadgeItem[] }) => {
const theme = useTheme();
const badgeGroup = badges.map((badge, index) => {
const badgeCss = badge.isAccent
? accentBadgeStyle({ theme, isDisabled: !isEnabled })
: badgeStyle({ theme, isDisabled: !isEnabled });
return (
<div key={`badge-${index}`} css={badgeCss}>
{badge.label}
</div>
);
});
return <>{badgeGroup}</>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these badges were bottom aligned before?

Screenshot 2025-06-26 at 7 15 27 AM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, missing position: absolute, added

import { badgeStyle, accentBadgeStyle, optionStyle } from './styles';
import { BadgeItem, VizDetailProps } from './types';

export const VizDetail = ({ title, description, previewImage, logoImage }: VizDetailProps) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Viz" versus "Visualization"
I'd stick with one or the other, and I think we lean towards the full word.

(acknowledging that I used Viz in my suggestions, lots of quick stuff there that wasn't meant to be indicative of good naming, just better React)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming, colocated styles, badge alignment, setAppElement are all addressed now, but I can't find the TS errors you're reporting? @ciaranschutte

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice this content: url() is missing quotes which is messing up intellisense on my end https://github.com/overture-stack/stage/blob/main/components/pages/explorer/QueryBar.tsx#L74

But otherwise I'm able to run a successful build, no Type errors found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok now! Not sure where they came from

@ciaranschutte
ciaranschutte self-requested a review June 26, 2025 19:52
@demariadaniel
demariadaniel merged commit f0c1279 into iobioJun 26, 2025
@demariadaniel
demariadaniel deleted the 66/feat-add-visualizer-modal branch June 26, 2025 19:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@demariadaniel@ciaranschutte@joneubank