Skip to content

Commit

Permalink
Vep form submission (#1150)
Browse files Browse the repository at this point in the history
- Added initial implementation for VEP options
  - Fetching a config with VEP options
    - Currently, the config is mocked
  - Displaying checkboxes for options to include gene symbols
    and transcript biotypes in the analysis
  - Showing the transcript set selector with data from the config
- Enabled visibility rules around the form, and rules for form completeness
  - The options section only shows up
    after user's input for variants has been committed
  - If user's variants input has been committed,
    it shows up in the collapsed view of the variants section
  - The "Run" button that submits the form gets enabled
    only in presence of a selected species, user's input, and form parameters
- Enabled VEP form submission
  - According to the agreed-upon api spec, user's plain-text input is transformed into a file at submission
  • Loading branch information
azangru authored Jul 4, 2024
1 parent 7411777 commit 171aa56
Show file tree
Hide file tree
Showing 21 changed files with 821 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
.container {
--form-section-min-height: 62px; /* declaring it as a custom property so that children of FormSection could use it */
border-width: 1px;
border-style: solid;
border-color: var(--form-section-border-color, var(--color-medium-light-grey));
min-height: var(--form-section-min-height);
}

.container + .container {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* 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 {
getSelectedSpecies,
getVepFormParameters,
getVepFormInputText,
getVepFormInputFile,
getVepFormInputCommittedFlag
} from 'src/content/app/tools/vep/state/vep-form/vepFormSelectors';

import { useVepFormSubmissionMutation } from 'src/content/app/tools/vep/state/vep-api/vepApiSlice';

import { PrimaryButton } from 'src/shared/components/button/Button';

import type {
VEPSubmissionPayload,
VepSelectedSpecies
} from 'src/content/app/tools/vep/types/vepSubmission';

const VepSubmitButton = () => {
const selectedSpecies = useAppSelector(getSelectedSpecies);
const inputText = useAppSelector(getVepFormInputText);
const inputFile = useAppSelector(getVepFormInputFile);
const formParameters = useAppSelector(getVepFormParameters);
const isInputCommitted = useAppSelector(getVepFormInputCommittedFlag);
const [submitVepForm] = useVepFormSubmissionMutation();

const canSubmit = Boolean(
selectedSpecies &&
(inputText || inputFile) &&
Object.keys(formParameters).length &&
isInputCommitted
);

const onSubmit = () => {
const payload = preparePayload({
species: selectedSpecies as VepSelectedSpecies,
inputText,
inputFile,
parameters: formParameters
});

submitVepForm(payload);
};

return (
<PrimaryButton disabled={!canSubmit} onClick={onSubmit}>
Run
</PrimaryButton>
);
};

const preparePayload = ({
species,
inputText,
inputFile,
parameters
}: {
species: VepSelectedSpecies;
inputText: string;
inputFile: File | null;
parameters: Record<string, unknown>;
}): VEPSubmissionPayload => {
if (inputText) {
inputFile = new File([inputText], 'input.txt', {
type: 'text/plain'
});
}

return {
genome_id: species.genome_id,
input_file: inputFile as File,
parameters: JSON.stringify(parameters)
};
};

export default VepSubmitButton;
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.grid {
display: grid;
flex-grow: 1;
align-items: center;
grid-template-columns:
[vep-logo] max-content
Expand Down
56 changes: 45 additions & 11 deletions src/content/app/tools/vep/components/vep-top-bar/VepTopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,22 @@
* limitations under the License.
*/

import { useAppSelector } from 'src/store';

import {
getSelectedSpecies,
getVepFormParameters
} from 'src/content/app/tools/vep/state/vep-form/vepFormSelectors';

import { useVepFormConfigQuery } from 'src/content/app/tools/vep/state/vep-api/vepApiSlice';

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 SimpleSelect, {
type Option
} from 'src/shared/components/simple-select/SimpleSelect';
import ButtonLink from 'src/shared/components/button-link/ButtonLink';
import VepSubmitButton from '../vep-submit-button/VepSubmitButton';

import logoUrl from 'static/img/tools/vep/ensembl-vep.svg?url';

Expand All @@ -30,15 +41,8 @@ const VepTopBar = () => {
<div className={styles.grid}>
<img src={logoUrl} alt="Ensembl VEP logo" className={styles.logo} />
<div className={styles.runAJob}>Run a job</div>
<div className={styles.transcriptSet}>
Transcript set
<SimpleSelect
options={[{ label: 'Select', value: 'none' }]}
disabled={true}
className={styles.transcriptSetSelector}
/>
</div>
<PrimaryButton disabled={true}>Run</PrimaryButton>
<TranscriptSetSelector />
<VepSubmitButton />
<div className={styles.vepVersion}>
<Logotype />
<span>Variant effect predictor </span>
Expand All @@ -57,4 +61,34 @@ const VepTopBar = () => {
);
};

const TranscriptSetSelector = () => {
const selectedSpecies = useAppSelector(getSelectedSpecies); // TODO: use genome id of the species to fetch the form config
const vepFormParameters = useAppSelector(getVepFormParameters);
const { currentData: vepFormConfig } = useVepFormConfigQuery();

const canPopulateSelect = selectedSpecies && vepFormConfig;

let options: Option[] = [];

if (!canPopulateSelect) {
options = [{ label: 'Select', value: 'none' }];
} else {
options = vepFormConfig.parameters.transcript_set.options;
}

const selectedValue = (vepFormParameters.transcript_set as string) ?? 'none';

return (
<div className={styles.transcriptSet}>
Transcript set
<SimpleSelect
options={options}
disabled={!canPopulateSelect}
className={styles.transcriptSetSelector}
value={selectedValue}
/>
</div>
);
};

export default VepTopBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 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 type { VepFormConfig } from 'src/content/app/tools/vep/types/vepFormConfig';

const mockVepFormConfig = {
parameters: {
transcript_set: {
label: 'Transcript set',
description: null,
type: 'select',
options: [
{
label: 'GENCODE',
value: 'gencode_comprehensive'
}
],
default_value: 'gencode_comprehensive'
},
symbol: {
label: 'Gene symbol',
description: null,
type: 'boolean',
default_value: true
},
biotype: {
label: 'Transcript biotype',
description: null,
type: 'boolean',
default_value: true
}
}
} satisfies VepFormConfig;

export default mockVepFormConfig;
66 changes: 65 additions & 1 deletion src/content/app/tools/vep/state/vep-api/vepApiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,56 @@
* limitations under the License.
*/

import config from 'config';

import restApiSlice from 'src/shared/state/api-slices/restSlice';
import { setDefaultParameters } from '../vep-form/vepFormSlice';

import { getVepFormParameters } from '../vep-form/vepFormSelectors';

import type { VepResultsResponse } from 'src/content/app/tools/vep/types/vepResultsResponse';
import type { VepFormConfig } from 'src/content/app/tools/vep/types/vepFormConfig';
import type { VEPSubmissionPayload } from 'src/content/app/tools/vep/types/vepSubmission';
import type { RootState } from 'src/store';

const vepApiSlice = restApiSlice.injectEndpoints({
endpoints: (builder) => ({
vepFormConfig: builder.query<VepFormConfig, void>({
queryFn: async (_, { dispatch, getState }) => {
// TODO: the query function will accept a genome id,
// and will send request to:
// `${config.toolsApiBaseUrl}/vep/config?genome_id=${genomeId}`
// to fetch data.
// Meanwhile, until the back-end endpoint is developed,
// this function returns hard-coded response payload.
const mockResponseModule = await import(
'src/content/app/tools/vep/state/vep-api/fixtures/mockVepFormConfig'
);
const vepFormConfig = mockResponseModule.default;

const vepFormParametersInState = getVepFormParameters(
getState() as RootState
);

if (!Object.keys(vepFormParametersInState).length) {
dispatch(setDefaultParameters(vepFormConfig));
}

return {
data: vepFormConfig
};
}
}),
vepFormSubmission: builder.mutation<
{ submission_id: string },
VEPSubmissionPayload
>({
query: (payload) => ({
url: `${config.toolsApiBaseUrl}/vep/submissions`,
method: 'POST',
body: prepareSubmissionFormData(payload)
})
}),
vepResults: builder.query<VepResultsResponse, void>({
queryFn: async () => {
// TODO: the query function will accept a submission id,
Expand All @@ -40,4 +84,24 @@ const vepApiSlice = restApiSlice.injectEndpoints({
})
});

export const { useVepResultsQuery } = vepApiSlice;
/**
* This function transforms the JSON payload passed into vepFormSubmission function
* into a FormData object necessary to submit a multipart/form-data request.
* While vepFormSubmission could have received a FormData object as its argument in the first place,
* the presence of this function allows us to type-check the payload.
*/
const prepareSubmissionFormData = (payload: VEPSubmissionPayload) => {
const formData = new FormData();

for (const [key, value] of Object.entries(payload)) {
formData.append(key, value);
}

return formData;
};

export const {
useVepFormConfigQuery,
useVepResultsQuery,
useVepFormSubmissionMutation
} = vepApiSlice;
15 changes: 15 additions & 0 deletions src/content/app/tools/vep/state/vep-form/vepFormSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ import type { RootState } from 'src/store';

export const getSelectedSpecies = (state: RootState) =>
state.vep.vepForm.selectedSpecies;

export const getVepSubmissionName = (state: RootState) =>
state.vep.vepForm.submissionName;

export const getVepFormParameters = (state: RootState) =>
state.vep.vepForm.parameters;

export const getVepFormInputText = (state: RootState) =>
state.vep.vepForm.inputText;

export const getVepFormInputFile = (state: RootState) =>
state.vep.vepForm.inputFile;

export const getVepFormInputCommittedFlag = (state: RootState) =>
state.vep.vepForm.isInputCommitted;
Loading

0 comments on commit 171aa56

Please sign in to comment.