diff --git a/src/content/app/tools/shared/components/tools-top-bar/ToolsTopBar.module.css b/src/content/app/tools/shared/components/tools-top-bar/ToolsTopBar.module.css index 56bee2cac6..988b0e4acb 100644 --- a/src/content/app/tools/shared/components/tools-top-bar/ToolsTopBar.module.css +++ b/src/content/app/tools/shared/components/tools-top-bar/ToolsTopBar.module.css @@ -1,5 +1,7 @@ .topBar { min-height: 74px; + display: flex; + flex-direction: column; background-color: var(--color-light-grey); padding: 24px 30px 20px; box-shadow: 0 3px 5px var(--global-box-shadow-color); diff --git a/src/content/app/tools/vep/VepPage.tsx b/src/content/app/tools/vep/VepPage.tsx new file mode 100644 index 0000000000..c06d707ebe --- /dev/null +++ b/src/content/app/tools/vep/VepPage.tsx @@ -0,0 +1,61 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { lazy, useEffect } from 'react'; + +import { useAppDispatch } from 'src/store'; +import useHasMounted from 'src/shared/hooks/useHasMounted'; + +import { updatePageMeta } from 'src/shared/state/page-meta/pageMetaSlice'; + +import type { ServerFetch } from 'src/routes/routesConfig'; + +const VepPageContent = lazy(() => import('./VepPageContent')); + +// Copied these from the current Ensembl site; will probably need changing in the future. +const pageTitle = 'Ensembl Variant Effect Predictor (VEP)'; +const pageDescription = ` +VEP determines the effect of your variants (SNPs, insertions, deletions, CNVs or structural variants) +on genes, transcripts, and protein sequence, as well as regulatory regions. +`; + +const VepPage = () => { + const hasMounted = useHasMounted(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch( + updatePageMeta({ + title: pageTitle, + description: pageDescription + }) + ); + }, []); + + return hasMounted ? : null; +}; + +export default VepPage; + +// not really fetching anything; just setting page meta +export const serverFetch: ServerFetch = async (params) => { + params.store.dispatch( + updatePageMeta({ + title: pageTitle, + description: pageDescription + }) + ); +}; diff --git a/src/content/app/tools/vep/VepPageContent.tsx b/src/content/app/tools/vep/VepPageContent.tsx new file mode 100644 index 0000000000..7d8ba78f78 --- /dev/null +++ b/src/content/app/tools/vep/VepPageContent.tsx @@ -0,0 +1,57 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Route, Routes } from 'react-router-dom'; + +import VepAppBar from './components/vep-app-bar/VepAppBar'; +import VepTopBar from './components/vep-top-bar/VepTopBar'; +import VepForm from './views/vep-form/VepForm'; +import { NotFoundErrorScreen } from 'src/shared/components/error-screen'; + +const VepPageContent = () => { + return ( +
+ +
+
+ ); +}; + +const Main = () => { + return ( +
+ + + + } /> + List of unviewed submissions
} + /> + + List of viewed submissions} /> + Results of a single VEP analysis} + /> + + } /> + + + ); +}; + +export default VepPageContent; diff --git a/src/content/app/tools/vep/components/form-section/FormSection.module.css b/src/content/app/tools/vep/components/form-section/FormSection.module.css new file mode 100644 index 0000000000..7149be777f --- /dev/null +++ b/src/content/app/tools/vep/components/form-section/FormSection.module.css @@ -0,0 +1,9 @@ +.container { + border-width: 1px; + border-style: solid; + border-color: var(--form-section-border-color, var(--color-medium-light-grey)); +} + +.container + .container { + border-top: none; +} diff --git a/src/content/app/tools/vep/components/form-section/FormSection.tsx b/src/content/app/tools/vep/components/form-section/FormSection.tsx new file mode 100644 index 0000000000..dadcc44166 --- /dev/null +++ b/src/content/app/tools/vep/components/form-section/FormSection.tsx @@ -0,0 +1,56 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from 'classnames'; + +import type { ReactNode } from 'react'; + +import styles from './FormSection.module.css'; + +/** + * This component has first appeared in VEP submission form; + * but it is almost a certainty that it will be used in other places as well. + * + * There already exist two similar components: Accordion and ExpandableSection; + * however, this new components doesn't seem to fit either of those: + * - Both the Accordion and the ExpandableSection components have a predefined + * toggle element (an upwards- or downwards-pointing chevron); and in case of + * the Accordion, the whole area of the closed section acts as a button. + * In contrast, this new component can have anything in the right-hand corner. + * - Both the Accordion and the ExpandableSection components have a clear + * distinction between the "closed" (collapsed) and the "opened" (expanded) + * states. In contrast, this new component can show none of its content, + * a little bit of its content, or its full content (see e.g. behaviour of + * a VEP options section, which may show none of the options, some of the options, + * or all options) + * + * Here is what this component needs to be able to do: + * - It has a border; yet, when it is immediately followed by another FormSection, + * their adjacent borders should visually collapse into one. + */ + +type Props = { + children: ReactNode; + className?: string; +}; + +const FormSection = (props: Props) => { + const componentClasses = classNames(styles.container, props.className); + + return
{props.children}
; +}; + +export default FormSection; diff --git a/src/content/app/tools/vep/components/vep-app-bar/VepAppBar.tsx b/src/content/app/tools/vep/components/vep-app-bar/VepAppBar.tsx new file mode 100644 index 0000000000..e746ca4451 --- /dev/null +++ b/src/content/app/tools/vep/components/vep-app-bar/VepAppBar.tsx @@ -0,0 +1,50 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useAppSelector } from 'src/store'; +import { getEnabledCommittedSpecies } from 'src/content/app/species-selector/state/species-selector-general-slice/speciesSelectorGeneralSelectors'; + +import AppBar, { AppName } from 'src/shared/components/app-bar/AppBar'; +import SpeciesManagerIndicator from 'src/shared/components/species-manager-indicator/SpeciesManagerIndicator'; +import { SelectedSpecies } from 'src/shared/components/selected-species'; +import SpeciesTabsSlider from 'src/shared/components/species-tabs-slider/SpeciesTabsSlider'; +import { AppName as AppNameText } from 'src/global/globalConfig'; + +const VepAppBar = () => { + return ( + } + topRight={} + topLeft={{AppNameText.TOOLS}} + /> + ); +}; + +const SpeciesTabs = () => { + const speciesList = useAppSelector(getEnabledCommittedSpecies); + + const speciesTabs = speciesList.map((species) => ( + + )); + + return {speciesTabs}; +}; + +export default VepAppBar; diff --git a/src/content/app/tools/vep/components/vep-top-bar/VepTopBar.module.css b/src/content/app/tools/vep/components/vep-top-bar/VepTopBar.module.css new file mode 100644 index 0000000000..5def92bfd0 --- /dev/null +++ b/src/content/app/tools/vep/components/vep-top-bar/VepTopBar.module.css @@ -0,0 +1,64 @@ +.grid { + display: grid; + flex-grow: 1; + align-items: center; + grid-template-columns: + [vep-logo] max-content + [form-title] max-content + [transcript-set] auto + [submit-button] min-content + [vep-version] auto + [job-lists-navigation] auto; + column-gap: 32px; + white-space: nowrap; +} + +.logo { + grid-column: vep-logo; + height: 16px; +} + +.runAJob { + grid-column: form-title; + margin-left: 20px; + font-weight: var(--font-weight-bold); +} + +.transcriptSet { + grid-column: transcript-set; + justify-self: end; +} + +.transcriptSetSelector { + margin-left: 20px; + min-width: 300px; +} + +.submit { + grid-column: submit-button; +} + +.vepVersion { + grid-column: vep-version; + justify-self: end; + display: flex; + align-items: baseline; + white-space: pre; + color: var(--color-medium-dark-grey); +} + +.vepVersion svg { + height: 11px; + margin-right: 0.6ch; + fill: var(--color-medium-dark-grey); +} + +.vepVersion span { + color: var(--color-black); +} + +.jobListsNavigation { + grid-column: job-lists-navigation; + display: flex; + column-gap: 12px; +} diff --git a/src/content/app/tools/vep/components/vep-top-bar/VepTopBar.tsx b/src/content/app/tools/vep/components/vep-top-bar/VepTopBar.tsx new file mode 100644 index 0000000000..452de41b01 --- /dev/null +++ b/src/content/app/tools/vep/components/vep-top-bar/VepTopBar.tsx @@ -0,0 +1,60 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ToolsTopBar from 'src/content/app/tools/shared/components/tools-top-bar/ToolsTopBar'; +import { PrimaryButton } from 'src/shared/components/button/Button'; +import Logotype from 'static/img/brand/logotype.svg'; +import SimpleSelect from 'src/shared/components/simple-select/SimpleSelect'; +import ButtonLink from 'src/shared/components/button-link/ButtonLink'; + +import logoUrl from 'static/img/tools/vep/ensembl-vep.svg?url'; + +import styles from './VepTopBar.module.css'; + +const VepTopBar = () => { + return ( + +
+ Ensembl VEP logo +
Run a job
+
+ Transcript set + +
+ Run +
+ + Variant effect predictor + v111 +
+
+ + Unviewed jobs + + + Jobs list + +
+
+
+ ); +}; + +export default VepTopBar; diff --git a/src/content/app/tools/vep/views/vep-form/VepForm.module.css b/src/content/app/tools/vep/views/vep-form/VepForm.module.css new file mode 100644 index 0000000000..366f66ee26 --- /dev/null +++ b/src/content/app/tools/vep/views/vep-form/VepForm.module.css @@ -0,0 +1,73 @@ +.container { + margin-top: 22px; + margin-left: var(--double-standard-gutter); + margin-bottom: var(--global-padding-bottom); + max-width: 1160px; +} + +/* +* THE TOPMOST AREA, ABOVE OTHER FORM SECTIONS +*/ +.topmostAreaGrid { + --padding-side: 42px; + display: grid; + grid-template-columns: [submission-name-field] 1fr [form-reset] max-content; + align-items: end; + padding-left: var(--padding-side); + padding-right: var(--padding-side); + margin-bottom: 20px; +} + +.submissionName { + grid-column: submission-name-field; + justify-self: start; + display: flex; + align-items: center; + column-gap: 12px; + font-weight: var(--font-weight-light); + font-size: 11px; + white-space: nowrap; +} + +.resetForm { + grid-column: form-reset; +} + + +.formSection { + min-height: 62px; +} + +/* +* TOP SECTIONS OF THE FORM +*/ + +/* Grid to use to display the regular content of the section, i.e. not the expanded content */ +.topFormSectionRegularGrid { + --column-gap: 20px; + --padding-side: 42px; + display: grid; + grid-template-columns: + [section-name] calc(152px - var(--column-gap)) + [section-main-content] 1fr + [section-toggle] auto; + column-gap: var(--column-gap); + align-items: center; + height: 62px; +} + +.topFormSectionName { + grid-column: section-name; + font-weight: var(--font-weight-light); + padding-left: var(--padding-side); +} + +.topFormSectionMain { + grid-column: section-main-content; +} + +.topFormSectionToggle { + grid-column: section-toggle; + padding-right: var(--padding-side); + display: flex; +} diff --git a/src/content/app/tools/vep/views/vep-form/VepForm.tsx b/src/content/app/tools/vep/views/vep-form/VepForm.tsx new file mode 100644 index 0000000000..cc24cea26e --- /dev/null +++ b/src/content/app/tools/vep/views/vep-form/VepForm.tsx @@ -0,0 +1,64 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FormSection from 'src/content/app/tools/vep/components/form-section/FormSection'; +import PlusButton from 'src/shared/components/plus-button/PlusButton'; +import TextButton from 'src/shared/components/text-button/TextButton'; +import ShadedInput from 'src/shared/components/input/ShadedInput'; +import { DeleteButtonWithLabel } from 'src/shared/components/delete-button/DeleteButton'; + +import styles from './VepForm.module.css'; + +const VepForm = () => { + return ( +
+
+
+ + +
+
+ +
+
+ + +
+
Species
+
+ Select a species / assembly +
+
+ +
+
+
+ +
+
Variants
+
+ Add variants +
+
+ +
+
+
+
+ ); +}; + +export default VepForm; diff --git a/src/header/launchbar/Launchbar.tsx b/src/header/launchbar/Launchbar.tsx index f3f4fe09e0..8071d40e75 100644 --- a/src/header/launchbar/Launchbar.tsx +++ b/src/header/launchbar/Launchbar.tsx @@ -27,6 +27,7 @@ import LaunchbarButton from './LaunchbarButton'; import SpeciesSelectorLaunchbarButton from './SpeciesSelectorLaunchbarButton'; import EntityViewerLaunchbarButton from './EntityViewerLaunchbarButton'; import BlastLaunchbarButton from './BlastLaunchbarButton'; +import VepLaunchbarButton from './VepLaunchbarButton'; import Logotype from 'static/img/brand/logotype.svg'; @@ -63,6 +64,7 @@ const Launchbar = () => {
+
{ + if (isProductionEnvironment()) { + return null; + } + + // TODO: add the code to enable notifications after submissions have been enabled + return ( + + ); +}; + +export default VepLaunchbarButton; diff --git a/src/routes/routesConfig.tsx b/src/routes/routesConfig.tsx index 843e0d175b..ba1c3ed3aa 100644 --- a/src/routes/routesConfig.tsx +++ b/src/routes/routesConfig.tsx @@ -34,6 +34,9 @@ import EntityViewerPage, { import BlastPage, { serverFetch as blastServerFetch } from 'src/content/app/tools/blast/BlastPage'; +import VepPage, { + serverFetch as vepServerFetch +} from 'src/content/app/tools/vep/VepPage'; import AboutPage, { serverFetch as aboutPageServerFetch } from 'src/content/app/about/AboutPage'; @@ -85,6 +88,11 @@ const routes: RouteConfig[] = [ element: , serverFetch: blastServerFetch }, + { + path: '/vep/*', + element: , + serverFetch: vepServerFetch + }, { path: '/about/*', element: , diff --git a/src/shared/components/app-icon/AppIcon.module.css b/src/shared/components/app-icon/AppIcon.module.css index c56d63f75d..888fb62700 100644 --- a/src/shared/components/app-icon/AppIcon.module.css +++ b/src/shared/components/app-icon/AppIcon.module.css @@ -49,6 +49,13 @@ get used when AppIcon is placed inside of the ImageButton component */ height: 75%; } +.vep svg { + --image-button-svg-width: 95%; + --image-button-svg-height: 95%; + width: 95%; + height: 95%; +} + .help svg { --image-button-svg-width: 70%; --image-button-svg-height: 70%; diff --git a/src/shared/components/app-icon/VepIcon.tsx b/src/shared/components/app-icon/VepIcon.tsx new file mode 100644 index 0000000000..3a0d14c3cb --- /dev/null +++ b/src/shared/components/app-icon/VepIcon.tsx @@ -0,0 +1,34 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { memo } from 'react'; +import classNames from 'classnames'; + +import SVGIcon from 'static/icons/icon_launchbar_vep.svg'; + +import styles from './AppIcon.module.css'; + +const BlastIcon = () => { + const elementClasses = classNames(styles.appIcon, styles.vep); + + return ( +
+ +
+ ); +}; + +export default memo(BlastIcon); diff --git a/src/shared/components/app-icon/index.ts b/src/shared/components/app-icon/index.ts index 38da3367f9..6b6e2aed15 100644 --- a/src/shared/components/app-icon/index.ts +++ b/src/shared/components/app-icon/index.ts @@ -20,3 +20,4 @@ export { default as GlobalSearchIcon } from './GlobalSearchIcon'; export { default as EntityViewerIcon } from './EntityViewerIcon'; export { default as BlastIcon } from './BlastIcon'; export { default as HelpIcon } from './HelpIcon'; +export { default as VepIcon } from './VepIcon'; diff --git a/src/shared/components/simple-select/SimpleSelect.module.css b/src/shared/components/simple-select/SimpleSelect.module.css index 7ff7d1b022..f39012969d 100644 --- a/src/shared/components/simple-select/SimpleSelect.module.css +++ b/src/shared/components/simple-select/SimpleSelect.module.css @@ -43,3 +43,11 @@ 50% 100% ); /* idea from https://moderncss.dev/custom-select-styles-with-pure-css */ } + +.disabled select { + border: var(--simple-select-disabled-border, 1px solid var(--color-grey)); +} + +.disabled::after { + background-color: var(--simple-select-disabled-arrowhead-color, var(--color-grey)); +} diff --git a/src/shared/components/simple-select/SimpleSelect.tsx b/src/shared/components/simple-select/SimpleSelect.tsx index 3193c82893..9163dfbfca 100644 --- a/src/shared/components/simple-select/SimpleSelect.tsx +++ b/src/shared/components/simple-select/SimpleSelect.tsx @@ -63,8 +63,7 @@ type OptionsSelectProps = CommonProps & OptionsSpecificProps; type OptionGroupsSelectProps = CommonProps & OptionGroupsSpecificProps; export type SimpleSelectProps = HTMLSelectProps & - (OptionsSelectProps | OptionGroupsSelectProps) & - CommonProps; + (OptionsSelectProps | OptionGroupsSelectProps); export type SimpleSelectMethods = { clear: () => void; @@ -74,7 +73,12 @@ const SimpleSelect = ( props: SimpleSelectProps, ref: ForwardedRef<{ clear: () => void }> ) => { - const { className, placeholder, ...otherProps } = props; + const { + className: classNameFromProps, + placeholder, + disabled, + ...otherProps + } = props; const selectRef = useRef(null); useImperativeHandle(ref, () => ({ @@ -92,7 +96,11 @@ const SimpleSelect = ( } })); - const selectClassnames = classNames(styles.select, className); + const selectClassnames = classNames( + styles.select, + { [styles.disabled]: disabled }, + classNameFromProps + ); const selectProps = pickBy( otherProps, (_, key) => !['options', 'optionGroups'].includes(key) @@ -125,6 +133,7 @@ const SimpleSelect = (