From acce2371987c01ab0f5132d137f384933c108b93 Mon Sep 17 00:00:00 2001 From: Laetitia Fesselier Date: Fri, 18 Jun 2021 18:31:24 -0400 Subject: [PATCH 01/21] Phase 2 - additional features et layout changes --- .eslintrc.json | 10 +- package.json | 2 + public/preload.js | 8 + python/eeg2bids.py | 67 ++- python/libs/BIDS.py | 9 + python/libs/{TSV.py => Modifier.py} | 178 ++++-- python/libs/iEEG.py | 64 +- python/libs/loris_api.py | 160 +++++ requirements.txt | 8 +- src/css/Configuration.css | 20 +- src/css/Converter.css | 19 +- src/css/Menu.css | 7 +- src/css/Modal.css | 4 +- src/css/Validator.css | 8 +- src/css/Welcome.css | 1 - src/css/index.css | 48 +- src/jsx/Configuration.js | 870 +++++++++++++++++++++++----- src/jsx/Converter.js | 552 +++++++++--------- src/jsx/SplashScreen.js | 4 +- src/jsx/Validator.js | 40 +- src/jsx/Welcome.js | 52 +- src/jsx/elements/inputs.js | 248 +++++++- src/jsx/elements/menu.js | 8 +- src/jsx/elements/modal.js | 14 +- wiki/macOS/README.md | 4 +- 25 files changed, 1775 insertions(+), 630 deletions(-) rename python/libs/{TSV.py => Modifier.py} (50%) create mode 100644 python/libs/loris_api.py diff --git a/.eslintrc.json b/.eslintrc.json index ab39341..0d1b34b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,7 +20,11 @@ "globalReturn": false } }, - "extends": ["eslint:recommended", "plugin:react/recommended", "google"], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "google" + ], "rules": { "max-len": ["error", { "code": 80, @@ -35,7 +39,9 @@ "require-jsdoc": "error", "no-console": ["warn", { "allow": ["info", "warn", "error"] - }] + }], + "react/jsx-curly-brace-presence": "warn", + "react/jsx-key": "warn" }, "globals": { "React": true, diff --git a/package.json b/package.json index 28f0439..d00bc5f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.24.0", "lint-staged": "^11.0.0", + "react-datepicker": "^4.1.1", + "react-switch": "^6.0.0", "wait-on": "^5.3.0" }, "scripts": { diff --git a/public/preload.js b/public/preload.js index a5cfc46..7b717d8 100644 --- a/public/preload.js +++ b/public/preload.js @@ -5,10 +5,18 @@ contextBridge.exposeInMainWorld('myAPI', { const {dialog} = require('@electron/remote'); return dialog; }, + visitBIDS: () => { + const {shell} = require('electron'); + shell.openExternal('https://bids.neuroimaging.io'); + }, visitGitHub: () => { const {shell} = require('electron'); shell.openExternal('https://github.com/aces/eeg2bids'); }, + visitIssues: () => { + const {shell} = require('electron'); + shell.openExternal('https://github.com/aces/eeg2bids/issues'); + }, visitMNE: () => { const {shell} = require('electron'); shell.openExternal('https://mne.tools/mne-bids/'); diff --git a/python/eeg2bids.py b/python/eeg2bids.py index 3842954..9852b9f 100644 --- a/python/eeg2bids.py +++ b/python/eeg2bids.py @@ -3,12 +3,15 @@ from eventlet import tpool import socketio from python.libs import iEEG +from python.libs.Modifier import Modifier from python.libs import BIDS - +from python.libs.loris_api import LorisAPI +import csv # Create socket listener. sio = socketio.Server(async_mode='eventlet', cors_allowed_origins=[]) app = socketio.WSGIApp(sio) +loris_api = LorisAPI() # EEG2BIDS Wizard version appVersion = '1.0.0' @@ -44,6 +47,22 @@ def tarfile_bids(sid, data): sio.emit('response', send) +@sio.event +def get_loris_sites(sid): + sio.emit('loris_sites', loris_api.get_sites()) + +@sio.event +def get_loris_projects(sid): + sio.emit('loris_projects', loris_api.get_projects()) + +@sio.event +def get_loris_subprojects(sid, project): + sio.emit('loris_subprojects', loris_api.get_subprojects(project)) + +@sio.event +def get_loris_visits(sid, project): + sio.emit('loris_visits', loris_api.get_visits(project)) + @sio.event def ieeg_get_header(sid, data): # data = { file_path: 'path to iEEG file' } @@ -59,24 +78,42 @@ def ieeg_get_header(sid, data): response = { 'error': 'Failed to retrieve EDF header information', } - sio.emit('response', response) + sio.emit('edf_header', response) +@sio.event +def get_metadata(sid, data): + # data = { file_path: 'path to metadata file' } + print('metadata file:', data) + + if not data['file_path']: + print('No file path found.') + response = { + 'error': 'No file path found.', + } + else : + try: + with open(data['file_path']) as fd: + reader = csv.DictReader(fd, delimiter="\t", quotechar='"') + response = { + 'metadata': {rows['Field']:rows['Value'] for rows in reader} + } + except IOError: + print("Could not read the metadata file.") + response = { + 'error': 'No file path found.', + } + + sio.emit('metadata', response) def edf_to_bids_thread(data): print('data is ') print(data) error_messages = [] - if not data['file_path']: - error_messages.append('The file.edf to convert is missing.') + if not data['file_paths']: + error_messages.append('No .edf file(s) to convert.') if not data['bids_directory']: error_messages.append('The BIDS output directory is missing.') - if not data['site_id']: - error_messages.append('The LORIS SiteID is missing.') - if not data['project_id']: - error_messages.append('The LORIS ProjectID is missing.') - if not data['sub_project_id']: - error_messages.append('The LORIS SubProjectID is missing.') - if not data['visit_label']: + if not data['session']: error_messages.append('The LORIS Visit Label is missing.') if not error_messages: @@ -84,10 +121,10 @@ def edf_to_bids_thread(data): data['output_time'] = 'output-' + time.latest_output iEEG.Converter(data) # EDF to BIDS format. - # store subject_id for iEEG.Modifier + # store subject_id for Modifier data['subject_id'] = iEEG.Converter.m_info['subject_id'] data['appVersion'] = appVersion - iEEG.Modifier(data, sio) # Modifies data of BIDS format + Modifier(data) # Modifies data of BIDS format response = { 'output_time': data['output_time'] } @@ -100,9 +137,9 @@ def edf_to_bids_thread(data): @sio.event def edf_to_bids(sid, data): - # data = { file_path: '', bids_directory: '', read_only: false, + # data = { file_paths: [], bids_directory: '', read_only: false, # events_tsv: '', line_freq: '', site_id: '', project_id: '', - # sub_project_id: '', visit_label: '', subject_id: ''} + # sub_project_id: '', session: '', subject_id: ''} print('edf_to_bids: ', data) response = eventlet.tpool.execute(edf_to_bids_thread, data) print(response) diff --git a/python/libs/BIDS.py b/python/libs/BIDS.py index be8f73b..b6f6f0b 100644 --- a/python/libs/BIDS.py +++ b/python/libs/BIDS.py @@ -15,6 +15,15 @@ def __init__(self, data): validator = BIDSValidator() for path, dirs, files in os.walk(start_path): for filename in files: + if filename == '.bidsignore': + continue + + if filename.endswith('_annotations.tsv'): + continue + + if filename.endswith('_annotations.json'): + continue + temp = os.path.join(path, filename) file_paths.append(temp[len(start_path):len(temp)]) result.append(validator.is_bids(temp[len(start_path):len(temp)])) diff --git a/python/libs/TSV.py b/python/libs/Modifier.py similarity index 50% rename from python/libs/TSV.py rename to python/libs/Modifier.py index b46519a..267faf6 100644 --- a/python/libs/TSV.py +++ b/python/libs/Modifier.py @@ -2,14 +2,60 @@ import csv import json import re +import shutil -# Writer - writes to tsv file -class Writer: - def __init__(self, data, sio): - print('- Writer: init started.') +class Modifier: + def __init__(self, data): + self.data = data + print(self.data) + + print('- Modifier: init started.') + + self.modify_dataset_description_json() + self.modify_participants_tsv() + self.modify_participants_json() + self.copy_events_tsv() + self.copy_annotations_files() + self.modify_eeg_json() + + + def get_bids_root_path(self): + return os.path.join( + self.data['bids_directory'], + self.data['output_time'] + ) + + def get_eeg_path(self): + directory_path = 'sub-' + self.data['participantID'].replace('_', '').replace('-', '').replace(' ', '') + + return os.path.join( + self.get_bids_root_path(), + directory_path, + 'ses-' + self.data['session'], + self.data['modality'] + ) + + def modify_dataset_description_json(self): + file_path = os.path.join( + self.get_bids_root_path(), + 'dataset_description.json' + ) + + try: + with open(file_path, "r") as fp: + file_data = json.load(fp) + file_data['PreparedBy'] = self.data['preparedBy'] + + with open(file_path, "w") as fp: + json.dump(file_data, fp, indent=4) + + except IOError: + print("Could not read or write dataset_description.json file") + + + def modify_participants_tsv(self): file_path = os.path.join( - data['bids_directory'], - data['output_time'], + self.get_bids_root_path(), 'participants.tsv' ) @@ -27,12 +73,12 @@ def __init__(self, data, sio): output.append( [ participant_id, - age, - sex, - hand, - data['site_id'], - data['project_id'], - data['sub_project_id'] + self.data['age'], + self.data['sex'], + self.data['hand'], + self.data['site_id'], + self.data['sub_project_id'], + self.data['project_id'] ] ) except ValueError: @@ -41,45 +87,45 @@ def __init__(self, data, sio): output.append( [ participant_id, - age, - sex, - hand, - data['site_id'], - data['project_id'], - data['sub_project_id'] + self.data['age'], + self.data['sex'], + self.data['hand'], + self.data['site_id'], + self.data['sub_project_id'], + self.data['project_id'] ] ) except ValueError: print('error: ValueError') with open(file_path, mode='w', newline='') as tsv_file: - headers = ['participant_id', 'age', 'sex', 'hand', 'site', 'project', 'subproject'] + headers = ['participant_id', 'age', 'sex', 'hand', 'site', 'subproject', 'project'] writer = csv.writer(tsv_file, delimiter='\t') writer.writerow(headers) writer.writerows(output) tsv_file.close() - # modify the participants.json file - # and include siteID, projectID, subprojectID + + def modify_participants_json(self): file_path = os.path.join( - data['bids_directory'], - data['output_time'], + self.get_bids_root_path(), 'participants.json' ) + with open(file_path, mode='r+', encoding='utf-8') as json_file: json_data = json.load(json_file) user_data = { 'site': { - 'Description': data['site_id'] - }, - 'project': { - 'Description': data['project_id'] + 'Description': "Site of the testing" }, 'subproject': { - 'Description': data['sub_project_id'] + 'Description': "Subproject of the participant" + }, + 'project': { + 'Description': "Project of the participant" }, 'debug': { - 'Version': data['appVersion'] + 'Version': self.data['appVersion'] } } json_data.update(user_data) @@ -88,18 +134,45 @@ def __init__(self, data, sio): json_file.close() -class Copy: - def __init__(self, data, sio): - print(data) - directory_path = 'sub-' + data['subject_id'].replace( - '_', '' - ).replace('-', '').replace(' ', '') + def copy_annotations_files(self): + if not self.data['annotations_tsv'] and not self.data['annotations_json']: + return + + file = os.path.join( + self.get_bids_root_path(), + '.bidsignore' + ) + + with open(file, mode='w', newline='') as bidsignore: + bidsignore.write('*_annotations.json\n') + bidsignore.write('*_annotations.tsv\n') + bidsignore.close() + + edf_file = [f for f in os.listdir(self.get_eeg_path()) if f.endswith('.edf')] + filename = os.path.join(self.get_eeg_path(), re.sub(r"_i?eeg.edf", '_annotations', edf_file[0])) + + if self.data['annotations_tsv']: + shutil.copyfile( + self.data['annotations_tsv'], + os.path.join(self.get_eeg_path(), filename + '.tsv') + ) + if self.data['annotations_json']: + shutil.copyfile( + self.data['annotations_json'], + os.path.join(self.get_eeg_path(), filename + '.json') + ) + + + def copy_events_tsv(self): + if not self.data['events_tsv']: + return + # events.tsv data collected: output = [] # Open user supplied events.tsv and grab data. - with open(data['events_tsv'], mode='r', newline='') as tsv_file: + with open(self.data['events_tsv'], mode='r', newline='') as tsv_file: tsv_file.readline() reader = csv.reader(tsv_file, delimiter='\t') rows = list(reader) @@ -117,13 +190,7 @@ def __init__(self, data, sio): print('error: ValueError') # The BIDS events.tsv location: - start_path = os.path.join( - data['bids_directory'], - data['output_time'], - directory_path, - 'ses-' + data['visit_label'], - (data['modality'].lower() or 'ieeg') - ) + start_path = self.get_eeg_path() path_events_tsv = '' eeg_edf = '' @@ -172,3 +239,28 @@ def __init__(self, data, sio): writer.writerows(output) tsv_file.close() + + def modify_eeg_json(self): + eeg_json = [f for f in os.listdir(self.get_eeg_path()) if f.endswith('eeg.json')] + if len(eeg_json) != 1: + raise ValueError('Found more than one eeg.json file') + + file_path = os.path.join(self.get_eeg_path(), eeg_json[0]) + + try: + with open(file_path, "r") as fp: + file_data = json.load(fp) + file_data["SoftwareFilters"] = self.data['software_filters'] + file_data["RecordingType"] = self.data['recording_type'] + + if (self.data["modality"] == "ieeg"): + file_data["iEEGReference"] = self.data['reference'] + else: + file_data["EEGReference"] = self.data['reference'] + + with open(file_path, "w") as fp: + json.dump(file_data, fp, indent=4) + + except IOError as e: + print(e) + print("Could not read or write eeg.json file") diff --git a/python/libs/iEEG.py b/python/libs/iEEG.py index 7412e0d..cafbde0 100644 --- a/python/libs/iEEG.py +++ b/python/libs/iEEG.py @@ -1,9 +1,8 @@ import os import mne from python.libs import EDF -from python.libs import TSV from mne_bids import write_raw_bids, BIDSPath - +import json # TarFile - tarfile the BIDS data. class TarFile: @@ -68,23 +67,26 @@ class Converter: # data = { file_path: '', bids_directory: '', read_only: false, # events_tsv: '', line_freq: '', site_id: '', project_id: '', - # sub_project_id: '', visit_label: '', subject_id: ''} + # sub_project_id: '', session: '', subject_id: ''} def __init__(self, data): print('- Converter: init started.') modality = 'seeg' - if data['modality'] == 'EEG': + if data['modality'] == 'eeg': modality = 'eeg' - self.to_bids( - file=data['file_path'], - ch_type=modality, - bids_directory=data['bids_directory'], - subject_id=data['subject_id'], - visit_label=data['visit_label'], - output_time=data['output_time'], - read_only=data['read_only'], - line_freq=data['line_freq'] - ) + for i, file_path in enumerate(data['file_paths']): + self.to_bids( + file=file_path, + ch_type=modality, + task=data['taskName'], + bids_directory=data['bids_directory'], + subject_id=data['participantID'], + session=data['session'], + split=((i+1) if len(data['file_paths']) > 1 else None), + output_time=data['output_time'], + read_only=data['read_only'], + line_freq=data['line_freq'] + ) @staticmethod def validate(path): @@ -102,12 +104,13 @@ def to_bids(self, file, bids_directory, subject_id, - visit_label, + session, output_time, task='test', + split=None, ch_type='seeg', read_only=False, - line_freq=60): + line_freq='n/a'): if self.validate(file): reader = EDF.EDFReader(fname=file) m_info, c_info = reader.open(fname=file) @@ -123,20 +126,16 @@ def to_bids(self, os.makedirs(bids_directory + os.path.sep + output_time, exist_ok=True) bids_directory = bids_directory + os.path.sep + output_time bids_root = bids_directory - m_info['subject_id'] = subject_id # 'alizee' + + m_info['subject_id'] = subject_id subject = m_info['subject_id'].replace('_', '').replace('-', '').replace(' ', '') - bids_basename = BIDSPath(subject=subject, task=task, root=bids_root, acquisition="seeg") - session = visit_label + bids_basename = BIDSPath(subject=subject, task=task, root=bids_root, acquisition=ch_type, split=split) + session = session bids_basename.update(session=session) raw.info['line_freq'] = line_freq - raw.info['subject_info'] = { - # 'his_id': "test", - # 'birthday': (1993, 1, 26), - # 'sex': 1, - # 'hand': 2, - } + raw._init_kwargs = { 'input_fname': file, 'eog': None, @@ -147,7 +146,11 @@ def to_bids(self, 'verbose': None } try: - write_raw_bids(raw, bids_basename, anonymize=dict(daysback=33630), overwrite=False, verbose=False) + write_raw_bids(raw, bids_basename, overwrite=False, verbose=False) + with open(bids_basename, 'r+b') as f: + f.seek(8) # id_info field starts 8 bytes in + f.write(bytes("X X X X".ljust(80), 'ascii')) + except Exception as ex: print(ex) print('finished') @@ -162,12 +165,3 @@ def __init__(self): from datetime import datetime now = datetime.now() self.latest_output = now.strftime("%Y-%m-%d-%Hh%Mm%Ss") - - -# Modifier - 1) used for SiteID to participants.tsv -# 2) used for user's events.tsv to BIDS output events.tsv -class Modifier: - def __init__(self, data, sio): - print('- Modifier: init started.') - TSV.Writer(data, sio) # includes SiteID to participants.tsv - TSV.Copy(data, sio) # copies events.tsv to ieeg directory. diff --git a/python/libs/loris_api.py b/python/libs/loris_api.py new file mode 100644 index 0000000..db70c59 --- /dev/null +++ b/python/libs/loris_api.py @@ -0,0 +1,160 @@ +import json +import requests +import urllib + +class LorisAPI: + url = 'https://localhost/api/v0.0.4-dev/' + username = 'admin' + password = '' + + #url = 'https://inhance-dev.loris.ca/api/v0.0.3/' + #username = 'admin' + #password = 'LORISitb2021!' + token = '' + + def __init__(self): + self.login() + #self.get_projects() + #self.get_sites() + #self.save_instrument() + #self.get_project('Pumpernickel') + #self.create_candidate() + #visit = self.get_visit(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel') + #print(visit) + #self.start_next_stage(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel', "2021-03-06") + + def login(self): + resp = json.loads(requests.post( + url = self.url + 'login', + json = { + 'username': self.username, + 'password': self.password + }, + verify = False + ).content.decode('ascii')) + + if resp.get('error'): + raise RuntimeError(resp.get('error')) + + self.token = resp.get('token') + print(self.token) + + def get_projects(self): + resp = requests.get( + url = self.url + 'projects', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + return json_resp.get('Projects') + + def get_subprojects(self, project): + project = self.get_project(project) + return project.get('Subprojects') + + def get_visits(self, project): + project = self.get_project(project) + return project.get('Visits') + + def get_sites(self): + resp = requests.get( + url = self.url + 'sites', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + sites = json_resp.get('Sites') + return sites + + def get_project(self, project): + resp = requests.get( + url = self.url + 'projects/' + urllib.parse.quote(project), + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + return json_resp + + def save_instrument(self): + resp = requests.put( + url = self.url + '/candidates/661630/V1/instruments/pet_mri_scans', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "Meta" : { + "Instrument" : 'PET/MRI scans', + "Visit" : 'V1', + "Candidate" : 661630, + "DDE" : False + }, + "PET/MRI scans" : { + "Date_taken" : "2021-06-07", + "Examiner" : "Rida", + "completion-date" : "2021-06-07", + } + }), + verify = False + ) + + print(resp) + json_resp = json.loads(resp.content.decode('ascii')) + print(json_resp) + + def get_visit(self, candid, visit, site, subproject, project): + resp = requests.get( + url = self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "Meta" : { + "CandID" : candid, + "Visit" : visit, + "Site" : site, + "Battery" : subproject, + "Project" : project + } + }), + verify = False + ) + + json_resp = json.loads(resp.content.decode('ascii')) + return json_resp + + def start_next_stage(self, candid, visit, site, subproject, project, date): + resp = requests.patch( + url = self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "CandID" : candid, + "Visit" : visit, + "Site" : site, + "Battery" : subproject, + "Project" : project, + "NextStageDate" : date + }), + verify = False + ) + + print (resp.status_code) + print(resp.text) + + def create_candidate(self): + resp = requests.post( + url = self.url + '/candidates/', + headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data = json.dumps({ + "Candidate" : { + "Project" : 'Pumpernickel', + "DoB" : "1985-12-22", + "Sex" : "Female", + "Site" : 'Montreal', + } + }), + verify = False + ) + + print(resp) + json_resp = json.loads(resp.content.decode('ascii')) + print(json_resp) + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bd16f75..85f997b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,12 @@ eventlet>=0.31.0 -mne~=0.22.0 -mne-bids~=0.6 +mne~=0.23.0 +# MNE-BIDS 0.8 will support the .EDF extension +# Before its release we need to use the dev version +git+git://github.com/mne-tools/mne-bids@main#egg=mne-bids mne-features~=0.1 numpy~=1.19.5 python-socketio~=5.0.4 python-engineio~=4.0.0 bids-validator~=1.6.0 +pybv>=0.4 +requests>=2.25.0 \ No newline at end of file diff --git a/src/css/Configuration.css b/src/css/Configuration.css index 1dcace5..1e7cecf 100644 --- a/src/css/Configuration.css +++ b/src/css/Configuration.css @@ -1,10 +1,5 @@ -.anonymize { - margin-top: 12px; - max-height: 30px; -} .readonly { cursor: default; - text-align: center; background: transparent; border: 1px none #3276b1; border-bottom-style: solid; @@ -12,3 +7,18 @@ .readonly:focus, .readonly:focus{ outline: none; } +input[type="button"].primary-btn { + color: #fff; + background-color: #064785; + border: none; + border-radius: .25rem; + padding: .375rem .75rem; +} +input[type="button"].primary-btn:disabled { + color: #000; + background-color: #ddd; + border: 1px solid #333; +} +textarea { + vertical-align: top; +} \ No newline at end of file diff --git a/src/css/Converter.css b/src/css/Converter.css index d61ec90..54c6678 100644 --- a/src/css/Converter.css +++ b/src/css/Converter.css @@ -1,15 +1,20 @@ .checkmark { - cursor: default; - font-size: 24px; - color: transparent; - text-shadow: 0 0 0 #43a243; + font-size: 18px; + padding-right: 10px; + color: #43a243; font-family: Segoe UI Symbol, serif; } .warning { - font-size: 24px; + font-size: 20px; + padding-right: 10px; + color: #FF8C00; font-family: Segoe UI Symbol, serif; } - +.error { + font-size: 18px; + padding-right: 10px; + color: red; +} .tooltip { cursor: default; position: relative; @@ -41,5 +46,5 @@ } .convert-bids-row { padding-top: 0; - margin-top: -5px; + margin-top: 10px; } diff --git a/src/css/Menu.css b/src/css/Menu.css index f4c391c..6fffc4b 100644 --- a/src/css/Menu.css +++ b/src/css/Menu.css @@ -6,15 +6,12 @@ border-bottom: 5px solid #252326; } .menu { - width: 100%; - margin: 0 auto; - display: table; + display: flex; + justify-content: space-around; } .menuTab { user-select: none; - position: relative; - display: table-cell; -webkit-user-select: none; } diff --git a/src/css/Modal.css b/src/css/Modal.css index cc2345e..c2af293 100644 --- a/src/css/Modal.css +++ b/src/css/Modal.css @@ -53,7 +53,7 @@ .modalHeader { height: 30px; display: flex; - padding: 30px; + padding: 30px 30px 0 30px; font-size: 18pt; align-items: center; flex-direction: row; @@ -69,7 +69,7 @@ .modalContent { font-size: 14pt; min-height: 80px; - padding: 10px 15px 5px 15px; + padding: 30px; -webkit-overflow-scrolling: touch; } .bids-errors { diff --git a/src/css/Validator.css b/src/css/Validator.css index d40df93..5ddec55 100644 --- a/src/css/Validator.css +++ b/src/css/Validator.css @@ -1,11 +1,8 @@ -.red-font-bold { +.red { color: #ff0000; - font-weight: bold; } -.green-font-italic { +.green { color: #02a902; - font-weight: bold; - font-style: italic; } .key-terminal { margin: 20px; @@ -15,6 +12,7 @@ background-color: #f4f4f4; } .terminal { + color: white; margin: 20px; overflow: auto; max-height: 600px; diff --git a/src/css/Welcome.css b/src/css/Welcome.css index 8006cc2..ea714ca 100644 --- a/src/css/Welcome.css +++ b/src/css/Welcome.css @@ -19,7 +19,6 @@ .footer { bottom: 0; width: 100%; - height: 40px; padding: 10px; color: #c8c8c8; position: fixed; diff --git a/src/css/index.css b/src/css/index.css index e0681d4..1291204 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -25,11 +25,17 @@ body { text-align: center; vertical-align: middle; } +.container { + display: flex; +} .info { padding: 14px; background-color: #f3f4f8; /*background-color: #039b83;*/ } +.half { + width: 50%; +} .small-pad { padding: 10px; } @@ -38,10 +44,50 @@ body { padding: 14px; flex-wrap: wrap; background-color: #f3f4f8; - /*background-color: #039b83;*/ +} +.info-flex-container input { + width: 100%; } .small-pad-flex { flex: 1; padding: 10px; max-width: 175px; } +.label { + width: 210px; + display: inline-block; + max-width: 100%; + padding-right: 20px; +} +.report .label { + width: auto; +} +input { + max-width: 100%; +} +.react-switch { + vertical-align: middle; + margin-right: 15px; +} +.comboField { + display: inline-block; + max-width: calc(100% - 230px); + vertical-align: top; +} +.alert { + position: relative; + padding: .75rem 1.25rem; + margin: 1rem 0 2rem 0; + border: 1px solid transparent; + border-radius: .25rem; +} +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} \ No newline at end of file diff --git a/src/jsx/Configuration.js b/src/jsx/Configuration.js index 81d10b1..6b08db0 100644 --- a/src/jsx/Configuration.js +++ b/src/jsx/Configuration.js @@ -2,17 +2,23 @@ import React, {useContext, useState, useEffect} from 'react'; import {AppContext} from '../context'; import PropTypes from 'prop-types'; import '../css/Configuration.css'; +import Switch from 'react-switch'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; // Components import { DirectoryInput, FileInput, - NumberInput, RadioInput, + NumberInput, + RadioInput, TextInput, + SelectInput, + TextareaInput, } from './elements/inputs'; // Socket.io -import {Event, SocketContext} from './socket.io'; +import {SocketContext} from './socket.io'; /** * Configuration - the Data Configuration component. @@ -25,103 +31,280 @@ const Configuration = (props) => { const socketContext = useContext(SocketContext); // React State - const [edfFile, setEdfFile] = useState({}); - const [edfType, setEdfType] = useState('iEEG'); - const [eventsTSV, setEventsTSV] = useState({}); - const [bidsDirectory, setBidsDirectory] = useState(null); - const [lineFreq, setLineFreq] = useState(''); - const [siteID, setSiteID] = useState(''); - const [projectID, setProjectID] = useState(''); - const [subProjectID, setSubProjectID] = useState(''); - const [visitLabel, setVisitLabel] = useState(''); - const [headerFields, setHeaderFields] = useState(null); - const [edfHeader, setHeader] = useState({ + const state = {}; + state.edfFiles = {}; + [state.edfFiles.get, state.edfFiles.set] = useState([]); + state.edfType = {}; + [state.edfType.get, state.edfType.set] = useState('ieeg'); + state.eventsTSV = {}; + [state.eventsTSV.get, state.eventsTSV.set] = useState([]); + state.annotationsTSV = {}; + [state.annotationsTSV.get, state.annotationsTSV.set] = useState([]); + state.annotationsJSON = {}; + [state.annotationsJSON.get, state.annotationsJSON.set] = useState([]); + state.bidsDirectory = {}; + [state.bidsDirectory.get, state.bidsDirectory.set] = useState(null); + state.LORIScompliant = {}; + [state.LORIScompliant.get, state.LORIScompliant.set] = useState(true); + state.siteID = {}; + [state.siteID.get, state.siteID.set] = useState('n/a'); + state.siteOptions = {}; + [state.siteOptions.get, state.siteOptions.set] = useState([]); + state.siteUseAPI = {}; + [state.siteUseAPI.get, state.siteUseAPI.set] = useState(false); + state.projectID = {}; + [state.projectID.get, state.projectID.set] = useState('n/a'); + state.projectOptions = {}; + [state.projectOptions.get, state.projectOptions.set] = useState([]); + state.projectUseAPI = {}; + [state.projectUseAPI.get, state.projectUseAPI.set] = useState(false); + state.subprojectID = {}; + [state.subprojectID.get, state.subprojectID.set] = useState('n/a'); + state.recordingDate = {}; + [state.recordingDate.get, state.recordingDate.set] = useState(null); + state.subprojectOptions = {}; + [state.subprojectOptions.get, state.subprojectOptions.set] = useState([]); + state.subprojectUseAPI = {}; + [state.subprojectUseAPI.get, state.subprojectUseAPI.set] = useState(false); + state.session = {}; + [state.session.get, state.session.set] = useState(''); + state.sessionOptions = {}; + state.eegMetadataFile = {}; + [state.eegMetadataFile.get, state.eegMetadataFile.set] = useState([]); + state.eegMetadata = {}; + [state.eegMetadata.get, state.eegMetadata.set] = useState([]); + state.lineFreq = {}; + [state.lineFreq.get, state.lineFreq.set] = useState(''); + state.taskName = {}; + [state.taskName.get, state.taskName.set] = useState(''); + state.reference = {}; + [state.reference.get, state.reference.set] = useState('n/a'); + state.recordingType = {}; + [state.recordingType.get, state.recordingType.set] = useState('n/a'); + state.softwareFilters = {}; + [state.softwareFilters.get, state.softwareFilters.set] = useState('n/a'); + [state.sessionOptions.get, state.sessionOptions.set] = useState([]); + state.sessionUseAPI = {}; + [state.sessionUseAPI.get, state.sessionUseAPI.set] = useState(false); + state.participantEntryMode = {}; + [ + state.participantEntryMode.get, + state.participantEntryMode.set, + ] = useState('loris'); + state.participantID = {}; + [state.participantID.get, state.participantID.set] = useState(''); + state.participantDOB = {}; + [state.participantDOB.get, state.participantDOB.set] = useState(''); + state.participantAge = {}; + [state.participantAge.get, state.participantAge.set] = useState('n/a'); + state.participantSex = {}; + [state.participantSex.get, state.participantSex.set] = useState('n/a'); + state.participantHand = {}; + [state.participantHand.get, state.participantHand.set] = useState('n/a'); + state.anonymize = {}; + [state.anonymize.get, state.anonymize.set] = useState(false); + state.subjectID = {}; + [state.subjectID.get, state.subjectID.set] = useState(''); + state.edfHeader = {}; + [state.edfHeader.get, state.edfHeader.set] = useState({ subject_id: '', recording_id: '', day: '', month: '', year: '', hour: '', minute: '', second: '', subtype: '', }); + useEffect(() => { + Object.keys(state).map((key) => appContext.setTask(key, state[key].get)); + }, []); + /** * Similar to componentDidMount and componentDidUpdate. */ useEffect(() => { - const keys = [ - 'subject_id', 'recording_id', - 'day', 'month', 'year', - 'hour', 'minute', 'second', - 'subtype', - ]; - const renderFields = []; - for (const key of keys) { - renderFields.push( -
- -
, + Object.keys(state.edfHeader.get).map((key) => { + appContext.setTask(key, state.edfHeader.get[key]); + }); + + if (!isNaN(parseInt(state.edfHeader.get['year']))) { + const date = new Date( + state.edfHeader.get['year'] < 85 ? + '20' + state.edfHeader.get['year'] : + '19' + state.edfHeader.get['year'], + (state.edfHeader.get['month']-1), + state.edfHeader.get['day'], + state.edfHeader.get['hour'], + state.edfHeader.get['minute'], + state.edfHeader.get['second'], ); - appContext.setTask(key, edfHeader[key]); + state.recordingDate.set(date); + appContext.setTask('recordingDate', date); } - setHeaderFields(renderFields); - }, [edfHeader]); + }, [state.edfHeader.get]); + + /** + * Similar to componentDidMount and componentDidUpdate. + */ + useEffect(() => { + if (socketContext) { + socketContext.emit('get_loris_sites'); + socketContext.on('loris_sites', (sites) => { + const siteOpts = []; + sites.map((site) => { + siteOpts.push(site.Name); + }); + state.siteOptions.set(siteOpts); + }); + + socketContext.emit('get_loris_projects'); + socketContext.on('loris_projects', (projects) => { + const projectOpts = []; + Object.keys(projects).map((project) => { + projectOpts.push(project); + }); + state.projectOptions.set(projectOpts); + }); + + socketContext.on('loris_subprojects', (subprojects) => { + const subprojectOpts = []; + subprojects.map((subproject) => { + subprojectOpts.push(subproject); + }); + state.subprojectOptions.set(subprojectOpts); + }); + + socketContext.on('loris_visits', (visits) => { + const visitOpts = []; + visits.map((visit) => { + visitOpts.push(visit); + }); + state.sessionOptions.set(visitOpts); + }); + + socketContext.on('edf_header', (message) => { + if (message['error']) { + console.error(message['error']); + } + + if (message['header']) { + state.edfHeader.set(message['header']); + state.subjectID.set(message['header']['subject_id']); + } + }); + + socketContext.on('metadata', (message) => { + if (message['error']) { + console.error(message['error']); + } + + if (message['metadata']) { + state.eegMetadata.set(message['metadata']); + } + }); + } + }, [socketContext]); + /** * onUserInput - input change by user. * @param {string} name - element name - * @param {object|string} value - element value + * @param {object|string|boolean} value - element value */ const onUserInput = (name, value) => { // Update the state of Configuration. switch (name) { - case 'edfFile': { - setEdfFile(value); - createHeaderFields(value['path']); + case 'edfFiles': + state.edfFiles.set(value); + createHeaderFields(value[0]['path']); break; - } - case 'edfType': { - setEdfType(value); + case 'eegMetadataFile': + state.eegMetadataFile.set(value); + createMetadataFields(value[0]['path']); break; - } - case 'eventsTSV': { - setEventsTSV(value); + case 'LORIScompliant': + if (value === 'yes') { + value = true; + state.participantEntryMode.set('loris'); + } else { + value = false; + state.participantEntryMode.set('manual'); + } + state.LORIScompliant.set(value); break; - } - case 'bidsDirectory': { - setBidsDirectory(value); + case 'siteID_API': + if (value == 'Manual entry') { + value = ''; + state.siteUseAPI.set(false); + } else { + state.siteUseAPI.set(true); + } + state.siteID.set(value); + name = 'siteID'; break; - } - case 'lineFreq': { - setLineFreq(value); + case 'siteID_Manual': + state.siteID.set(value); + name = 'siteID'; break; - } - case 'siteID': { - setSiteID(value); + case 'projectID_API': + if (value == 'Manual entry') { + state.projectUseAPI.set(false); + value = ''; + } else { + state.projectUseAPI.set(true); + socketContext.emit('get_loris_subprojects', value); + socketContext.emit('get_loris_visits', value); + } + state.projectID.set(value); + name = 'projectID'; break; - } - case 'projectID': { - setProjectID(value); + case 'projectID_Manual': + state.projectID.set(value); + name = 'projectID'; break; - } - case 'subProjectID': { - setSubProjectID(value); + case 'subprojectID_API': + if (value == 'Manual entry') { + state.subprojectUseAPI.set(false); + value = ''; + } else { + state.subprojectUseAPI.set(true); + } + state.subprojectID.set(value); + name = 'subprojectID'; break; - } - case 'visitLabel': { - setVisitLabel(value); + case 'subprojectID_Manual': + state.subprojectID.set(value); + name = 'subprojectID'; break; - } - default: { - return; - } + case 'session_API': + if (value == 'Manual entry') { + state.sessionUseAPI.set(false); + value = ''; + } else { + state.sessionUseAPI.set(true); + } + state.session.set(value); + name = 'session'; + break; + case 'session_Manual': + state.session.set(value); + name = 'session'; + break; + case 'anonymize': + if (value) { + anonymizeHeaderValues(); + } else { + onUserHeaderFieldInput('subject_id', state.subjectID.get); + } + state.anonymize.set(value); + break; + default: + if (name in state) { + state[name].set(value); + } + } + if (name in state) { + // Update the 'task' of app context. + appContext.setTask(name, value); } - // Update the 'task' of app context. - appContext.setTask(name, value); }; /** @@ -130,7 +313,7 @@ const Configuration = (props) => { * @param {object|string} value - element value */ const onUserHeaderFieldInput = (name, value) => { - setHeader((prevState) => { + state.edfHeader.set((prevState) => { return {...prevState, [name]: value}; }); // Update the 'task' of app context. @@ -149,31 +332,38 @@ const Configuration = (props) => { }; /** - * onMessage - received message from python. - * @param {object} message - response + * createMetadataFields - Metadata file given from user. + * @param {string} path - metadata file path */ - const onMessage = (message) => { - console.info(message); - if (message['header']) { - setHeader(message['header']); - } + const createMetadataFields = (path) => { + socketContext.emit('get_metadata', { + file_path: path, + }); + }; + + /** + * arrayToObject - Convert an array to an object + * { value: value } + * + * @param {array} array + * + * @return {Object} + */ + const arrayToObject = (array) => { + return array.reduce((obj, item) => { + return { + ...obj, + [item]: item, + }; + }, {}); }; /** * anonymizeHeaderValues - anonymize iEEG header values. */ const anonymizeHeaderValues = () => { - const keys = [ - 'subject_id', 'recording_id', - 'day', 'month', 'year', - ]; - const anonymize = { - subject_id: '0 X X X', - recording_id: 'Startdate 31-DEC-1924 X mne-bids_anonymize X', - day: 31, - month: 12, - year: 85, - }; + const keys = ['subject_id']; + const anonymize = {subject_id: 'X X X X'}; for (const key of keys) { onUserHeaderFieldInput(key, anonymize[key]); appContext.setTask(key, anonymize[key]); @@ -181,113 +371,485 @@ const Configuration = (props) => { }; /** - * Renders the React component. - * @return {JSX.Element} - React markup for component. + * createCandidate */ + const createCandidate = () => { + }; + return props.visible ? ( <> - + Data Configuration -
-
- +
+ edfFile['name']).join(', ') + } + label='EDF Recording to convert' + required={true} onUserInput={onUserInput} /> +
Can be split into multiple EDF files
-
+
-
+
-
- +
-
- +
-
- - LORIS metadata - -
-
- + eegMetadataFile['name'], + ).join(', ') + } + label='Parameter metadata (tsv)' onUserInput={onUserInput} />
-
- +
-
- +
-
- +
+ + Recording details + +
+
+
+ +
+ {state.LORIScompliant.get && + <> +
+ +
+ + {!state.siteUseAPI.get && + + } +
+
+
+ +
+ + {!state.projectUseAPI.get && + + } +
+
+
+ +
+ + {!state.subprojectUseAPI.get && + + } +
+
+ + } +
+ +
+ {state.LORIScompliant.get && + + } + {!state.sessionUseAPI.get && + + } +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
- - EDF header data + + Participant Data -
- {headerFields} - +
+ {state.LORIScompliant.get && +
+ +
+ } + {state.participantEntryMode.get == 'loris' && + <> +
+ + onUserInput('participantDOB', date)} + /> +
+
+ +
+
+ +
+ + + } + {state.participantEntryMode.get == 'manual' && + <> +
+ + {state.LORIScompliant.get && +
Use the LORIS PSCID
+ } +
+
+ +
+
+ +
+
+ +
+ + } +
+ + EDF Header Data + + +
+
+
+ { + state.subjectID.set(value); + onUserHeaderFieldInput(name, value); + }} + placeholder={state.edfHeader.get['subject_id']} + /> +
+ Recommended EDF anonymization: "X X X X"
+ (EDF spec: patientID patientSex patientBirthdate patientName) +
+
+
+
+ +
+ (EDF spec: Startdate dd-MMM-yyyy + administrationCode investigatorCode equipmentCode) + +
+
+
+ +
+
- ) : null; }; diff --git a/src/jsx/Converter.js b/src/jsx/Converter.js index 876ee9f..0e277d8 100644 --- a/src/jsx/Converter.js +++ b/src/jsx/Converter.js @@ -5,6 +5,7 @@ import '../css/Converter.css'; // Display Loading, Success, Error import Modal from './elements/modal'; +import {TextInput} from './elements/inputs'; // Socket.io import {Event, SocketContext} from './socket.io'; @@ -15,6 +16,8 @@ import {Event, SocketContext} from './socket.io'; * @return {JSX.Element} */ const Converter = (props) => { + const [preparedBy, setPreparedBy] = useState(''); + // React Context const appContext = useContext(AppContext); const socketContext = useContext(SocketContext); @@ -32,14 +35,14 @@ const Converter = (props) => { }, message: { loading: - + BIDS creation in progress... 😴 , success: - - Success creating BIDS! + + Success creating BIDS! , error: '', }, @@ -55,18 +58,32 @@ const Converter = (props) => { }); setModalVisible(true); socketContext.emit('edf_to_bids', { - file_path: appContext.getFromTask('edfFile') ? - appContext.getFromTask('edfFile').path : '', + file_paths: appContext.getFromTask('edfFiles') ? + appContext.getFromTask('edfFiles') + .map((edfFile) => edfFile['path']) : [], modality: appContext.getFromTask('edfType') ?? 'ieeg', bids_directory: appContext.getFromTask('bidsDirectory') ?? '', read_only: false, - events_tsv: appContext.getFromTask('eventsTSV') ? - appContext.getFromTask('eventsTSV').path : '', - line_freq: appContext.getFromTask('lineFreq') ?? 'n/a', + events_tsv: appContext.getFromTask('eventsTSV').length > 0 ? + appContext.getFromTask('eventsTSV')[0]['path'] : '', + annotations_tsv: appContext.getFromTask('annotationsTSV').length > 0 ? + appContext.getFromTask('annotationsTSV')[0]['path'] : '', + annotations_json: appContext.getFromTask('annotationsJSON').length > 0 ? + appContext.getFromTask('annotationsJSON')[0]['path'] : '', site_id: appContext.getFromTask('siteID') ?? '', project_id: appContext.getFromTask('projectID') ?? '', - sub_project_id: appContext.getFromTask('subProjectID') ?? '', - visit_label: appContext.getFromTask('visitLabel') ?? '', + sub_project_id: appContext.getFromTask('subprojectID') ?? '', + session: appContext.getFromTask('session') ?? '', + participantID: appContext.getFromTask('participantID') ?? '', + age: appContext.getFromTask('participantAge') ?? '', + hand: appContext.getFromTask('participantHand') ?? '', + sex: appContext.getFromTask('participantSex') ?? '', + preparedBy: appContext.getFromTask('preparedBy') ?? '', + line_freq: appContext.getFromTask('lineFreq') || 'n/a', + software_filters: appContext.getFromTask('softwareFilters') ?? 'n/a', + recording_type: appContext.getFromTask('recordingType') ?? 'n/a', + taskName: appContext.getFromTask('taskName') ?? '', + reference: appContext.getFromTask('reference') ?? '', subject_id: appContext.getFromTask('subject_id') ?? '', }); }; @@ -81,7 +98,7 @@ const Converter = (props) => { time = time.slice(0, time.lastIndexOf('-')) + ' ' + time.slice(time.lastIndexOf('-')+1); setSuccessMessage(<> - Last created at: {time} + Last created at: {time} ); } }, [outputTime]); @@ -109,8 +126,9 @@ const Converter = (props) => { } else { setModalText((prevState) => { prevState.message['error'] = ( -
- {message['error'].map((error, i) =>

{error}

)} +
+ {message['error'].map((error, i) => + {error}
)}
); return {...prevState, ['mode']: 'error'}; @@ -118,287 +136,271 @@ const Converter = (props) => { } }; + let error = false; + /** * Renders the React component. + * + * @param {string} _ + * @param {string} value + * * @return {JSX.Element} - React markup for component. */ return props.visible ? ( <> - + EDF to BIDS format -
-
- Review your Configuration selections: -
    -
  • - {appContext.getFromTask('edfFile') ? - (<> - EDF data file:  - {appContext.getFromTask('edfFile').name} - - ) : - (<> - No EDF file selected. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('eventsTSV') ? - (<> - Including:  - {appContext.getFromTask('eventsTSV').name} - - ) : - (<> - No events.tsv selected. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('bidsDirectory') ? - (<> - BIDS output folder:  - {appContext.getFromTask('bidsDirectory')} - - ) : - (<> - The BIDS output directory hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('lineFreq') ? - (<> - Line frequency::  - {appContext.getFromTask('lineFreq')} - - ) : - (<> - The Line frequency hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
-
-
- Review your LORIS metadata: -
    -
  • - {appContext.getFromTask('siteID') ? - (<> - The Site:  - {appContext.getFromTask('siteID')} - - ) : - (<> - The Site hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('projectID') ? - (<> - The Project:  - {appContext.getFromTask('projectID')} - - ) : - (<> - The Project hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('subProjectID') ? - (<> - The SubProject:  - {appContext.getFromTask('subProjectID')} - - ) : - (<> - The SubProject hasn't been set in configuration. - ❌ - - Please correct. - - - ) - } -
  • -
  • - {appContext.getFromTask('visitLabel') ? - (<> - No Visit Label set:  - {appContext.getFromTask('visitLabel')} - - ) : - (<> - Please enter a value in the Configuration tab. - ❌ - - Please correct. - - - ) - } -
  • -
+
+
+ Review your data configuration: +
+ {(appContext.getFromTask('edfFiles') && + appContext.getFromTask('edfFiles').length > 0) ? + <> + + EDF data file(s):  + {appContext.getFromTask('edfFiles') + .map((edfFile) => edfFile['name']).join(', ') + } + : + <> + {error = true} + No EDF file selected. + + } +
+
+ {(appContext.getFromTask('eventsTSV') && + Object.keys(appContext.getFromTask('eventsTSV')) + .length > 0) ? + <> + Including: + {appContext.getFromTask('eventsTSV').name} + : + <> + + No events.tsv selected. + + } +
+
+ {appContext.getFromTask('bidsDirectory') ? + <> + + BIDS output directory: + {appContext.getFromTask('bidsDirectory')} + : + <> + {error = true} + + No BIDS output directory selected. + + } +
-
- Verify anonymization of EDF header data: - +
+
+
+ Review your participant data: +
+ {appContext.getFromTask('participantID') ? + <> + + Participant ID: {appContext.getFromTask('participantID')} + : + <> + {error = true} + <> + + Participant ID is not specified. + + + } +
+
+
+ Verify anonymization of EDF header data: +
+ {appContext.getFromTask('subject_id') ? + <> + Subject ID:  + {appContext.getFromTask('subject_id')} + : + <> + + Subject ID is not modified. + + } +
+ {appContext.getFromTask('recording_id') && +
+ Recording ID:  + {appContext.getFromTask('recording_id')} +
+ } + {appContext.getFromTask('recordingDate') && +
+ Recording Date:  + {new Intl.DateTimeFormat( + 'en-US', + { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }, + ).format(appContext.getFromTask('recordingDate'))} +
+ } +
+ + {error ? +
+ ❌ Please correct the above errors. +
: +
+ ✔ Ready to proceed +
+ } + +
+ +
+ setPreparedBy(value)} + /> + {!preparedBy && +
+ + Please enter your name for verification tracking purposes. +
+ }
-
- - Click to convert data:  - - + {successMessage}
@@ -407,7 +409,7 @@ const Converter = (props) => { title={modalText.title[modalText.mode]} show={modalVisible} close={hideModal} - width={'500px'} + width='500px' > {modalText.message[modalText.mode]} diff --git a/src/jsx/SplashScreen.js b/src/jsx/SplashScreen.js index 31aa660..021b359 100644 --- a/src/jsx/SplashScreen.js +++ b/src/jsx/SplashScreen.js @@ -18,11 +18,11 @@ const SplashScreen = (props) => { */ return props.visible ? ( <> -

+

EEG2BIDS Wizard is loading ...

diff --git a/src/jsx/Validator.js b/src/jsx/Validator.js index d635d46..f448c61 100644 --- a/src/jsx/Validator.js +++ b/src/jsx/Validator.js @@ -32,15 +32,15 @@ const Validator = (props) => { }, message: { loading: - + BIDS compression in progress ... 😴 , success: - - Success compressing BIDS! + + Success compressing BIDS! , error: '', }, @@ -92,37 +92,33 @@ const Validator = (props) => { validator['file_paths'].forEach((value, index) => { if (validator['result'][index]) { renderFields.push( -
- {value} +
+ {value}
, ); } else { renderFields.push( -
- {value} +
+ {value}
, ); } }); renderPackageBIDS.push( -
-
+
+
Package BIDS output folder into a compressed file:  + type='button' + value='Compress BIDS'/>
, ); } setValidPaths(<> -
- Valid is green. - Invalid is red. -
-
+
{renderFields}
{renderPackageBIDS} @@ -150,17 +146,17 @@ const Validator = (props) => { */ return props.visible ? ( <> - + Validation confirmation -
-
+
+
Run BIDS Validator:  + type='button' + value='Validate BIDS'/>
{validPath} @@ -168,7 +164,7 @@ const Validator = (props) => { title={modalText.title[modalText.mode]} show={modalVisible} close={hideModal} - width={'500px'} + width='500px' > {modalText.message[modalText.mode]} diff --git a/src/jsx/Welcome.js b/src/jsx/Welcome.js index 850d41e..14f301a 100644 --- a/src/jsx/Welcome.js +++ b/src/jsx/Welcome.js @@ -8,6 +8,13 @@ import '../css/Welcome.css'; * @return {JSX.Element} */ const Welcome = (props) => { + /** + * openGitHub - Navigate browser to EEG2BIDS Wizard. + */ + const openBIDS = () => { + const myAPI = window['myAPI']; + myAPI.visitBIDS(); + }; /** * openGitHub - Navigate browser to EEG2BIDS Wizard. */ @@ -15,6 +22,13 @@ const Welcome = (props) => { const myAPI = window['myAPI']; myAPI.visitGitHub(); }; + /** + * openGitHub - Navigate browser to EEG2BIDS Wizard. + */ + const openIssues = () => { + const myAPI = window['myAPI']; + myAPI.visitIssues(); + }; /** * openMCIN - Navigate browser to MCIN. */ @@ -43,22 +57,25 @@ const Welcome = (props) => { */ return props.visible ? ( <> - + Welcome to EEG2BIDS Wizard -
-

EEG2BIDS Wizard  - is a tool for de-identification of EDF data and conversion to BIDS -  format for data sharing. +

+

+ EEG2BIDS Wizard is a tool for de-identification of EDF data + and conversion to BIDS format for data sharing.

This software is designed to run on EDF files (EEG or iEEG) for -  one subject at a time. Events and metadata such as a -  LORIS ProjectID and Visit Label can be included. + one recording from one subject at a time. Events and metadata such as + a LORIS ProjectID and Visit Label can be included.

@@ -68,8 +85,10 @@ const Welcome = (props) => { Configuration tab:

    -
  • Select the data file, events file (events.tsv), and output  - folder +
  • + Select the data file(s) (multiple files allowed for a + single recording split into multiple files), + events file (events.tsv), and output folder
  • Set metadata values
  • Anonymize the EDF header data
  • @@ -96,15 +115,16 @@ const Welcome = (props) => {
    {/**/}
    -
    - Powered by  - +
    + Powered by open source software - and  - + and MNE-BIDS .
    - Copyright © 2021 + + Contact us + with your feedback.
    + Copyright © 2021 MCIN.
    diff --git a/src/jsx/elements/inputs.js b/src/jsx/elements/inputs.js index ff45c5b..ad7bf00 100644 --- a/src/jsx/elements/inputs.js +++ b/src/jsx/elements/inputs.js @@ -13,8 +13,8 @@ export const FileInput = (props) => { */ const handleChange = (event) => { // Send current file to parent component - const file = event.target.files[0] ? event.target.files[0] : ''; - props.onUserInput(props.id, file); + const files = event.target.files ? Array.from(event.target.files) : []; + props.onUserInput(props.id, files); }; /** * Renders the React component. @@ -22,26 +22,42 @@ export const FileInput = (props) => { */ return ( <> - + - -  {props.placeholder ?? 'No file chosen'} - + /> {props.placeholder ?? 'No file chosen'} ); }; +FileInput.defaultProps = { + multiple: false, +}; FileInput.propTypes = { id: PropTypes.string, + multiple: PropTypes.bool, name: PropTypes.string, label: PropTypes.string, accept: PropTypes.string, @@ -73,7 +89,14 @@ export const DirectoryInput = (props) => { */ return ( <> - + { */ return ( <> - + {props.label && + + } { }; TextInput.defaultProps = { readonly: false, + required: false, }; TextInput.propTypes = { id: PropTypes.string, name: PropTypes.string, + required: PropTypes.bool, label: PropTypes.string, value: PropTypes.oneOfType([ PropTypes.string, @@ -167,8 +201,10 @@ export const RadioInput = (props) => { * @param {object} event - input event */ const handleChange = (event) => { - const value = event.target.value; - props.onUserInput(props.id, value); + props.onUserInput( + props.id, + event.target.value, + ); }; /** * generateRadioLayout - creates the radio input layout. @@ -221,9 +257,13 @@ export const RadioInput = (props) => {
    , ); } - return
    - + return
    + {content}
    ; }; @@ -237,15 +277,117 @@ export const RadioInput = (props) => { ); }; +RadioInput.defaultProps = { + required: false, +}; RadioInput.propTypes = { id: PropTypes.string, name: PropTypes.string, + required: PropTypes.bool, label: PropTypes.string, options: PropTypes.object, checked: PropTypes.string, onUserInput: PropTypes.func, }; +/** + * SelectInput - the select component. + * @param {object} props + * @return {JSX.Element} + */ +export const SelectInput = (props) => { + /** + * handleChange - input change by user. + * @param {object} event - input event + */ + const handleChange = (event) => { + props.onUserInput( + props.id, + event.target.value, + ); + }; + /** + * generateRadioLayout - creates the radio input layout. + * @return {JSX.Element} + */ + const generateSelectLayout = () => { + const styleRow = { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + width: '100%', + }; + const styleColumn = { + display: 'flex', + flexDirection: 'column', + alignSelf: 'flex-start', + marginRight: '10px', + }; + const styleInput = { + display: 'inline-block', + margin: '0 10px 10px 0', + cursor: 'pointer', + }; + + let emptyOptionHTML = null; + // Add empty option + if (props.emptyOption) { + emptyOptionHTML = ; + } + + const optionList = Object.keys(props.options).map((key, index) => { + return ( + + ); + }); + + return ( + <> + {props.label && + + } + + + ); + }; + /** + * Renders the React component. + * @return {JSX.Element} - React markup for component. + */ + return ( + <> + {generateSelectLayout()} + + ); +}; +SelectInput.propTypes = { + id: PropTypes.string, + name: PropTypes.string, + label: PropTypes.string, + value: PropTypes.string, + emptyOption: PropTypes.string, + options: PropTypes.object, + onUserInput: PropTypes.func, +}; + /** * NumberInput - the input type='number' component. * @param {object} props @@ -257,8 +399,10 @@ export const NumberInput = (props) => { * @param {object} event - input event */ const handleChange = (event) => { - const value = event.target.value; - props.onUserInput(props.id, value); + props.onUserInput( + props.id, + event.target.value, + ); }; /** * Renders the React component. @@ -266,7 +410,9 @@ export const NumberInput = (props) => { */ return ( <> - + { + /** + * handleChange - input change by user. + * @param {object} event - input event + */ + const handleChange = (event) => { + props.onUserInput( + props.name, + event.target.value, + ); + }; + + /** + * Renders the React component. + * @return {JSX.Element} - React markup for the component + */ + return ( + <> + + + + ); +}; + +TextareaInput.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + value: PropTypes.string, + id: PropTypes.string, + required: PropTypes.bool, + rows: PropTypes.number, + cols: PropTypes.number, + onUserInput: PropTypes.func, +}; + +TextareaInput.defaultProps = { + required: false, + rows: 4, + cols: 25, }; diff --git a/src/jsx/elements/menu.js b/src/jsx/elements/menu.js index 0f6d0e7..b04df2b 100644 --- a/src/jsx/elements/menu.js +++ b/src/jsx/elements/menu.js @@ -9,7 +9,6 @@ import '../../css/Menu.css'; */ const MenuTab = (props) => { // css styling. - const menuTabWidth = {width: props.width}; const classesTitleText = props.active ? 'menu-title menu-active' : 'menu-title'; /** @@ -17,7 +16,7 @@ const MenuTab = (props) => { * @return {JSX.Element} - React markup for component. */ return ( -
    +
    {props.title} @@ -43,14 +42,13 @@ const Menu = (props) => { * @return {JSX.Element} - React markup for component. */ return props.visible ? ( -
    -
    +
    +
    { props.tabs.map((tab, index) => ( { transform: props.show ? 'translateY(0)' : 'translateY(-25%)', }; return ( -
    -
    -
    -
    - + {props.title} - × -
    +
    {props.children}
    diff --git a/wiki/macOS/README.md b/wiki/macOS/README.md index f451ba9..d7a6bf3 100644 --- a/wiki/macOS/README.md +++ b/wiki/macOS/README.md @@ -20,10 +20,10 @@ npm run start python -m venv . source bin/activate pip install -r requirements.txt -python -m python.pycat +python -m python.eeg2bids ``` -**Note:** Both the "python-service" & the "electron-app" need to be running simultaneously for pyCat to successfully function in development! +**Note:** Both the "python-service" & the "electron-app" need to be running simultaneously for EEG2BIDS Wizard to successfully function in development! ## Production From ea6244a91b5c8ee98047d3ec87cd3165ea5d13bd Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:14:38 -0400 Subject: [PATCH 02/21] reformat --- python/eeg2bids.py | 12 ++-- python/libs/loris_api.py | 152 +++++++++++++++++++-------------------- 2 files changed, 83 insertions(+), 81 deletions(-) diff --git a/python/eeg2bids.py b/python/eeg2bids.py index 9852b9f..0ed3d5b 100644 --- a/python/eeg2bids.py +++ b/python/eeg2bids.py @@ -8,13 +8,15 @@ from python.libs.loris_api import LorisAPI import csv +# EEG2BIDS Wizard version +appVersion = '1.0.0' + # Create socket listener. sio = socketio.Server(async_mode='eventlet', cors_allowed_origins=[]) app = socketio.WSGIApp(sio) -loris_api = LorisAPI() -# EEG2BIDS Wizard version -appVersion = '1.0.0' +# Create Loris API handler. +loris_api = LorisAPI() @sio.event @@ -84,13 +86,13 @@ def ieeg_get_header(sid, data): def get_metadata(sid, data): # data = { file_path: 'path to metadata file' } print('metadata file:', data) - + if not data['file_path']: print('No file path found.') response = { 'error': 'No file path found.', } - else : + else : try: with open(data['file_path']) as fd: reader = csv.DictReader(fd, delimiter="\t", quotechar='"') diff --git a/python/libs/loris_api.py b/python/libs/loris_api.py index db70c59..1914c9e 100644 --- a/python/libs/loris_api.py +++ b/python/libs/loris_api.py @@ -2,37 +2,38 @@ import requests import urllib + class LorisAPI: url = 'https://localhost/api/v0.0.4-dev/' username = 'admin' password = '' - #url = 'https://inhance-dev.loris.ca/api/v0.0.3/' - #username = 'admin' - #password = 'LORISitb2021!' + # url = 'https://inhance-dev.loris.ca/api/v0.0.3/' + # username = 'admin' + # password = 'LORISitb2021!' token = '' def __init__(self): self.login() - #self.get_projects() - #self.get_sites() - #self.save_instrument() - #self.get_project('Pumpernickel') - #self.create_candidate() - #visit = self.get_visit(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel') - #print(visit) - #self.start_next_stage(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel', "2021-03-06") + # self.get_projects() + # self.get_sites() + # self.save_instrument() + # self.get_project('Pumpernickel') + # self.create_candidate() + # visit = self.get_visit(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel') + # print(visit) + # self.start_next_stage(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel', "2021-03-06") def login(self): resp = json.loads(requests.post( - url = self.url + 'login', - json = { - 'username': self.username, + url=self.url + 'login', + json={ + 'username': self.username, 'password': self.password }, - verify = False + verify=False ).content.decode('ascii')) - + if resp.get('error'): raise RuntimeError(resp.get('error')) @@ -41,11 +42,11 @@ def login(self): def get_projects(self): resp = requests.get( - url = self.url + 'projects', - headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, - verify = False + url=self.url + 'projects', + headers={'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify=False ) - + json_resp = json.loads(resp.content.decode('ascii')) return json_resp.get('Projects') @@ -59,102 +60,101 @@ def get_visits(self, project): def get_sites(self): resp = requests.get( - url = self.url + 'sites', - headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, - verify = False + url=self.url + 'sites', + headers={'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify=False ) - + json_resp = json.loads(resp.content.decode('ascii')) sites = json_resp.get('Sites') return sites def get_project(self, project): resp = requests.get( - url = self.url + 'projects/' + urllib.parse.quote(project), - headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, - verify = False + url=self.url + 'projects/' + urllib.parse.quote(project), + headers={'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + verify=False ) - + json_resp = json.loads(resp.content.decode('ascii')) return json_resp def save_instrument(self): resp = requests.put( - url = self.url + '/candidates/661630/V1/instruments/pet_mri_scans', - headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, - data = json.dumps({ - "Meta" : { - "Instrument" : 'PET/MRI scans', - "Visit" : 'V1', - "Candidate" : 661630, - "DDE" : False + url=self.url + '/candidates/661630/V1/instruments/pet_mri_scans', + headers={'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data=json.dumps({ + "Meta": { + "Instrument": 'PET/MRI scans', + "Visit": 'V1', + "Candidate": 661630, + "DDE": False }, - "PET/MRI scans" : { - "Date_taken" : "2021-06-07", - "Examiner" : "Rida", - "completion-date" : "2021-06-07", + "PET/MRI scans": { + "Date_taken": "2021-06-07", + "Examiner": "Rida", + "completion-date": "2021-06-07", } }), - verify = False + verify=False ) - + print(resp) json_resp = json.loads(resp.content.decode('ascii')) print(json_resp) def get_visit(self, candid, visit, site, subproject, project): resp = requests.get( - url = self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), - headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, - data = json.dumps({ - "Meta" : { - "CandID" : candid, - "Visit" : visit, - "Site" : site, - "Battery" : subproject, - "Project" : project + url=self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), + headers={'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data=json.dumps({ + "Meta": { + "CandID": candid, + "Visit": visit, + "Site": site, + "Battery": subproject, + "Project": project } }), - verify = False + verify=False ) - + json_resp = json.loads(resp.content.decode('ascii')) return json_resp - + def start_next_stage(self, candid, visit, site, subproject, project, date): resp = requests.patch( - url = self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), - headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, - data = json.dumps({ - "CandID" : candid, - "Visit" : visit, - "Site" : site, - "Battery" : subproject, - "Project" : project, - "NextStageDate" : date + url=self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), + headers={'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data=json.dumps({ + "CandID": candid, + "Visit": visit, + "Site": site, + "Battery": subproject, + "Project": project, + "NextStageDate": date }), - verify = False + verify=False ) - print (resp.status_code) + print(resp.status_code) print(resp.text) def create_candidate(self): resp = requests.post( - url = self.url + '/candidates/', - headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, - data = json.dumps({ - "Candidate" : { - "Project" : 'Pumpernickel', - "DoB" : "1985-12-22", - "Sex" : "Female", - "Site" : 'Montreal', + url=self.url + '/candidates/', + headers={'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, + data=json.dumps({ + "Candidate": { + "Project": 'Pumpernickel', + "DoB": "1985-12-22", + "Sex": "Female", + "Site": 'Montreal', } }), - verify = False + verify=False ) - + print(resp) json_resp = json.loads(resp.content.decode('ascii')) print(json_resp) - \ No newline at end of file From a340c36c59229bebcdc26ae3fa86691c5076fb17 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:33:59 -0400 Subject: [PATCH 03/21] cleanup --- python/eeg2bids.py | 33 ++++++++++++++++++++++----------- src/jsx/Configuration.js | 26 +++++++++++++------------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/python/eeg2bids.py b/python/eeg2bids.py index 0ed3d5b..35f3f29 100644 --- a/python/eeg2bids.py +++ b/python/eeg2bids.py @@ -15,8 +15,9 @@ sio = socketio.Server(async_mode='eventlet', cors_allowed_origins=[]) app = socketio.WSGIApp(sio) + # Create Loris API handler. -loris_api = LorisAPI() +# loris_api = LorisAPI() @sio.event @@ -29,8 +30,8 @@ def connect(sid, environ): def tarfile_bids_thread(data): iEEG.TarFile(data) response = { - 'compression_time': 'example_5mins' - } + 'compression_time': 'example_5mins' + } return eventlet.tpool.Proxy(response) @@ -42,8 +43,8 @@ def tarfile_bids(sid, data): print('response received!') print(response) send = { - 'compression_time': response['compression_time'] - } + 'compression_time': response['compression_time'] + } print('send received!') print(send) sio.emit('response', send) @@ -51,19 +52,27 @@ def tarfile_bids(sid, data): @sio.event def get_loris_sites(sid): - sio.emit('loris_sites', loris_api.get_sites()) + print('get_loris_sites has ran!') + # sio.emit('loris_sites', loris_api.get_sites()) + @sio.event def get_loris_projects(sid): - sio.emit('loris_projects', loris_api.get_projects()) + print('get_loris_projects has ran!') + # sio.emit('loris_projects', loris_api.get_projects()) + @sio.event def get_loris_subprojects(sid, project): - sio.emit('loris_subprojects', loris_api.get_subprojects(project)) + print('get_loris_subprojects has ran!') + # sio.emit('loris_subprojects', loris_api.get_subprojects(project)) + @sio.event def get_loris_visits(sid, project): - sio.emit('loris_visits', loris_api.get_visits(project)) + print('get_loris_visits has ran!') + # sio.emit('loris_visits', loris_api.get_visits(project)) + @sio.event def ieeg_get_header(sid, data): @@ -82,6 +91,7 @@ def ieeg_get_header(sid, data): } sio.emit('edf_header', response) + @sio.event def get_metadata(sid, data): # data = { file_path: 'path to metadata file' } @@ -92,12 +102,12 @@ def get_metadata(sid, data): response = { 'error': 'No file path found.', } - else : + else: try: with open(data['file_path']) as fd: reader = csv.DictReader(fd, delimiter="\t", quotechar='"') response = { - 'metadata': {rows['Field']:rows['Value'] for rows in reader} + 'metadata': {rows['Field']: rows['Value'] for rows in reader} } except IOError: print("Could not read the metadata file.") @@ -107,6 +117,7 @@ def get_metadata(sid, data): sio.emit('metadata', response) + def edf_to_bids_thread(data): print('data is ') print(data) diff --git a/src/jsx/Configuration.js b/src/jsx/Configuration.js index 6b08db0..234f2ee 100644 --- a/src/jsx/Configuration.js +++ b/src/jsx/Configuration.js @@ -128,13 +128,13 @@ const Configuration = (props) => { if (!isNaN(parseInt(state.edfHeader.get['year']))) { const date = new Date( state.edfHeader.get['year'] < 85 ? - '20' + state.edfHeader.get['year'] : - '19' + state.edfHeader.get['year'], - (state.edfHeader.get['month']-1), - state.edfHeader.get['day'], - state.edfHeader.get['hour'], - state.edfHeader.get['minute'], - state.edfHeader.get['second'], + Number('20' + state.edfHeader.get['year']) : + Number('19' + state.edfHeader.get['year']), + Number(state.edfHeader.get['month']-1), + Number(state.edfHeader.get['day']), + Number(state.edfHeader.get['hour']), + Number(state.edfHeader.get['minute']), + Number(state.edfHeader.get['second']), ); state.recordingDate.set(date); appContext.setTask('recordingDate', date); @@ -231,7 +231,7 @@ const Configuration = (props) => { state.LORIScompliant.set(value); break; case 'siteID_API': - if (value == 'Manual entry') { + if (value === 'Manual entry') { value = ''; state.siteUseAPI.set(false); } else { @@ -245,7 +245,7 @@ const Configuration = (props) => { name = 'siteID'; break; case 'projectID_API': - if (value == 'Manual entry') { + if (value === 'Manual entry') { state.projectUseAPI.set(false); value = ''; } else { @@ -261,7 +261,7 @@ const Configuration = (props) => { name = 'projectID'; break; case 'subprojectID_API': - if (value == 'Manual entry') { + if (value === 'Manual entry') { state.subprojectUseAPI.set(false); value = ''; } else { @@ -275,7 +275,7 @@ const Configuration = (props) => { name = 'subprojectID'; break; case 'session_API': - if (value == 'Manual entry') { + if (value === 'Manual entry') { state.sessionUseAPI.set(false); value = ''; } else { @@ -667,7 +667,7 @@ const Configuration = (props) => { />
    } - {state.participantEntryMode.get == 'loris' && + {state.participantEntryMode.get === 'loris' && <>
    + ) : null; }; diff --git a/src/jsx/Welcome.js b/src/jsx/Welcome.js index 14f301a..2d82576 100644 --- a/src/jsx/Welcome.js +++ b/src/jsx/Welcome.js @@ -30,7 +30,7 @@ const Welcome = (props) => { myAPI.visitIssues(); }; /** - * openMCIN - Navigate browser to MCIN. + * openMNE - Navigate browser to MNE. */ const openMNE = () => { const myAPI = window['myAPI']; diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 60a2989..8405974 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -1,24 +1,89 @@ -import React from 'react'; +import React, {useState} from 'react'; import PropTypes from 'prop-types'; +import '../../css/Authentication.css'; -export const Authentication = (props) => { +export const AuthenticationMessage = (props) => { + const [loginStatus, setLoginStatus] = useState(false); + const [loginMessage, setLoginMessage] = useState( + 'You are not logged in to LORIS Account', + ); + const [loginLink, setLoginLink] = useState( + 'Log in...', + ); const handleClick = () => { - + props.setAuthCredentialsVisible(true); + if (loginStatus) { + // Already logged in. + } else { + // Not logged in. + } }; return ( <> - +
    + + {loginMessage} + + +     {loginLink} + +
    ); }; -Authentication.propTypes = { +AuthenticationMessage.propTypes = { onUserInput: PropTypes.func, }; +export const AuthenticationCredentials = (props) => { + const handleClose = () => { + props.close(true); + }; + const styleVisible = {visibility: props.show ? 'visible' : 'hidden'}; + const styleAnimation = {width: props.width ? props.width : 'auto'}; + const styleContainer = { + opacity: props.show ? 1 : 0, + transform: props.show ? 'translateY(0)' : 'translateY(-25%)', + }; + return ( +
    +
    +
    +
    + + {props.title} + + × + + +
    + {props.children} +
    +
    +
    +
    +
    + ); +}; +AuthenticationCredentials.propTypes = { + show: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + width: PropTypes.string, + title: PropTypes.string, + children: PropTypes.node, +}; +AuthenticationCredentials.defaultProps = { + width: null, + title: null, + children: null, +}; + export default { - Authentication, + AuthenticationMessage, + AuthenticationCredentials, }; From 21714c545ef8afcc1a455d27c5ddb141321a7f06 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 10:11:44 -0400 Subject: [PATCH 07/21] package.json fix for production --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 404b7e7..bd05b68 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "prop-types": "^15.7.2", "react": "^17.0.2", "react-color": "^2.19.3", + "react-datepicker": "^4.1.1", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "^4.0.2", + "react-switch": "^6.0.0", "socket.io-client": "^4.1.2" }, "devDependencies": { @@ -31,8 +33,6 @@ "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.24.0", "lint-staged": "^11.0.0", - "react-datepicker": "^4.1.1", - "react-switch": "^6.0.0", "wait-on": "^6.0.0" }, "scripts": { From f07e79468f14760ec136a27fe3476082c05d3464 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 10:20:18 -0400 Subject: [PATCH 08/21] update --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index bd05b68..c2802a5 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,10 @@ "not ie <= 11", "not op_mini all" ], + "contributors": [ + "Christine Rogers", + "Laëtitia Fesselier" + ], "author": { "name": "Alizée Wickenheiser", "email": "alizee.wickenheiser@mcgill.ca", From 2cf73d4f9f6c58872c05f0289e271fb726c55293 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 10:51:04 -0400 Subject: [PATCH 09/21] update --- public/preload.js | 3 +++ src/jsx/elements/authentication.js | 30 +++++++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/public/preload.js b/public/preload.js index 7b717d8..9f17c22 100644 --- a/public/preload.js +++ b/public/preload.js @@ -1,5 +1,8 @@ const {contextBridge} = require('electron'); +/** + * contextBridge should be cautious of security risk. + */ contextBridge.exposeInMainWorld('myAPI', { dialog: () => { const {dialog} = require('@electron/remote'); diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 8405974..4719b0f 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useState, useEffect} from 'react'; import PropTypes from 'prop-types'; import '../../css/Authentication.css'; @@ -10,6 +10,16 @@ export const AuthenticationMessage = (props) => { const [loginLink, setLoginLink] = useState( 'Log in...', ); + + /** + * Similar to componentDidMount and componentDidUpdate. + */ + useEffect(() => { + }, []); + + /** + * User clicked sign in.. + */ const handleClick = () => { props.setAuthCredentialsVisible(true); if (loginStatus) { @@ -19,16 +29,14 @@ export const AuthenticationMessage = (props) => { } }; return ( - <> -
    - - {loginMessage} - - -     {loginLink} - -
    - +
    + + {loginMessage} + + +     {loginLink} + +
    ); }; AuthenticationMessage.propTypes = { From 38fc2729b9b3a75ff08625195f208364ba46769f Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 11:00:42 -0400 Subject: [PATCH 10/21] update --- public/preload.js | 7 +++++++ src/jsx/elements/authentication.js | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/public/preload.js b/public/preload.js index 9f17c22..36c49be 100644 --- a/public/preload.js +++ b/public/preload.js @@ -28,6 +28,13 @@ contextBridge.exposeInMainWorld('myAPI', { const {shell} = require('electron'); shell.openExternal('https://mcin.ca'); }, + getLorisAuthenticationCredentials: () => { + // todo + return 'hello world'; + }, + setLorisAuthenticationCredentials: () => { + // todo + }, openSettings: () => { const ipcRenderer = require('electron').ipcRenderer; ipcRenderer.send('openSettingsWindow', null); diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 4719b0f..84b4b16 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -15,6 +15,12 @@ export const AuthenticationMessage = (props) => { * Similar to componentDidMount and componentDidUpdate. */ useEffect(() => { + const myAPI = window['myAPI']; + const credentials = myAPI.getLorisAuthenticationCredentials(); + if (credentials) { + setLoginMessage('LORIS Account set as [todo username]'); + setLoginLink('Sign in to another account..'); + } }, []); /** From 94cd792b95bada622ce38fbb279f37749137e02d Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 11:21:35 -0400 Subject: [PATCH 11/21] update --- src/jsx/elements/authentication.js | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 84b4b16..d82ea75 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -2,7 +2,12 @@ import React, {useState, useEffect} from 'react'; import PropTypes from 'prop-types'; import '../../css/Authentication.css'; +import { + TextInput, +} from './inputs'; + export const AuthenticationMessage = (props) => { + // React state const [loginStatus, setLoginStatus] = useState(false); const [loginMessage, setLoginMessage] = useState( 'You are not logged in to LORIS Account', @@ -50,9 +55,35 @@ AuthenticationMessage.propTypes = { }; export const AuthenticationCredentials = (props) => { + // React state + const [lorisURL, setLorisURL] = useState(''); + const [lorisUsername, setLorisUsername] = useState(''); + const [lorisPassword, setLorisPassword] = useState(''); + /** + * Close the Authentication Credentials + */ const handleClose = () => { props.close(true); }; + /** + * onUserInput - input change by user. + * @param {string} name - element name + * @param {object|string|boolean} value - element value + */ + const onUserInput = (name, value) => { + console.log(name); + switch (name) { + case 'lorisURL': + setLorisURL(value); + break; + case 'lorisUsername': + setLorisUsername(value); + break; + case 'lorisPassword': + setLorisPassword(value); + break; + } + }; const styleVisible = {visibility: props.show ? 'visible' : 'hidden'}; const styleAnimation = {width: props.width ? props.width : 'auto'}; const styleContainer = { @@ -76,6 +107,30 @@ export const AuthenticationCredentials = (props) => {
    + + + {props.children}
    From 03503053bc1c46b7d2fddcbe7c4ec84120a5730d Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 11:37:50 -0400 Subject: [PATCH 12/21] update --- package.json | 5 +++-- src/jsx/Configuration.js | 2 +- src/jsx/elements/authentication.js | 25 +++++++++++++++++-------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index c2802a5..9b29f49 100644 --- a/package.json +++ b/package.json @@ -70,12 +70,13 @@ "not op_mini all" ], "contributors": [ + "Alizée Wickenheiser", "Christine Rogers", "Laëtitia Fesselier" ], "author": { - "name": "Alizée Wickenheiser", - "email": "alizee.wickenheiser@mcgill.ca", + "name": "Loris Team", + "email": "loris-dev@bic.mni.mcgill.ca", "url": "https://github.com/aces/eeg2bids" }, "build": { diff --git a/src/jsx/Configuration.js b/src/jsx/Configuration.js index 62549ac..21c92f9 100644 --- a/src/jsx/Configuration.js +++ b/src/jsx/Configuration.js @@ -867,7 +867,7 @@ const Configuration = (props) => {
    { + // React Context + const appContext = useContext(AppContext); + const socketContext = useContext(SocketContext); + // React state const [lorisURL, setLorisURL] = useState(''); const [lorisUsername, setLorisUsername] = useState(''); const [lorisPassword, setLorisPassword] = useState(''); + /** * Close the Authentication Credentials */ const handleClose = () => { props.close(true); }; + /** * onUserInput - input change by user. * @param {string} name - element name * @param {object|string|boolean} value - element value */ const onUserInput = (name, value) => { - console.log(name); switch (name) { case 'lorisURL': setLorisURL(value); @@ -84,6 +94,8 @@ export const AuthenticationCredentials = (props) => { break; } }; + + // Styling for rendering const styleVisible = {visibility: props.show ? 'visible' : 'hidden'}; const styleAnimation = {width: props.width ? props.width : 'auto'}; const styleContainer = { @@ -111,7 +123,7 @@ export const AuthenticationCredentials = (props) => { name='lorisURL' required={true} label='URL of LORIS instance' - placeholder={'Example: https://loris.ca'} + placeholder='Example: https://loris.ca' value={lorisURL} onUserInput={onUserInput} /> @@ -119,7 +131,7 @@ export const AuthenticationCredentials = (props) => { name='lorisUsername' required={true} label='Username' - placeholder={'Username'} + placeholder='Username' value={lorisUsername} onUserInput={onUserInput} /> @@ -127,11 +139,10 @@ export const AuthenticationCredentials = (props) => { name='lorisPassword' required={true} label='Password' - placeholder={'Password'} + placeholder='Password' value={lorisPassword} onUserInput={onUserInput} /> - {props.children}
    @@ -144,12 +155,10 @@ AuthenticationCredentials.propTypes = { close: PropTypes.func.isRequired, width: PropTypes.string, title: PropTypes.string, - children: PropTypes.node, }; AuthenticationCredentials.defaultProps = { width: null, title: null, - children: null, }; export default { From 57a51931cb37877787bde7641cd1ea0d5a48bc6b Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 12:51:14 -0400 Subject: [PATCH 13/21] update --- src/App.js | 2 +- src/jsx/elements/authentication.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 901b91d..fba5a07 100644 --- a/src/App.js +++ b/src/App.js @@ -52,7 +52,7 @@ const App = () => { setTask(task); }, getFromTask: (key) => { - return task[key]; + return task[key] ?? ''; }, }}> <> diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 576d4be..d5b1e20 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -69,10 +69,20 @@ export const AuthenticationCredentials = (props) => { const [lorisUsername, setLorisUsername] = useState(''); const [lorisPassword, setLorisPassword] = useState(''); + /** + * Similar to componentDidMount and componentDidUpdate. + */ + useEffect(() => { + setLorisURL(appContext.getFromTask('lorisURL')); + setLorisUsername(appContext.getFromTask('lorisUsername')); + setLorisPassword(appContext.getFromTask('lorisPassword')); + }, []); + /** * Close the Authentication Credentials */ const handleClose = () => { + // todo send update - user credentials changed props.close(true); }; @@ -93,6 +103,8 @@ export const AuthenticationCredentials = (props) => { setLorisPassword(value); break; } + // Update the 'task' of app context. + appContext.setTask(name, value); }; // Styling for rendering From b021fcbbf2f7bc06bee2835242d21302befbdca8 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 13:51:31 -0400 Subject: [PATCH 14/21] update --- public/electron.js | 17 +++++++++++++++ public/preload.js | 19 +++++++++++------ src/jsx/elements/authentication.js | 33 +++++++++++++++++++++++------- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/public/electron.js b/public/electron.js index 64ceb21..ea9f0cd 100644 --- a/public/electron.js +++ b/public/electron.js @@ -131,6 +131,23 @@ app.on('ready', async () => { createSettingsWindow(); } }); + ipcMain.on('setLorisAuthenticationCredentials', (event, credentials) => { + const keytar = require('keytar'); + keytar.setPassword( + 'EEG2BIDS', + credentials.lorisUsername, + credentials.lorisPassword, + ); + }); + ipcMain.handle('getLorisAuthenticationCredentials', async (event, arg) => { + const keytar = require('keytar'); + const credentials = await keytar.findCredentials('EEG2BIDS'); + return { + lorisURL: '', + lorisUsername: credentials[0] ? credentials[0].account : '', + lorisPassword: credentials[0] ? credentials[0].password : '', + }; + }); }); app.on('window-all-closed', () => { diff --git a/public/preload.js b/public/preload.js index 36c49be..61a1acc 100644 --- a/public/preload.js +++ b/public/preload.js @@ -28,12 +28,19 @@ contextBridge.exposeInMainWorld('myAPI', { const {shell} = require('electron'); shell.openExternal('https://mcin.ca'); }, - getLorisAuthenticationCredentials: () => { - // todo - return 'hello world'; - }, - setLorisAuthenticationCredentials: () => { - // todo + getLorisAuthenticationCredentials: async () => { + const ipcRenderer = require('electron').ipcRenderer; + const credentials = await ipcRenderer.invoke( + 'getLorisAuthenticationCredentials', + null, + ); + console.log('credentials is '); + console.log(credentials); + return credentials; + }, + setLorisAuthenticationCredentials: (credentials) => { + const ipcRenderer = require('electron').ipcRenderer; + ipcRenderer.send('setLorisAuthenticationCredentials', credentials); }, openSettings: () => { const ipcRenderer = require('electron').ipcRenderer; diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index d5b1e20..059d634 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -12,6 +12,10 @@ import { } from './inputs'; export const AuthenticationMessage = (props) => { + // React Context + const appContext = useContext(AppContext); + const socketContext = useContext(SocketContext); + // React state const [loginStatus, setLoginStatus] = useState(false); const [loginMessage, setLoginMessage] = useState( @@ -24,12 +28,19 @@ export const AuthenticationMessage = (props) => { /** * Similar to componentDidMount and componentDidUpdate. */ - useEffect(() => { + useEffect(async () => { const myAPI = window['myAPI']; - const credentials = myAPI.getLorisAuthenticationCredentials(); + const credentials = await myAPI.getLorisAuthenticationCredentials(); if (credentials) { setLoginMessage('LORIS Account set as [todo username]'); setLoginLink('Sign in to another account..'); + appContext.setTask('lorisURL', credentials.lorisURL); + appContext.setTask('lorisUsername', credentials.lorisUsername); + appContext.setTask('lorisPassword', credentials.lorisPassword); + console.log('Check here 1:'); + console.log(appContext.getFromTask('lorisURL')); + console.log(appContext.getFromTask('lorisUsername')); + console.log(appContext.getFromTask('lorisPassword')); } }, []); @@ -72,17 +83,25 @@ export const AuthenticationCredentials = (props) => { /** * Similar to componentDidMount and componentDidUpdate. */ - useEffect(() => { - setLorisURL(appContext.getFromTask('lorisURL')); - setLorisUsername(appContext.getFromTask('lorisUsername')); - setLorisPassword(appContext.getFromTask('lorisPassword')); + useEffect(async () => { + const myAPI = window['myAPI']; + const credentials = await myAPI.getLorisAuthenticationCredentials(); + setLorisURL(credentials.lorisURL); + setLorisUsername(credentials.lorisUsername); + setLorisPassword(credentials.lorisPassword); }, []); /** * Close the Authentication Credentials + * but first update (?new) credentials. */ const handleClose = () => { - // todo send update - user credentials changed + const myAPI = window['myAPI']; + myAPI.setLorisAuthenticationCredentials({ + lorisURL: lorisURL, + lorisUsername: lorisUsername, + lorisPassword: lorisPassword, + }); props.close(true); }; From 6831be4ccffeb3f5cb50bed17dc73a62c3926ba0 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 14:14:39 -0400 Subject: [PATCH 15/21] update --- public/electron.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/public/electron.js b/public/electron.js index ea9f0cd..3bbd5b3 100644 --- a/public/electron.js +++ b/public/electron.js @@ -138,12 +138,23 @@ app.on('ready', async () => { credentials.lorisUsername, credentials.lorisPassword, ); + const Store = require('electron-store'); + const schema = { + lorisURL: { + type: 'string', + format: 'url', + }, + }; + const store = new Store({schema}); + store.set('lorisURL', credentials.lorisURL); }); ipcMain.handle('getLorisAuthenticationCredentials', async (event, arg) => { const keytar = require('keytar'); const credentials = await keytar.findCredentials('EEG2BIDS'); + const Store = require('electron-store'); + const store = new Store(); return { - lorisURL: '', + lorisURL: store.get('lorisURL') ?? '', lorisUsername: credentials[0] ? credentials[0].account : '', lorisPassword: credentials[0] ? credentials[0].password : '', }; From d63f908ffbefe050c1a0ba675d45e26c6a47c414 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 14:42:47 -0400 Subject: [PATCH 16/21] update --- public/electron.js | 42 ++++++++++++++++++------------ src/jsx/elements/authentication.js | 8 +----- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/public/electron.js b/public/electron.js index 3bbd5b3..b9ece88 100644 --- a/public/electron.js +++ b/public/electron.js @@ -131,23 +131,31 @@ app.on('ready', async () => { createSettingsWindow(); } }); - ipcMain.on('setLorisAuthenticationCredentials', (event, credentials) => { - const keytar = require('keytar'); - keytar.setPassword( - 'EEG2BIDS', - credentials.lorisUsername, - credentials.lorisPassword, - ); - const Store = require('electron-store'); - const schema = { - lorisURL: { - type: 'string', - format: 'url', - }, - }; - const store = new Store({schema}); - store.set('lorisURL', credentials.lorisURL); - }); + ipcMain.on('setLorisAuthenticationCredentials', + async (event, credentials) => { + const keytar = require('keytar'); + // Delete all old credentials + const services = await keytar.findCredentials('EEG2BIDS'); + for (const service of services) { + await keytar.deletePassword('EEG2BIDS', service.account); + } + // Set new credentials (secure) + await keytar.setPassword( + 'EEG2BIDS', + credentials.lorisUsername, + credentials.lorisPassword, + ); + // Set lorisURL in electron-store (not secure) + const Store = require('electron-store'); + const schema = { + lorisURL: { + type: 'string', + format: 'url', + }, + }; + const store = new Store({schema}); + store.set('lorisURL', credentials.lorisURL); + }); ipcMain.handle('getLorisAuthenticationCredentials', async (event, arg) => { const keytar = require('keytar'); const credentials = await keytar.findCredentials('EEG2BIDS'); diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 059d634..68a8c17 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -17,7 +17,6 @@ export const AuthenticationMessage = (props) => { const socketContext = useContext(SocketContext); // React state - const [loginStatus, setLoginStatus] = useState(false); const [loginMessage, setLoginMessage] = useState( 'You are not logged in to LORIS Account', ); @@ -32,7 +31,7 @@ export const AuthenticationMessage = (props) => { const myAPI = window['myAPI']; const credentials = await myAPI.getLorisAuthenticationCredentials(); if (credentials) { - setLoginMessage('LORIS Account set as [todo username]'); + setLoginMessage(`LORIS Account set as ${credentials.lorisUsername}`); setLoginLink('Sign in to another account..'); appContext.setTask('lorisURL', credentials.lorisURL); appContext.setTask('lorisUsername', credentials.lorisUsername); @@ -49,11 +48,6 @@ export const AuthenticationMessage = (props) => { */ const handleClick = () => { props.setAuthCredentialsVisible(true); - if (loginStatus) { - // Already logged in. - } else { - // Not logged in. - } }; return (
    From 357b953f78916c9ad93eedef64151a3706b7f735 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 14:45:21 -0400 Subject: [PATCH 17/21] update --- public/preload.js | 2 -- src/jsx/elements/authentication.js | 4 ---- 2 files changed, 6 deletions(-) diff --git a/public/preload.js b/public/preload.js index 61a1acc..6b2f8fb 100644 --- a/public/preload.js +++ b/public/preload.js @@ -34,8 +34,6 @@ contextBridge.exposeInMainWorld('myAPI', { 'getLorisAuthenticationCredentials', null, ); - console.log('credentials is '); - console.log(credentials); return credentials; }, setLorisAuthenticationCredentials: (credentials) => { diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 68a8c17..1cd0e38 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -36,10 +36,6 @@ export const AuthenticationMessage = (props) => { appContext.setTask('lorisURL', credentials.lorisURL); appContext.setTask('lorisUsername', credentials.lorisUsername); appContext.setTask('lorisPassword', credentials.lorisPassword); - console.log('Check here 1:'); - console.log(appContext.getFromTask('lorisURL')); - console.log(appContext.getFromTask('lorisUsername')); - console.log(appContext.getFromTask('lorisPassword')); } }, []); From 21b61cd21ef08ffac68a864b35aed735626df4cf Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 14:49:43 -0400 Subject: [PATCH 18/21] update --- src/jsx/elements/authentication.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index 1cd0e38..ebd777e 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -36,6 +36,7 @@ export const AuthenticationMessage = (props) => { appContext.setTask('lorisURL', credentials.lorisURL); appContext.setTask('lorisUsername', credentials.lorisUsername); appContext.setTask('lorisPassword', credentials.lorisPassword); + socketContext.emit('lorisCredentials', credentials); } }, []); @@ -87,11 +88,13 @@ export const AuthenticationCredentials = (props) => { */ const handleClose = () => { const myAPI = window['myAPI']; - myAPI.setLorisAuthenticationCredentials({ + const credentials = { lorisURL: lorisURL, lorisUsername: lorisUsername, lorisPassword: lorisPassword, - }); + }; + myAPI.setLorisAuthenticationCredentials(credentials); + socketContext.emit('lorisCredentials', credentials); props.close(true); }; From ed70ddaf7ca8a43af0d9924e0be0b6f3781c6ff9 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 15:04:19 -0400 Subject: [PATCH 19/21] update --- python/eeg2bids.py | 29 ++++++++++++++++++++++++----- python/libs/loris_api.py | 2 +- src/jsx/Configuration.js | 4 ++-- src/jsx/elements/authentication.js | 4 ++-- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/python/eeg2bids.py b/python/eeg2bids.py index 29e068d..fa0def9 100644 --- a/python/eeg2bids.py +++ b/python/eeg2bids.py @@ -11,13 +11,20 @@ # EEG2BIDS Wizard version appVersion = '1.0.1' +# LORIS credentials of user +lorisCredentials = { + 'lorisURL': '', + 'lorisUsername': '', + 'lorisPassword': '', +} + # Create socket listener. sio = socketio.Server(async_mode='eventlet', cors_allowed_origins=[]) app = socketio.WSGIApp(sio) # Create Loris API handler. -# loris_api = LorisAPI() +loris_api = LorisAPI() @sio.event @@ -50,28 +57,40 @@ def tarfile_bids(sid, data): sio.emit('response', send) +@sio.event +def set_loris_credentials(sid, data): + print('set_loris_credentials:', data) + lorisCredentials = data + loris_api.url = lorisCredentials.lorisURL + loris_api.username = lorisCredentials.lorisUsername + loris_api.password = lorisCredentials.lorisPassword + loris_api.login() + sio.emit('loris_sites', loris_api.get_sites()) + sio.emit('loris_projects', loris_api.get_projects()) + + @sio.event def get_loris_sites(sid): print('get_loris_sites has ran!') - # sio.emit('loris_sites', loris_api.get_sites()) + sio.emit('loris_sites', loris_api.get_sites()) @sio.event def get_loris_projects(sid): print('get_loris_projects has ran!') - # sio.emit('loris_projects', loris_api.get_projects()) + sio.emit('loris_projects', loris_api.get_projects()) @sio.event def get_loris_subprojects(sid, project): print('get_loris_subprojects has ran!') - # sio.emit('loris_subprojects', loris_api.get_subprojects(project)) + sio.emit('loris_subprojects', loris_api.get_subprojects(project)) @sio.event def get_loris_visits(sid, project): print('get_loris_visits has ran!') - # sio.emit('loris_visits', loris_api.get_visits(project)) + sio.emit('loris_visits', loris_api.get_visits(project)) @sio.event diff --git a/python/libs/loris_api.py b/python/libs/loris_api.py index 1914c9e..c206c34 100644 --- a/python/libs/loris_api.py +++ b/python/libs/loris_api.py @@ -14,7 +14,7 @@ class LorisAPI: token = '' def __init__(self): - self.login() + # self.login() # self.get_projects() # self.get_sites() # self.save_instrument() diff --git a/src/jsx/Configuration.js b/src/jsx/Configuration.js index 21c92f9..af5057e 100644 --- a/src/jsx/Configuration.js +++ b/src/jsx/Configuration.js @@ -151,7 +151,7 @@ const Configuration = (props) => { */ useEffect(() => { if (socketContext) { - socketContext.emit('get_loris_sites'); + // socketContext.emit('get_loris_sites'); socketContext.on('loris_sites', (sites) => { const siteOpts = []; sites.map((site) => { @@ -160,7 +160,7 @@ const Configuration = (props) => { state.siteOptions.set(siteOpts); }); - socketContext.emit('get_loris_projects'); + // socketContext.emit('get_loris_projects'); socketContext.on('loris_projects', (projects) => { const projectOpts = []; Object.keys(projects).map((project) => { diff --git a/src/jsx/elements/authentication.js b/src/jsx/elements/authentication.js index ebd777e..5a5c640 100644 --- a/src/jsx/elements/authentication.js +++ b/src/jsx/elements/authentication.js @@ -36,7 +36,7 @@ export const AuthenticationMessage = (props) => { appContext.setTask('lorisURL', credentials.lorisURL); appContext.setTask('lorisUsername', credentials.lorisUsername); appContext.setTask('lorisPassword', credentials.lorisPassword); - socketContext.emit('lorisCredentials', credentials); + socketContext.emit('set_loris_credentials', credentials); } }, []); @@ -94,7 +94,7 @@ export const AuthenticationCredentials = (props) => { lorisPassword: lorisPassword, }; myAPI.setLorisAuthenticationCredentials(credentials); - socketContext.emit('lorisCredentials', credentials); + socketContext.emit('set_loris_credentials', credentials); props.close(true); }; From 6499546be81d80e296a3ca53af3e78de1f34e56c Mon Sep 17 00:00:00 2001 From: Laetitia Fesselier Date: Mon, 28 Jun 2021 15:21:50 -0400 Subject: [PATCH 20/21] Remove LORIS credentials --- python/libs/loris_api.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/python/libs/loris_api.py b/python/libs/loris_api.py index db70c59..02643d8 100644 --- a/python/libs/loris_api.py +++ b/python/libs/loris_api.py @@ -4,24 +4,12 @@ class LorisAPI: url = 'https://localhost/api/v0.0.4-dev/' - username = 'admin' + username = '' password = '' - - #url = 'https://inhance-dev.loris.ca/api/v0.0.3/' - #username = 'admin' - #password = 'LORISitb2021!' token = '' def __init__(self): self.login() - #self.get_projects() - #self.get_sites() - #self.save_instrument() - #self.get_project('Pumpernickel') - #self.create_candidate() - #visit = self.get_visit(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel') - #print(visit) - #self.start_next_stage(317604, 'Visit 01', 'Data Coordinating Center', 'Stale', 'Pumpernickel', "2021-03-06") def login(self): resp = json.loads(requests.post( @@ -32,7 +20,7 @@ def login(self): }, verify = False ).content.decode('ascii')) - + if resp.get('error'): raise RuntimeError(resp.get('error')) @@ -45,7 +33,7 @@ def get_projects(self): headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, verify = False ) - + json_resp = json.loads(resp.content.decode('ascii')) return json_resp.get('Projects') @@ -63,7 +51,7 @@ def get_sites(self): headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, verify = False ) - + json_resp = json.loads(resp.content.decode('ascii')) sites = json_resp.get('Sites') return sites @@ -74,7 +62,7 @@ def get_project(self, project): headers = {'Authorization': 'Bearer %s' % self.token, 'LORIS-Overwrite': 'overwrite'}, verify = False ) - + json_resp = json.loads(resp.content.decode('ascii')) return json_resp @@ -97,7 +85,7 @@ def save_instrument(self): }), verify = False ) - + print(resp) json_resp = json.loads(resp.content.decode('ascii')) print(json_resp) @@ -117,10 +105,10 @@ def get_visit(self, candid, visit, site, subproject, project): }), verify = False ) - + json_resp = json.loads(resp.content.decode('ascii')) return json_resp - + def start_next_stage(self, candid, visit, site, subproject, project, date): resp = requests.patch( url = self.url + '/candidates/' + str(candid) + '/' + urllib.parse.quote(visit), @@ -153,8 +141,8 @@ def create_candidate(self): }), verify = False ) - + print(resp) json_resp = json.loads(resp.content.decode('ascii')) print(json_resp) - \ No newline at end of file + From 2d0e38d807851bbb61edc72b65834b876dfc66d1 Mon Sep 17 00:00:00 2001 From: maltheism <16293415+maltheism@users.noreply.github.com> Date: Mon, 28 Jun 2021 15:28:34 -0400 Subject: [PATCH 21/21] update --- python/eeg2bids.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/eeg2bids.py b/python/eeg2bids.py index fa0def9..f6a43c0 100644 --- a/python/eeg2bids.py +++ b/python/eeg2bids.py @@ -61,7 +61,9 @@ def tarfile_bids(sid, data): def set_loris_credentials(sid, data): print('set_loris_credentials:', data) lorisCredentials = data - loris_api.url = lorisCredentials.lorisURL + if lorisCredentials.lorisURL.endswith('/'): + lorisCredentials.lorisURL = lorisCredentials.lorisURL[:-1] + loris_api.url = lorisCredentials.lorisURL + '/api/v0.0.4-dev/' loris_api.username = lorisCredentials.lorisUsername loris_api.password = lorisCredentials.lorisPassword loris_api.login()