Skip to content

Commit

Permalink
Merge pull request #211 from vgteam/demos
Browse files Browse the repository at this point in the history
Demos for the controls
  • Loading branch information
adamnovak authored Jan 14, 2023
2 parents 4f88c78 + 3d6fee6 commit 94bdd24
Show file tree
Hide file tree
Showing 13 changed files with 485 additions and 16,503 deletions.
16,721 changes: 225 additions & 16,496 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@material-ui/core": "^4.12.4",
Expand All @@ -30,8 +31,11 @@
"polyfill-object.fromentries": "^1.0.1",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-demo": "^2.0.0",
"react-demo-library": "^2.0.0",
"react-dom": "^17.0.2",
"react-fast-compare": "^3.2.0",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1",
"react-select": "^5.6.1",
"react-select-event": "^5.5.1",
Expand Down
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import HeaderForm from "./components/HeaderForm";
import TubeMapContainer from "./components/TubeMapContainer";
import { urlParamsToViewTarget } from "./components/CopyLink";
import CustomizationAccordion from "./components/CustomizationAccordion";
import Footer from "./components/Footer";
import { dataOriginTypes } from "./enums";
import * as tubeMap from "./util/tubemap";
import config from "./config.json";
Expand Down Expand Up @@ -152,6 +153,7 @@ class App extends Component {
}
setColorSetting={this.setColorSetting}
/>
<Footer/>
</div>
);
}
Expand Down
82 changes: 82 additions & 0 deletions src/components/DemoLibrary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/// DemoLibrary.js: page that shows a library of demos for different components included in this project.
/// When developing a component, you can work on it on its demo page until it
/// actually starts working, and then perfect automated tests and add it to the
/// actual application. This is compatible with the dev server's hot reloading!
///
/// To use this:
/// 1. Make your component, Whatever.js
/// 2. Make a demo file, Whatever.demo.js
/// 3. In the demo file, import your component, import Demo from 'react-demo',
/// and make the default export be a <Demo> component:
///
/// import { Whatever } from './Whatever'
/// import { Demo, props as P } from 'react-demo'
/// export default (<Demo target={Whatever} />)
///
/// 4. If the component needs props, pass a "props" attribute to the Demo
/// component, with the props to use. See
/// <https://github.com/rpominov/react-demo#available-demoprops>.
/// 5. If the component needs two-way binding, use "advanced mode"
/// <https://github.com/rpominov/react-demo#advanced-mode>. Make the Demo
/// tag have a body which is a function that takes "props" and "update".
/// Return your component rendered with the "props", and use "update" as if
/// it were setState to update the props.
///
/// For an example, see RegionInput.demo.js
///
/// To see your demos, run `npm run start`, and go to:
///
/// http://127.0.0.1:3001/demo
///
/// (Assuming this component is mounted there in the React router.)

import Library from 'react-demo-library'
import { SafeLink } from "./SafeLink"

// To auto-magically pick up demos, we use this magic to tell Webpack what we
// want it to grab (*.demo.js, recursively), which it can work out at bundle
// time. See
// <https://webpack.js.org/guides/dependency-management/#requirecontext>.
// Can't use constandt here, arguments *MUST* be literals for the magic to
// work.
const getFromContext = require.context('.', true, /\.demo\.js$/)

// Each demo gets an object in this array with fields:
// location: array of path components to mount at, after a hashbang
// demo: React element for the demo itself, a react-demo Demo component
// description: text about the demo, if any
// importPath: hint about where to find the component to use it
// fullWidth: boolean, true if we want to hide the demo picker
// files: array of name and content objects to show along with the demo.
//
// Really we just need location and demo set.
let demoList = []

for (let moduleName of getFromContext.keys()) {
// Load all the demos available.

// Work out what import this should be a demo for (drop the whole extension)
let componentFile = moduleName.replace(/\.demo\.js$/, '')
// Work out what this should be a demo for (drop './' and extension and split
// path segments).
// Last one will be the component name.
let componentPathSegments = componentFile.replace(/^\.\//, '').split('/')

// We need to fetch out the default export manually by its magic name. See
// <https://stackoverflow.com/a/33705077>
demoList.push({location: componentPathSegments, importPath: componentFile, demo: getFromContext(moduleName).default})
}

export const DemoLibrary = () => {
return (
<>
{
// Because the demo UI does a bunch of full-browser-size stuff we have to float our back link over it
}
<div style={{position: "absolute", left: "0.5em", bottom: "0.5em", zIndex: 1001}}>
<SafeLink to="/">Back to Tube Map</SafeLink>
</div>
<Library demos={demoList}/>
</>
)
}
44 changes: 44 additions & 0 deletions src/components/Footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/// Footer.js: main page footer component. Includes project page links and demo link.

import { Container, Row, Col, Navbar, Nav, NavItem, NavLink } from "reactstrap"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faGithub } from "@fortawesome/free-brands-svg-icons"
import { faCode, faDna } from "@fortawesome/free-solid-svg-icons"

import SafeLink from "./SafeLink"

import PACKAGE from "../../package.json"


export const Footer = () => {
return (
<Container tag="footer" fluid={true} style={{marginTop: "1em"}}>
<Row className="bg-light">
<Col lg={{offset: 2, size: 8}}>
<Navbar>
<Nav className="mr-auto">
<NavItem>
<NavLink target="_blank" href="https://github.com/vgteam/sequenceTubeMap"><FontAwesomeIcon icon={faGithub} size="md" /> Github</NavLink>
</NavItem>
<NavItem>
<NavLink target="_blank" href="https://genomics.ucsc.edu/"><FontAwesomeIcon icon={faDna} size="md" /> UCSC GI</NavLink>
</NavItem>
</Nav>
<Nav>
<NavItem>
{PACKAGE.name} v{PACKAGE.version}
</NavItem>
</Nav>
<Nav>
<NavItem>
<SafeLink to="/demo" title="Component Demos" style={{textDecoration: "none"}}><NavLink><FontAwesomeIcon icon={faCode} size="md" /> Component Demos</NavLink></SafeLink>
</NavItem>
</Nav>
</Navbar>
</Col>
</Row>
</Container>
)
}

export default Footer
10 changes: 6 additions & 4 deletions src/components/HeaderForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import DataPositionFormRow from "./DataPositionFormRow";
import MountedDataFormRow from "./MountedDataFormRow";
import FileUploadFormRow from "./FileUploadFormRow";
import ExampleSelectButtons from "./ExampleSelectButtons";
import { RegionInput } from "./RegionInput";
import RegionInput from "./RegionInput";
// See src/Types.ts

const DATA_SOURCES = config.DATA_SOURCES;
Expand Down Expand Up @@ -342,9 +342,11 @@ class HeaderForm extends Component {
}
};
getNextViewTarget = () => ({
tracks: new Array(createTrack({name: this.state.graphFile, type: fileTypes.GRAPH}),
createTrack({name: this.state.gbwtFile, type: fileTypes.HAPLOTYPE}),
createTrack({name: this.state.gamFile, type: fileTypes.READ})),
tracks: [
createTrack({name: this.state.graphFile, type: fileTypes.GRAPH}),
createTrack({name: this.state.gbwtFile, type: fileTypes.HAPLOTYPE}),
createTrack({name: this.state.gamFile, type: fileTypes.READ})
],
bedFile: this.state.bedFile,
name: this.state.name,
region: this.state.region,
Expand Down
30 changes: 30 additions & 0 deletions src/components/RegionInput.demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Demo, {props as P} from 'react-demo'
// See https://github.com/rpominov/react-demo for how to make a demo

import RegionInput from "./RegionInput";

// We want to two-way-bind the demo region prop so we use advanced mode and pass the render function.
export default (<Demo
props={{
region: P.string('pathy:1-100'),
regionInfo: P.json({
chr: ['chr600'],
start: [10],
end: [90],
desc: ['Something cool']
}),
pathNames: P.json(["pathy", "anotherPath", "node", "chr600"])
}}
>
{
(props, update) => {
// We need to render the component under test using the props in props, and
// call update when the component wants to adjust the props.
return <RegionInput {...props} handleRegionChange={(newRegion) => {
// When region is changed by the component, update the demo's region state
update({region: newRegion})
}}/>
}
}
</Demo>)

2 changes: 2 additions & 0 deletions src/components/RegionInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@ RegionInput.propTypes = {
start: PropTypes.array,
}),
};

export default RegionInput;
2 changes: 1 addition & 1 deletion src/components/RegionInput.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, fireEvent, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { RegionInput } from "./RegionInput";
import RegionInput from "./RegionInput";
import "@testing-library/jest-dom";
const handleRegionChangeMock = jest.fn();
const MOCK_PATHS = ["pathy", "anotherPath", "node", "chr600"];
Expand Down
25 changes: 25 additions & 0 deletions src/components/SafeLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// SafeLink.js: React Router-compatible Link component that doesn't explode if
/// not used inside a React Router.

import { Link, useInRouterContext } from "react-router-dom"

export const SafeLink = ({children, ...props}) => {
// Find out if we are in a router
let insideRouter = useInRouterContext()
if (insideRouter) {
// We can use Link
return (
<Link {...props}>{children}</Link>
)
} else {
// We can't use Link
// Also try to fix to -> href
let {to, href, ...otherProps} = props
let fixedProps = {href: href || to, ...otherProps}
return (
<a {...fixedProps}>{children}</a>
)
}
}

export default SafeLink
28 changes: 28 additions & 0 deletions src/components/SelectionDropdown.demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Demo, {props as P} from 'react-demo'
// See https://github.com/rpominov/react-demo for how to make a demo

import SelectionDropdown from "./SelectionDropdown";

// We want to two-way-bind the demo region prop so we use advanced mode and pass the render function.
export default (<Demo
props={{
id: P.string('theDropdown'),
inputId: P.string('theTextBox'),
className: P.string('someCSSClass'),
value: P.string('one thing'),
options: P.json(['one thing', 'another thing', 'yet a third thing']),
}}
>
{
(props, update) => {
// We need to render the component under test using the props in props, and
// call update when the component wants to adjust the props.
return <SelectionDropdown {...props} onChange={(fakeEvent) => {
// Bind new value back up to value.
// Remember: we get a fake event object with a "target" that has an "id" and a "value"
update({value: fakeEvent.target.value})
}}/>
}
}
</Demo>)

15 changes: 14 additions & 1 deletion src/components/SelectionDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ import Select from "react-select";

/**
* A searchable selection dropdown component.
* Expects a two-way-binding where "value" is the selected value (out of the
* array in "options"), and calling "onChange" with an event-like object
* updates the value.
*
* The onChange argument is meant to look enough like a DOM change event on a
* "real" <select> to fool most people. It is an object with a "target"
* property, which then has an "id" property with this component's "id" prop,
* and a "value" property with the new value.
*
* So for example:
* <SelectionDropdown id="box1" value="a" options={["a", "b"]} onChange={(e) => {
* // Here e is {"target": {"id": "box1", "value": "b"}}
* }}>
*/
class SelectionDropdown extends Component {
export class SelectionDropdown extends Component {
render() {
// Tweaks to the default react-select styles so that it'll look good with tube maps.
const styles = {
Expand Down
23 changes: 22 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.css";
import App from "./App";
import { DemoLibrary } from "./components/DemoLibrary";

ReactDOM.render(<App />, document.getElementById("root"));

ReactDOM.render((

<BrowserRouter>
<Routes>
<Route path="/">
{
// Main application renders at the root
}
<Route index element={<App />} />
{
// Demos for custom controls show up at /demo
// Each demo gets a nice hashbang URL.
}
<Route path="demo" element={<DemoLibrary />} />
</Route>
</Routes>
</BrowserRouter>

), document.getElementById("root"));

0 comments on commit 94bdd24

Please sign in to comment.