diff --git a/src/api_service/websocket_functions.py b/src/api_service/websocket_functions.py index fec39e9f..237e34b9 100644 --- a/src/api_service/websocket_functions.py +++ b/src/api_service/websocket_functions.py @@ -106,3 +106,25 @@ def send_update_cluster_label_set_command(user_id: int): 'command': 'update_cluster_labels_sets' } send_message(user_group_name, message) + +def send_update_institutions_command(user_id: int): + """ + Sends a message indicating that a Institution state update has occurred + @param user_id: Institution's user's id to send the WS message + """ + user_group_name = f'notifications_{user_id}' + message = { + 'command': 'update_institutions' + } + send_message(user_group_name, message) + +def send_update_user_for_institution_command(user_id: int): + """ + Sends a message indicating that a Institution_user state update has occurred + @param user_id: Institution's user's id to send the WS message + """ + user_group_name = f'notifications_{user_id}' + message = { + 'command': 'update_user_for_institution' + } + send_message(user_group_name, message) \ No newline at end of file diff --git a/src/frontend/static/frontend/src/components/MainNavbar.tsx b/src/frontend/static/frontend/src/components/MainNavbar.tsx index 1d97cc26..f4e4e5ad 100644 --- a/src/frontend/static/frontend/src/components/MainNavbar.tsx +++ b/src/frontend/static/frontend/src/components/MainNavbar.tsx @@ -99,6 +99,9 @@ const MainNavbar = (props: MainNavbarProps) => { + + + } {/* Analysis menu */} @@ -173,21 +176,21 @@ const MainNavbar = (props: MainNavbarProps) => { /> } - - {/* Institutions panel (only for user who are admin of at least one institution) */} - {currentUser.is_institution_admin && - - } } + {/* Institutions */} + {currentUser && + + + Institutions + + + + } + {/* About us */} diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index a3b58c97..8b0cd8cf 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -4,8 +4,8 @@ import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form } import { DjangoCGDSStudy, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource } from '../../utils/util_functions' -import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse } from '../../utils/interfaces' -import { Biomarker, BiomarkerType, BiomarkerOrigin, ConfirmModal, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from './types' +import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal } from '../../utils/interfaces' +import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from './types' import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm' import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable' import { TableCellWithTitle } from '../common/TableCellWithTitle' @@ -1704,7 +1704,7 @@ export class BiomarkersPanel extends React.Component<{}, BiomarkersPanelState> { {/* Stop button */} {isInProcess && this.setState({ biomarkerToStop: biomarker })} /> } @@ -1712,7 +1712,7 @@ export class BiomarkersPanel extends React.Component<{}, BiomarkersPanelState> { {/* Delete button */} {!isInProcess && this.confirmBiomarkerDeletion(biomarker)} /> diff --git a/src/frontend/static/frontend/src/components/biomarkers/types.ts b/src/frontend/static/frontend/src/components/biomarkers/types.ts index 9411a955..20e63fbc 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/types.ts +++ b/src/frontend/static/frontend/src/components/biomarkers/types.ts @@ -99,13 +99,6 @@ interface Biomarker extends BiomarkerSimple { mrnas: SaveMoleculeStructure[] } -interface ConfirmModal { - confirmModal: boolean, - headerText: string, - contentText: string, - onConfirm: Function, -} - /** Represents a molecule info to show in molecules Dropdown. */ type MoleculeSymbol = { key: string, @@ -630,7 +623,6 @@ export { MoleculesMultipleSelection, MoleculesSectionData, MoleculeSectionItem, - ConfirmModal, MoleculeSymbol, MoleculesSymbolFinder, ClusteringScoringMethod, diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx new file mode 100644 index 00000000..ef82c92c --- /dev/null +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx @@ -0,0 +1,176 @@ +import ky from 'ky' +import React, { useEffect, useState } from 'react' +import { Form, Button } from 'semantic-ui-react' +import { getDjangoHeader } from '../../utils/util_functions' +import { CustomAlertTypes, Nullable } from '../../utils/interfaces' +import { DjangoInstitution } from '../../utils/django_interfaces' + +const defaultForm: { + id: undefined | number; + name: string; + location: string; + email: string; + telephone_number: string; + isLoading: boolean; +} = { + id: undefined, + name: '', + location: '', + email: '', + telephone_number: '', + isLoading: false +} + +declare const urlCreateInstitution: string +declare const urlEditInstitution: string + +interface Props { + institutionToEdit: Nullable, + handleResetInstitutionToEdit: (callbackToCancel: () => void) => void, + handleUpdateAlert(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable, isEdit?: boolean): void, +} + +const InstitutionForm = (props: Props) => { + const [formData, setFormData] = useState(defaultForm) + + /** + * Handle form state data + * @param e event + * @param param.name key name to edit field in form + * @param param.value key value to edit field in form + */ + + const handleChange = (e, { name, value }) => { + setFormData({ ...formData, [name]: value }) + } + + /** + * Handle user form to edit or create + */ + const handleSubmit = () => { + const myHeaders = getDjangoHeader() + + if (props.institutionToEdit?.id) { + const jsonParams = { + id: formData.id, + name: formData.name, + location: formData.location, + email: formData.email, + telephone_number: formData.telephone_number + } + const editUrl = `${urlEditInstitution}/${formData.id}/` + + ky.patch(editUrl, { headers: myHeaders, json: jsonParams }).then((response) => { + setFormData(prevState => ({ ...prevState, isLoading: true })) + response.json().then((jsonResponse: DjangoInstitution) => { + props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, `Institution ${jsonResponse.name} Updated!`, () => setFormData(defaultForm), true) + }).catch((err) => { + setFormData(prevState => ({ ...prevState, isLoading: false })) + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + console.error('Error adding new Institution ->', err) + }) + } else { + const jsonParams = { + name: formData.name, + location: formData.location, + email: formData.email, + telephone_number: formData.telephone_number + } + ky.post(urlCreateInstitution, { headers: myHeaders, json: jsonParams }).then((response) => { + setFormData(prevState => ({ ...prevState, isLoading: true })) + response.json().then((jsonResponse: DjangoInstitution) => { + props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, `Institution ${jsonResponse.name} created!`, () => setFormData(defaultForm)) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + setFormData(prevState => ({ ...prevState, isLoading: false })) + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false }))) + console.error('Error adding new Institution ->', err) + }) + } + } + + /** + * Handle if user Reset or cancel edit + */ + const handleCancelForm = () => { + if (props.institutionToEdit) { + props.handleResetInstitutionToEdit(() => setFormData(defaultForm)) + } else { + setFormData(defaultForm) + } + } + + /** + * use effect to handle if a institution for edit is sent + */ + useEffect(() => { + if (props.institutionToEdit) { + setFormData({ + ...props.institutionToEdit, + id: props.institutionToEdit?.id, + isLoading: false + }) + } + }, [props.institutionToEdit]) + return ( + <> +
+ + + + + + + + + ) +} + +export default InstitutionForm diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx new file mode 100644 index 00000000..8ae7c689 --- /dev/null +++ b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx @@ -0,0 +1,256 @@ +import React, { useContext, useEffect, useRef, useState } from 'react' +import { + ModalHeader, + ModalContent, + Modal, + Icon, + Segment, + Table, + Button, + Select +} from 'semantic-ui-react' +import { DjangoInstitutionUser } from '../../utils/django_interfaces' +import { CustomAlertTypes, Nullable, SemanticListItem } from '../../utils/interfaces' +import { PaginatedTable } from '../common/PaginatedTable' +import { TableCellWithTitle } from '../common/TableCellWithTitle' +import { InstitutionTableData } from './InstitutionsPanel' +import ky from 'ky' +import { getDjangoHeader } from '../../utils/util_functions' +import { CurrentUserContext } from '../Base' + +declare const urlGetUsersCandidates: string +declare const urlEditInstitutionAdmin: string +declare const urlNonUserListInstitution: string +declare const urlAddRemoveUserToInstitution: string + +export interface InstitutionModalState { + isOpen: boolean, + institution: Nullable +} + +interface Props extends InstitutionModalState { + /* Close modal function */ + handleCloseModal: () => void, + handleChangeConfirmModalState: (setOption: boolean, headerText: string, contentText: string, onConfirm: Function) => void, + handleUpdateAlert(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable): void, +} + +/** + * Institution modal + * @param {Props} props Props for component + * @returns Component + */ +export const InstitutionModal = (props: Props) => { + const [userIdToAdd, setUserIdToAdd] = useState(0) + const [userList, setUserList] = useState([]) + const abortController = useRef(new AbortController()) + const currentUser = useContext(CurrentUserContext) + + /** + * Function to add user to institution. + */ + const handleAddUser = () => { + if (userIdToAdd) { + const myHeaders = getDjangoHeader() + const body = { + userId: userIdToAdd, + isAdding: true, + institutionId: props.institution?.id + } + ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, signal: abortController.current.signal, json: body }).then((response) => { + response.json().then(() => { + usersListNonInInstitution() + }).catch((err) => { + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.error('Error getting users ->', err) + }) + } + } + + /** + * Function to remove user from institution. + * @param idUserCandidate user id to remove from institution. + */ + const handleRemoveUser = (idUserCandidate: number) => { + const myHeaders = getDjangoHeader() + const body = { + userId: idUserCandidate, + isAdding: false, + institutionId: props.institution?.id + } + ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, signal: abortController.current.signal, json: body }).then((response) => { + response.json().then(() => { + usersListNonInInstitution() + }).catch((err) => { + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.error('Error getting users ->', err) + }) + } + + /** + * Function to manage admin. + * @param adminSwitched switch if true is going to be admin, false if going to be normal user + * @param idInstitution institution id. + */ + const handleSwitchUserAdmin = (adminSwitched: boolean, idInstitution: number) => { + const myHeaders = getDjangoHeader() + const editUrl = `${urlEditInstitutionAdmin}/${idInstitution}/` + + ky.patch(editUrl, { headers: myHeaders }).then((response) => { + response.json().then(() => { + props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, adminSwitched ? 'User is admin!' : 'User is not admin!', null) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error for switch role!', null) + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error for switch role!', null) + console.error('Error adding new Institution ->', err) + }) + } + + /** + * Function to search users that are not in institution. + */ + const usersListNonInInstitution = () => { + const myHeaders = getDjangoHeader() + + const editUrl = `${urlNonUserListInstitution}/${props.institution?.id}/` + + ky.get(editUrl, { headers: myHeaders, signal: abortController.current.signal }).then((response) => { + response.json().then((jsonResponse: { id: number, username: string }[]) => { + setUserList(jsonResponse.map(user => ({ key: user.id.toString(), value: user.id.toString(), text: user.username }))) + }).catch((err) => { + console.error('Error parsing JSON ->', err) + }) + }).catch((err) => { + console.error('Error getting users ->', err) + }) + } + + useEffect(() => { + if (props.institution?.is_user_admin && props.institution?.id) { + usersListNonInInstitution() + } + + return () => { + abortController.current.abort() + } + }, [props.institution?.id]) + + return ( + } + > + {props.institution?.name} + + +