v;v++)q=v/60*h.PI,p.push(new o.Point(2*h.cos(q),-2*h.sin(q)));p.push(new o.Point(-2,0),new o.Point(-2,4),new o.Point(2,4));var E=new o.d3.Shape(p,1);this.render=function(c,n){this.bindBuffers(c);c.material.setDiffuseColor(n.macro_colorByChain?this.chainColor:n.nucleics_tubeColor);c.drawElements(c.TRIANGLES,
-this.vertexIndexBuffer.numItems,c.UNSIGNED_SHORT,0);if(this.partitions)for(var k=1,p=this.partitions.length;kk;k++)q=this.ends[k],q=g.translate(c.modelViewMatrix,[q.x,q.y,q.z],[]),p=l/2,g.scale(q,[p,p,p]),c.setMatrixUniforms(q),c.drawElements(c.TRIANGLES,c.sphereBuffer.vertexIndexBuffer.numItems,c.UNSIGNED_SHORT,0);c.cylinderBuffer.bindBuffers(c);
-k=0;for(p=f.length-1;k
e.dot(w,e.cross(t,
-A))&&(v*=-1),g.rotateY(q,v)),n.macro_colorByChain||("shapely"===n.nucleics_residueColor?j[r.name]?c.material.setDiffuseColor(j[r.name].shapelyColor):c.material.setDiffuseColor(j["*"].shapelyColor):"rainbow"===n.nucleics_residueColor&&c.material.setDiffuseColor(a.rainbowAt(k,p,n.macro_rainbowColors))),c.setMatrixUniforms(q),c.drawElements(c.TRIANGLES,E.vertexIndexBuffer.numItems,c.UNSIGNED_SHORT,0)}};l.Tube.prototype=new l._Mesh;l.CatmullTube=function(a,d,b,g){var j=[];a.push(a[a.length-1]);for(var l=
-0,v=a.length-2;l<=v;l++){for(var u=a[0==l?0:l-1],t=a[l+0],w=a[l+1],x=a[l==v?l+1:l+2],B=[],c=0;ce;e++)j=this.ends[e],h=g.translate(a.modelViewMatrix,[j.x,j.y,j.z],[]),j=b.proteins_tubeThickness/2,g.scale(h,[j,j,j]),a.setMatrixUniforms(h),a.drawElements(a.TRIANGLES,a.sphereBuffer.vertexIndexBuffer.numItems,a.UNSIGNED_SHORT,0)}})(ChemDoodle.extensions,ChemDoodle.RESIDUE,ChemDoodle.structures,ChemDoodle.structures.d3,Math,ChemDoodle.lib.mat4,ChemDoodle.lib.vec3,
-ChemDoodle.math);
-(function(b){b.UnitCell=function(b){this.unitCell=b;var o=[],l=[],h=function(b,a,d,g){o.push(b[0],b[1],b[2]);o.push(a[0],a[1],a[2]);o.push(d[0],d[1],d[2]);o.push(g[0],g[1],g[2]);for(b=0;4>b;b++)l.push(0,0,0)};h(b.o,b.x,b.xy,b.y);h(b.o,b.y,b.yz,b.z);h(b.o,b.z,b.xz,b.x);h(b.yz,b.y,b.xy,b.xyz);h(b.xyz,b.xz,b.z,b.yz);h(b.xy,b.x,b.xz,b.xyz);b=[];for(h=0;6>h;h++){var g=4*h;b.push(g,g+1,g+1,g+2,g+2,g+3,g+3,g)}this.storeData(o,l,b)};(b.UnitCell.prototype=new b._Mesh).render=function(b,o){b.setMatrixUniforms(b.modelViewMatrix);this.bindBuffers(b);
-b.material.setDiffuseColor(o.shapes_color);b.lineWidth(o.shapes_lineWidth);b.drawElements(b.LINES,this.vertexIndexBuffer.numItems,b.UNSIGNED_SHORT,0)}})(ChemDoodle.structures.d3,ChemDoodle.lib.vec3);
-(function(b,j,o){b.Plate=function(b){this.lanes=Array(b);i=0;for(ii=b;iA[p].atoms.length)f=A[p];A=!1;p=0;for(q=this.rings.length;pF.x;)F.x++;for(;1<=F.y;)F.y--;for(;0>F.y;)F.y++;for(;1<=F.z;)F.z--;for(;0>
-F.z;)F.z++}J=[];H=0;for(E=k.length;Hc.atoms[I].distance3D(F)){G=!0;break}if(!G){I=0;for(C=J.length;IJ[I].distance3D(F)){G=!0;break}G||J.push(F)}}c.atoms=c.atoms.concat(J)}E=[];for(H=0;Hq&&-4=n.start&&e<=n.end){g?a.helix=!0:a.sheet=!0;e===n.end&&(a.arrow=!0);break}}}o.PDBInterpreter=function(){};var d=o.PDBInterpreter.prototype=new o._Interpreter;d.calculateRibbonDistances=!1;d.deduceResidueBonds=!1;d.read=function(d,n){var p=new l.Molecule;p.chains=[];if(!d)return p;var q=d.split("\n");n||(n=1);for(var o=[],r=[],v,u=[],t=[],w=[],x=0,B=q.length;xk;k++){var G=new l.Residue(-1);G.cp1=C;G.cp2=C;u.push(G)}D!==Number.NaN&&u[u.length-1].resSeq!==D&&(k=new l.Residue(D),k.name=g(c.substring(17,20)),3===k.name.length?k.name=k.name.substring(0,1)+k.name.substring(1).toLowerCase():2===k.name.length&&"D"===k.name.charAt(0)&&(k.name=k.name.substring(1)),u.push(k),G=c.substring(21,22),a(k,o,G,D,!0),
-a(k,r,G,D,!1));c=g(c.substring(12,16));k=u[u.length-1];if("CA"===c||"P"===c||"O5'"===c)k.cp1||(k.cp1=C);else if("N3"===c&&("C"===k.name||"U"===k.name||"T"===k.name)||"N1"===c&&("A"===k.name||"G"===k.name))k.cp3=C;else if("C2"===c)k.cp4=C;else if("C4"===c&&("C"===k.name||"U"===k.name||"T"===k.name)||"C6"===c&&("A"===k.name||"G"===k.name))k.cp5=C;else if("O"===c||"C6"===c&&("C"===k.name||"U"===k.name||"T"===k.name)||"N9"===c){if(!u[u.length-1].cp2){if("C6"===c||"N9"===c)v=C;k.cp2=C}}else"C"===c&&(v=
-C)}}else if(j.stringStartsWith(c,"HETATM"))k=g(c.substring(76,78)),0===k.length&&(k=g(c.substring(12,16))),1D;D++)if(k=
-g(c.substring(11+5*D,16+5*D)),0!==k.length&&(k=parseInt(k),w[k])){for(var G=w[k],E=!1,k=0,H=p.bonds.length;ke;e++)g=new l.Residue(-1),g.cp1=d,g.cp2=b[b.length-1].cp2,b.push(g);a.chains.push(b)}};d.calculateDistances=function(a,b){for(var d=[],
-e=0,g=a.atoms.length;eE.length&&b.metadata.push(E),
-d=[G],j.stringStartsWith(E,"##TITLE\x3d"))b.title=E.substring(8).trim();else if(j.stringStartsWith(E,"##XUNITS\x3d"))b.xUnit=E.substring(9).trim(),this.convertHZ2PPM&&"HZ"===b.xUnit.toUpperCase()&&(b.xUnit="PPM",k=!0);else if(j.stringStartsWith(E,"##YUNITS\x3d"))b.yUnit=E.substring(9).trim();else if(!j.stringStartsWith(E,"##XYPAIRS\x3d"))if(j.stringStartsWith(E,"##FIRSTX\x3d"))q=parseFloat(E.substring(9).trim());else if(j.stringStartsWith(E,"##LASTX\x3d"))p=parseFloat(E.substring(8).trim());else if(j.stringStartsWith(E,
-"##FIRSTY\x3d"))o=parseFloat(E.substring(9).trim());else if(j.stringStartsWith(E,"##NPOINTS\x3d"))r=parseFloat(E.substring(10).trim());else if(j.stringStartsWith(E,"##XFACTOR\x3d"))v=parseFloat(E.substring(10).trim());else if(j.stringStartsWith(E,"##YFACTOR\x3d"))u=parseFloat(E.substring(10).trim());else if(j.stringStartsWith(E,"##DELTAX\x3d"))w=parseFloat(E.substring(9).trim());else if(j.stringStartsWith(E,"##.OBSERVE FREQUENCY\x3d"))this.convertHZ2PPM&&(t=parseFloat(E.substring(21).trim()));else if(j.stringStartsWith(E,
-"##.SHIFT REFERENCE\x3d"))this.convertHZ2PPM&&(B=E.substring(19).split(","),x=parseInt(B[2].trim()),B=parseFloat(B[3].trim()));else if(j.stringStartsWith(E,"##XYDATA\x3d")){k||(t=1);var H=c=!1,G=E.split("\n"),I=(p-q)/(r-1);if(-1!==w)for(var E=1,M=G.length;E]+/;b.assignments=[];E=1;for(M=G.length;Ea.p.distance(b)){d=0;for(f=b.assigned.as.length;dMath.abs(this.spectrum.getTransformedX(b.x,this.specs,this.spectrum.memory.width,this.spectrum.memory.offsetLeft)-a.p.x)){d=0;for(e=b.as.length;d=this.spectrum.memory.offsetLeft&&bh;h++)for(var f=0,j=a[h].length;fthis.width||a>this.height)this.specs.scale=0.85*
-e.min(this.width/b,this.height/a)};f.bondExists=function(a,b){for(var d=0,e=this.molecules.length;db-r.lastTouch?r.dbltap?(r.prehandleEvent(a),
-r.dbltap(a)):r.dblclick?(r.prehandleEvent(a),r.dblclick(a)):r.touchstart?(r.prehandleEvent(a),r.touchstart(a)):r.mousedown&&(r.prehandleEvent(a),r.mousedown(a)):r.touchstart?(r.prehandleEvent(a),r.touchstart(a),this.hold&&clearTimeout(this.hold),this.touchhold&&(this.hold=setTimeout(function(){r.touchhold(a)},1E3))):r.mousedown&&(r.prehandleEvent(a),r.mousedown(a));r.lastTouch=b}),f.bind("touchmove",function(a){this.hold&&(clearTimeout(this.hold),this.hold=void 0);if(!j.supports_gesture()&&2===a.originalEvent.touches.length&&
-r.gesturechange){var b=a.originalEvent.touches,d=new h.Point(b[0].pageX,b[0].pageY),f=new h.Point(b[1].pageX,b[1].pageY),b=d.distance(f),d=d.angle(f);a.originalEvent.scale=b/r.implementedGestureDist;a.originalEvent.rotation=180*(r.implementedGestureAngle-d)/e.PI;r.prehandleEvent(a);r.gesturechange(a)}if(1(new Date).getTime()-r.lastTouch&&(r.tap?(r.prehandleEvent(a),r.tap(a)):r.click&&(r.prehandleEvent(a),r.click(a)))}),f.bind("gesturestart",function(a){r.gesturestart&&(r.prehandleEvent(a),r.gesturestart(a))}),f.bind("gesturechange",function(a){r.gesturechange&&(r.prehandleEvent(a),r.gesturechange(a))}),f.bind("gestureend",function(a){r.gestureend&&(r.prehandleEvent(a),r.gestureend(a))})):(f.click(function(a){switch(a.which){case 1:r.click&&(r.prehandleEvent(a),r.click(a));break;
-case 2:r.middleclick&&(r.prehandleEvent(a),r.middleclick(a));break;case 3:r.rightclick&&(r.prehandleEvent(a),r.rightclick(a))}}),f.dblclick(function(a){r.dblclick&&(r.prehandleEvent(a),r.dblclick(a))}),f.mousedown(function(a){switch(a.which){case 1:l.CANVAS_DRAGGING=r;r.mousedown&&(r.prehandleEvent(a),r.mousedown(a));break;case 2:r.middlemousedown&&(r.prehandleEvent(a),r.middlemousedown(a));break;case 3:r.rightmousedown&&(r.prehandleEvent(a),r.rightmousedown(a))}}),f.mousemove(function(a){!l.CANVAS_DRAGGING&&
-r.mousemove&&(r.prehandleEvent(a),r.mousemove(a))}),f.mouseout(function(a){l.CANVAS_OVER=void 0;r.mouseout&&(r.prehandleEvent(a),r.mouseout(a))}),f.mouseover(function(a){l.CANVAS_OVER=r;r.mouseover&&(r.prehandleEvent(a),r.mouseover(a))}),f.mouseup(function(a){switch(a.which){case 1:r.mouseup&&(r.prehandleEvent(a),r.mouseup(a));break;case 2:r.middlemouseup&&(r.prehandleEvent(a),r.middlemouseup(a));break;case 3:r.rightmouseup&&(r.prehandleEvent(a),r.rightmouseup(a))}}),f.mousewheel(function(a,b){r.mousewheel&&
-(r.prehandleEvent(a),r.mousewheel(a,b))}));this.subCreate&&this.subCreate()};f.prehandleEvent=function(a){a.originalEvent.changedTouches&&(a.pageX=a.originalEvent.changedTouches[0].pageX,a.pageY=a.originalEvent.changedTouches[0].pageY);a.preventDefault();a.offset=g("#"+this.id).offset();a.p=new h.Point(a.pageX-a.offset.left,a.pageY-a.offset.top)}})(ChemDoodle,ChemDoodle.featureDetection,ChemDoodle.math,ChemDoodle.monitor,ChemDoodle.structures,ChemDoodle.lib.jQuery,Math,document,window,navigator.userAgent);
-(function(b,j){b._AnimatorCanvas=function(b,h,g){b&&this.create(b,h,g)};var o=b._AnimatorCanvas.prototype=new b._Canvas;o.timeout=33;o.startAnimation=function(){this.stopAnimation();this.lastTime=(new Date).getTime();var b=this;this.nextFrame&&(this.handle=j.requestInterval(function(){var h=(new Date).getTime();b.nextFrame(h-b.lastTime);b.repaint();b.lastTime=h},this.timeout))};o.stopAnimation=function(){this.handle&&(j.clearRequestInterval(this.handle),this.handle=void 0)};o.isRunning=function(){return void 0!==
-this.handle}})(ChemDoodle,ChemDoodle.animations);
-(function(b,j){b.FileCanvas=function(b,l,h,g){b&&this.create(b,l,h);j.writeln('\x3cbr\x3e\x3cform name\x3d"FileForm" enctype\x3d"multipart/form-data" method\x3d"POST" action\x3d"'+g+'" target\x3d"HiddenFileFrame"\x3e\x3cinput type\x3d"file" name\x3d"f" /\x3e\x3cinput type\x3d"submit" name\x3d"submitbutton" value\x3d"Show File" /\x3e\x3c/form\x3e\x3ciframe id\x3d"HFF-'+b+'" name\x3d"HiddenFileFrame" height\x3d"0" width\x3d"0" style\x3d"display:none;" onLoad\x3d"GetMolFromFrame(\'HFF-'+b+"', "+b+')"\x3e\x3c/iframe\x3e');
-this.emptyMessage="Click below to load file";this.repaint()};b.FileCanvas.prototype=new b._Canvas})(ChemDoodle,document);
-(function(b){b.HyperlinkCanvas=function(b,o,l,h,g,e){b&&this.create(b,o,l);this.urlOrFunction=h;this.color=g?g:"blue";this.size=e?e:2};b=b.HyperlinkCanvas.prototype=new b._Canvas;b.openInNewWindow=!0;b.hoverImage=void 0;b.drawChildExtras=function(b){this.e&&(this.hoverImage?b.drawImage(this.hoverImage,0,0):(b.strokeStyle=this.color,b.lineWidth=2*this.size,b.strokeRect(0,0,this.width,this.height)))};b.setHoverImage=function(b){this.hoverImage=new Image;this.hoverImage.src=b};b.click=function(){this.e=
-void 0;this.repaint();this.urlOrFunction instanceof Function?this.urlOrFunction():this.openInNewWindow?window.open(this.urlOrFunction):location.href=this.urlOrFunction};b.mouseout=function(){this.e=void 0;this.repaint()};b.mouseover=function(b){this.e=b;this.repaint()}})(ChemDoodle);
-(function(b,j,o,l){b.MolGrabberCanvas=function(b,g,e){b&&this.create(b,g,e);g=[];g.push('\x3cbr\x3e\x3cinput type\x3d"text" id\x3d"');g.push(b);g.push('_query" size\x3d"32" value\x3d"" /\x3e');g.push("\x3cbr\x3e\x3cnobr\x3e");g.push('\x3cselect id\x3d"');g.push(b);g.push('_select"\x3e');g.push('\x3coption value\x3d"chemexper"\x3eChemExper');g.push('\x3coption value\x3d"chemspider"\x3eChemSpider');g.push('\x3coption value\x3d"pubchem" selected\x3ePubChem');g.push("\x3c/select\x3e");g.push('\x3cbutton id\x3d"');
-g.push(b);g.push('_submit"\x3eShow Molecule\x3c/button\x3e');g.push("\x3c/nobr\x3e");l.getElementById(b);o("#"+b).after(g.join(""));var a=this;o("#"+b+"_submit").click(function(){a.search()});o("#"+b+"_query").keypress(function(b){13===b.which&&a.search()});this.emptyMessage="Enter search term below";this.repaint()};b=b.MolGrabberCanvas.prototype=new b._Canvas;b.setSearchTerm=function(b){o("#"+this.id+"_query").val(b);this.search()};b.search=function(){this.emptyMessage="Searching...";this.clear();
-var b=this;j.getMoleculeFromDatabase(o("#"+this.id+"_query").val(),{database:o("#"+this.id+"_select").val()},function(g){b.loadMolecule(g)})}})(ChemDoodle,ChemDoodle.iChemLabs,ChemDoodle.lib.jQuery,document);
-(function(b,j,o){var l=[],h=[1,0,0],g=[0,1,0],e=[0,0,1];b.RotatorCanvas=function(a,b,e,f){a&&this.create(a,b,e);this.rotate3D=f};b=b.RotatorCanvas.prototype=new b._AnimatorCanvas;j=j.PI/15;b.xIncrement=j;b.yIncrement=j;b.zIncrement=j;b.nextFrame=function(a){if(0===this.molecules.length&&0===this.shapes.length)this.stopAnimation();else if(a/=1E3,this.rotate3D){o.identity(l);o.rotate(l,this.xIncrement*a,h);o.rotate(l,this.yIncrement*a,g);o.rotate(l,this.zIncrement*a,e);a=0;for(var b=this.molecules.length;a<
-b;a++){for(var j=this.molecules[a],f=0,y=j.atoms.length;fthis.frames.length-1&&(this.curIndex=0);this.alpha=1;var b=this.frames[this.curIndex];this.loadContent(b.mols,b.shapes);this.phase=1;var h=this,g=1;this.innerHandle=setInterval(function(){h.alpha=(15-g)/15;h.repaint();15===g&&h.breakInnerHandle();g++},33)}else 1===
-this.phase&&(this.alpha=0,this.repaint())};b.addFrame=function(b,h){0===this.frames.length&&this.loadContent(b,h);this.frames.push({mols:b,shapes:h})}})(ChemDoodle,ChemDoodle.animations,ChemDoodle.math);
-(function(b,j,o,l,h){b.TransformCanvas=function(b,e,a,d){b&&this.create(b,e,a);this.rotate3D=d};b=b.TransformCanvas.prototype=new b._Canvas;b.lastPoint=void 0;b.rotationMultMod=1.3;b.lastPinchScale=1;b.lastGestureRotate=0;b.mousedown=function(b){this.lastPoint=b.p};b.dblclick=function(){this.center();this.repaint()};b.drag=function(b){if(!this.lastPoint.multi){if(j.ALT){var e=new o.Point(b.p.x,b.p.y);e.sub(this.lastPoint);for(var a=0,d=this.molecules.length;athis.specs.scale&&(this.specs.scale=0.01);this.repaint()};b.multitouchmove=function(b,e){if(2===e)if(this.lastPoint.multi){var a=new o.Point(b.p.x,b.p.y);a.sub(this.lastPoint);for(var d=0,h=this.molecules.length;dthis.specs.scale&&(this.specs.scale=0.01),this.lastPinchScale=b.originalEvent.scale);if(0!==this.lastGestureRotate-b.originalEvent.rotation){for(var e=(this.lastGestureRotate-b.originalEvent.rotation)/180*l.PI,a=new o.Point(this.width/2,this.height/2),d=0,h=this.molecules.length;d<
-h;d++){for(var f=this.molecules[d],j=0,p=f.atoms.length;jg%10?b.lineTo(g,o.round(this.height/
-2)):b.moveTo(g,o.round(this.height/2));b.stroke()}};l.mousewheel=function(b,g){this.specs.scale+=g/10;0.01>this.specs.scale&&(this.specs.scale=0.01);this.repaint()};l.dblclick=function(){this.spectrum.setup();this.specs.scale=1;this.repaint()};l.multitouchmove=function(h,g){2===g&&(!this.dragRange||!this.dragRange.multi?(this.dragRange=new b.structures.Point(h.p.x,h.p.x),this.dragRange.multi=!0):(this.spectrum.translate(h.p.x-this.dragRange.x,this.width),this.dragRange.x=h.p.x,this.dragRange.y=h.p.x,
-this.repaint()))};l.gesturechange=function(b){this.specs.scale*=b.originalEvent.scale/this.lastPinchScale;0.01>this.specs.scale&&(this.specs.scale=0.01);this.lastPinchScale=b.originalEvent.scale;this.repaint()};l.gestureend=function(){this.lastPinchScale=1}})(ChemDoodle,ChemDoodle.monitor,Math);
-(function(b,j,o){b.SeekerCanvas=function(b,g,e,a){b&&this.create(b,g,e);this.seekType=a};var l=b.SeekerCanvas.prototype=new b._SpectrumCanvas;l.superRepaint=l.innerRepaint;l.innerRepaint=function(h){this.superRepaint(h);if(this.spectrum&&0this.width-2&&(a-=6+d);g=g.y;0>g-this.specs.text_font_size-2&&(g+=this.specs.text_font_size);h.fillRect(a,g-this.specs.text_font_size,d,this.specs.text_font_size);h.fillStyle="black";h.fillText(e,a,g)}};l.mouseout=function(){this.p=void 0;this.repaint()};l.mousemove=function(b){this.p={x:b.p.x-
-2,y:b.p.y-3};this.repaint()};l.touchstart=function(b){this.mousemove(b)};l.touchmove=function(b){this.mousemove(b)};l.touchend=function(b){this.mouseout(b)};b.SeekerCanvas.SEEK_POINTER="pointer";b.SeekerCanvas.SEEK_PLOT="plot";b.SeekerCanvas.SEEK_PEAK="peak"})(ChemDoodle,ChemDoodle.extensions,Math);
-(function(b,j,o,l,h,g,e,a,d,n,f,y){b._Canvas3D=function(a,b,d){a&&this.create(a,b,d)};var p=b._Canvas3D.prototype=new b._Canvas;p.rotationMatrix=void 0;p.translationMatrix=void 0;p.lastPoint=void 0;p.emptyMessage="WebGL is Unavailable!";p.lastPinchScale=1;p.lastGestureRotate=0;p.afterLoadContent=function(){for(var a=new o.Bounds,b=0,g=this.molecules.length;bj&&(b/=j);this.specs.projectionOrthoWidth_3D=2*(Math.tan(b/360*Math.PI)*this.depth)*j;this.specs.projectionPerspectiveVerticalFieldOfView_3D=b;this.specs.projectionFrontCulling_3D=g;this.specs.projectionBackCulling_3D=h;this.specs.projectionWidthHeightRatio_3D=j;this.translationMatrix=d.translate(d.identity([]),[0,0,-this.depth]);this.maxDimension=e.max(a.maxX-
-a.minX,a.maxY-a.minY);this.setupScene()};p.setViewDistance=function(a){this.specs.projectionPerspectiveVerticalFieldOfView_3D=o.clamp(this.specs.projectionPerspectiveVerticalFieldOfView_3D/a,0.1,179.9);this.specs.projectionOrthoWidth_3D=2*(e.tan(this.specs.projectionPerspectiveVerticalFieldOfView_3D/360*Math.PI)*this.depth)*this.specs.projectionWidthHeightRatio_3D;this.updateScene()};p.repaint=function(){if(this.gl){this.gl.clear(this.gl.COLOR_BUFFER_BIT|this.gl.DEPTH_BUFFER_BIT);this.gl.modelViewMatrix=
-d.multiply(this.translationMatrix,this.rotationMatrix,[]);this.gl.rotationMatrix=this.rotationMatrix;var a=this.gl.getUniformLocation(this.gl.program,"u_projection_matrix");this.gl.uniformMatrix4fv(a,!1,this.gl.projectionMatrix);this.gl.fogging.setMode(this.specs.fog_mode_3D);for(var b=0,e=this.molecules.length;be.PI/2&&(t[x].guidePointsSmall.reverse(),
-t[x].guidePointsLarge.reverse(),f.scale(t[x].D,-1))}x=2;for(B=t.length-3;xd?0.1:179.9b?0.1:179.9this.frameNumber&&(this.frameNumber=1,this.reverse=!1)):(this.frameNumber++,this.frameNumber>=this.frames.length&&(2===this.playMode?(this.frameNumber-=2,this.reverse=!0):(this.frameNumber=0,0===this.playMode&&this.stopAnimation())))};o.center=function(){for(var b=new j.Atom,h=this.frames[0],g=0,e=h.mols.length;gf.atomicNumber||
-71f.atomicNumber||103f.atomicNumber)this.cells.push(new l(f,g,e,this.cellDimension)),g+=this.cellDimension,a++}e+=2*this.cellDimension;g=3*this.cellDimension+this.padding;for(d=57;104>d;d++)if(f=b.ELEMENT[b.SYMBOLS[d]],90===f.atomicNumber&&(e+=this.cellDimension,g=3*this.cellDimension+this.padding),58<=f.atomicNumber&&71>=f.atomicNumber||90<=f.atomicNumber&&103>=f.atomicNumber)this.cells.push(new l(f,g,e,this.cellDimension)),g+=this.cellDimension};h.drawCell=
-function(b,e,a){var d=b.createRadialGradient(a.x+a.dimension/3,a.y+a.dimension/3,1.5*a.dimension,a.x+a.dimension/3,a.y+a.dimension/3,a.dimension/10);d.addColorStop(0,"#000000");d.addColorStop(0.7,a.element.jmolColor);d.addColorStop(1,"#FFFFFF");b.fillStyle=d;j.contextRoundRect(b,a.x,a.y,a.dimension,a.dimension,a.dimension/8);if(a===this.hovered||a===this.selected||a.selected)b.lineWidth=2,b.strokeStyle="#c10000",b.stroke(),b.fillStyle="white";b.fill();b.font=j.getFontString(e.text_font_size,e.text_font_families);
-b.fillStyle=e.text_color;b.textAlign="center";b.textBaseline="middle";b.fillText(a.element.symbol,a.x+a.dimension/2,a.y+a.dimension/2)};h.click=function(){this.hovered&&(this.allowMultipleSelections?this.hovered.selected=!this.hovered.selected:this.selected=this.hovered,this.repaint())};h.touchstart=function(b){this.mousemove(b)};h.mousemove=function(b){var e=b.p.x;b=b.p.y;this.hovered=void 0;for(var a=0,d=this.cells.length;a this.notificationManager.showNotification(e.detail));
-
- // Shortcuts for common notifications
- this.addEventListener(NotificationUtils.NOTIFY_INFO, e => this.notificationManager.info(e.detail.title, e.detail.message));
- this.addEventListener(NotificationUtils.NOTIFY_SUCCESS, e => this.notificationManager.success(e.detail.title, e.detail.message));
- this.addEventListener(NotificationUtils.NOTIFY_WARNING, e => this.notificationManager.warning(e.detail.title, e.detail.message));
- this.addEventListener(NotificationUtils.NOTIFY_ERROR, e => this.notificationManager.error(e.detail.title, e.detail.message));
-
- // Notify a response
- this.addEventListener(NotificationUtils.NOTIFY_RESPONSE, e => this.notificationManager.response(e.detail));
-
- // keeps track of the executedQueries transitioning from browser tool to facet tool
- this.queries = [];
- // keeps track of status and version of the hosts (opencga and cellbase)
- this.host = {};
- globalThis.addEventListener("signingIn", e => {
- this.signingIn = e.detail.value;
- this.requestUpdate();
- }, false);
-
- globalThis.addEventListener("signingInError", e => {
- this.notificationManager.error("Signing in error", e.detail.value);
- }, false);
-
- globalThis.addEventListener("hostInit", e => {
- this.host = {...this.host, [e.detail.host]: e.detail.value};
- this.requestUpdate();
- }, false);
- }
-
- connectedCallback() {
- super.connectedCallback();
-
- // Import server configuration from conf/server.json file (if exists)
- // See issue https://github.com/opencb/jsorolla/issues/425
- UtilsNew.importJSONFile("conf/server.json").then(serverConf => {
- // Initialize opencga configuration
- const opencgaHost = serverConf?.host || this.config.opencga.host;
- const opencgaVersion = serverConf?.version || this.config.opencga.version;
- const opencgaPrefix = serverConf?.cookie?.prefix || this.config.opencga.cookie.prefix;
- // console.log(opencgaHost, opencgaVersion);
-
- // Initialise clients and create the session
- const sid = Cookies.get(opencgaPrefix + "_sid");
- const userId = Cookies.get(opencgaPrefix + "_userId");
- this.opencgaClient = new OpenCGAClient({
- host: opencgaHost,
- version: opencgaVersion,
- token: sid,
- userId: userId,
- cookies: {
- active: true,
- prefix: opencgaPrefix,
- },
- });
-
- if (sid) {
- this.checkSessionActive();
- this.intervalCheckSession = setInterval(this.checkSessionActive.bind(this), this.config.session.checkTime);
- this._createOpenCGASession();
- } else {
- this._createOpencgaSessionFromConfig();
- }
- });
- }
-
- updated(changedProperties) {
- if (changedProperties.has("opencgaSession")) {
- this.opencgaSessionObserver();
- }
- }
-
- opencgaSessionObserver() {
- this.renderHashFragments();
- this.queries = {};
- this.requestUpdate();
- }
-
- async _createOpenCGASession() {
- // This check prevents displaying the annoying message of 'No valid token:null' when the token has expired
- if (!this.opencgaClient._config.token) {
- return;
- }
- this.signingIn = "Creating session..";
- this.requestUpdate();
- await this.updateComplete;
- this.opencgaClient.createSession()
- .then(response => {
- const _response = response;
- console.log("_createOpenCGASession", response);
- // check if project array has been defined in the config.js
- if (UtilsNew.isNotEmptyArray(this.config.opencga.projects)) {
- // We store the project and study ids the user needs to visualise (defined in the config.js)
- const configProjects = {};
- for (let i = 0; i < this.config.opencga.projects.length; i++) {
- configProjects[this.config.opencga.projects[i].id] = [];
-
- for (let j = 0; j < this.config.opencga.projects[i].studies.length; j++) {
- configProjects[this.config.opencga.projects[i].id].push(
- this.config.opencga.projects[i].studies[j].id
- );
- }
- }
-
- // We must keep only the projects defined in the configuration file
- const activeProjects = [];
- for (let i = 0; i < response.projects.length; i++) {
- if (response.projects[i].id in configProjects) {
- const project = response.projects[i];
- const activeStudies = [];
- for (let j = 0; j < project.studies.length; j++) {
- const study = project.studies[j];
- if (configProjects[project.id].indexOf(study.id) > -1) {
- activeStudies.push(study);
- }
- }
-
- // We replace the studies obtained with the ones from the configuration file
- project.studies = activeStudies;
- activeProjects.push(project);
- }
- }
-
- // TODO we must query projects/info URL to get the whole object
- _response.projects = activeProjects || [];
- if (UtilsNew.isNotEmptyArray(response.projects[0].studies)) {
- _response.project = response.projects[0];
- _response.study = response.projects[0].studies[0];
- }
- }
- // this forces the observer to be executed.
- this.opencgaSession = {..._response};
- this.opencgaSession.mode = this.config.mode;
- this.config = {...this.config};
- })
- .catch(e => {
- console.error(e);
- this.notificationManager.error("Error creating session", e.message);
- }).finally(() => {
- this.signingIn = false;
- this.requestUpdate();
- });
- }
-
- // TODO turn this into a Promise
- _createOpencgaSessionFromConfig() {
- // Create a private opencga-session to avoid calling to the Observer
- const opencgaSession = this.opencgaClient.createAnonymousSession();
-
- // If 'config.opencga.anonymous' exists and contains either 'user' or 'projects'
- if (UtilsNew.isNotUndefinedOrNull(this.config.opencga.anonymous) && Object.keys(this.config.opencga.anonymous).length > 0) {
- // If 'projects' is defined we only load those projects
- if (UtilsNew.isNotUndefinedOrNull(this.config.opencga.anonymous.projects)) {
- if (this.config.opencga.anonymous.projects.length > 0) {
- // TODO we must query projects/info URL to get the whole object
- opencgaSession.projects = this.config.opencga.anonymous.projects;
- if (UtilsNew.isNotEmptyArray(opencgaSession.projects[0].studies)) {
- opencgaSession.project = opencgaSession.projects[0];
- opencgaSession.study = opencgaSession.projects[0].studies[0];
- }
- }
-
- // This triggers the event and call to opencgaSessionObserver
- this.opencgaSession = opencgaSession;
- } else {
- // When no 'projects' is defined we fetch all public projects
- if (UtilsNew.isNotUndefinedOrNull(this.config.opencga.anonymous.user)) {
- this.opencgaClient.users().projects(this.config.opencga.anonymous.user, {})
- .then(restResponse => {
- // _this._setup(_projects);
-
- opencgaSession.projects = restResponse.response[0].result;
- if (UtilsNew.isNotEmptyArray(opencgaSession.projects) && UtilsNew.isNotEmptyArray(opencgaSession.projects[0].studies)) {
- // this sets the current active project and study
- opencgaSession.project = opencgaSession.projects[0];
- opencgaSession.study = opencgaSession.projects[0].studies[0];
- }
-
- // This triggers the event and call to opencgaSessionObserver
- this.opencgaSession = opencgaSession;
- })
- .catch(function (response) {
- console.log("An error when getting projects");
- console.log(response);
- });
- }
- }
- } else {
- // This triggers the event and call to opencgaSessionObserver
- this.opencgaSession = opencgaSession;
- }
- }
-
- onLogin(credentials) {
- // This creates a new authenticated opencga-session object
- this.opencgaClient._config.token = credentials.detail.token;
- this._createOpenCGASession();
-
- if (this.tool === "#login") {
- this.tool = "#home";
- this.app = this.getActiveAppConfig();
- }
-
- // 60000 ms = 1 min. Every 1 min we check if session is close to expire.
- this.intervalCheckSession = setInterval(this.checkSessionActive.bind(this), this.config.session.checkTime);
- }
-
- refresh() {
- this.opencgaClient.refresh();
- }
-
- async logout() {
- // this delete token in the client and removes the Cookies
- await this.opencgaClient.logout();
- this._createOpencgaSessionFromConfig();
-
- this.tool = "#home";
- this.app = this.getActiveAppConfig();
- window.location.hash = "home";
- window.clearInterval(this.intervalCheckSession);
- }
-
- checkSessionActive() {
- // We check if refresh token has updated session id cookie
- // let sid = Cookies.get(this.config.opencga.cookie.prefix + "_sid");
-
- if (UtilsNew.isNotUndefinedOrNull(this.opencgaClient._config.token)) { // UtilsNew.isNotEmpty(this.opencgaSession.token) &&
- // this.token = sid;
- const decoded = jwt_decode(this.opencgaClient._config.token);
- const currentTime = new Date().getTime();
- const remainingTime = ((decoded.exp * 1000) - currentTime);
- // 600000 ms = 10 min = 1000(1sec) * 60(60 sec = 1min) * 10(10 min)
- if (remainingTime <= this.config.session.maxRemainingTime && remainingTime >= this.config.session.minRemainingTime) {
- const remainingMinutes = Math.floor(remainingTime / this.config.session.minRemainingTime);
-
- // Handle session refresh
- const handleSessionRefresh = () => {
- this.opencgaClient.refresh().then(response => {
- const sessionId = response.getResult(0).token;
- const decoded = jwt_decode(sessionId);
- const dateExpired = new Date(decoded.exp * 1000);
- const validTimeSessionId = moment(dateExpired, "YYYYMMDDHHmmss").format("D MMM YY HH:mm:ss");
-
- // Display confirmation message
- this.notificationManager.success(null, `Your session is now valid until ${validTimeSessionId}.`);
- });
- };
-
- // Display expiration notification
- this.notificationManager.showNotification({
- type: "warning",
- display: {
- showIcon: true,
- showCloseButton: true,
- },
- title: "Your session is close to expire",
- message: `
- In ${remainingMinutes} minutes your session will be automatically closed.
- To keep working, please click on Refresh Session button.
- `,
- removeAfter: 20000,
- buttons: [
- {
- text: "Refresh session",
- onClick: () => handleSessionRefresh(),
- removeOnClick: true,
- }
- ]
- });
- } else {
- if (remainingTime < this.config.session.minRemainingTime) {
- this.logout();
- window.clearInterval(this.intervalCheckSession);
- // Display notification message
- this.notificationManager.info(null, "Your session has expired");
- }
- }
- }
- }
-
- changeTool(e) {
- e.preventDefault();
- const target = e.currentTarget;
- $(".navbar-zetta ul > li", this).removeClass("active");
- $(target).parent("li").addClass("active");
- if ($(target).closest("ul").hasClass("dropdown-menu")) {
- $(target).closest("ul").closest("li").addClass("active");
- }
-
- if (UtilsNew.isNotUndefined(e)) {
- e.preventDefault();
- }
-
- if (UtilsNew.isNotUndefined(target) && UtilsNew.isNotUndefined(target.attributes.href)) {
- this.tool = target.attributes.href.value;
- } else {
- this.tool = "#home";
- }
- this.renderHashFragments();
- }
-
- renderHashFragments() {
- console.log("renderHashFragments - DEBUG", this.tool);
- let hashFrag = this.tool;
- if (this.opencgaSession?.project?.alias) {
-
- hashFrag += "/" + this.opencgaSession.project.id;
- if (UtilsNew.isNotUndefined(this.opencgaSession.study) && UtilsNew.isNotEmpty(this.opencgaSession.study.alias)) {
- hashFrag += "/" + this.opencgaSession.study.id;
- }
- }
-
- if (window.location.hash === hashFrag) {
- // debugger
- this.hashFragmentListener(this);
- } else {
- // debugger
- window.location.hash = hashFrag;
- }
- }
-
- route(e) {
- this.tool = e.detail.hash;
- if (e.detail?.resource) {
- this.queries = {...this.queries, [e.detail.resource]: e.detail?.query};
- }
- this.renderHashFragments();
- }
-
- hashFragmentListener(ctx) {
- console.log("hashFragmentListener - DEBUG", this.tool);
- // Hide all elements
- for (const element in this.config.enabledComponents) {
- if (UtilsNew.isNotUndefined(this.config.enabledComponents[element])) {
- this.config.enabledComponents[element] = false;
- }
- }
- let arr = window.location.hash.split("/");
-
- // TODO evaluate refactor
- const [hashTool, hashProject, hashStudy, feature] = arr;
-
- // Stopping the recursive call
- if (hashTool !== this.tool || hashProject !== this.opencgaSession?.project?.id || hashStudy !== this.opencgaSession?.study?.id) {
- if (arr.length > 1) {
- // Field 'project' is being observed, just in case Polymer triggers
- // an unnecessary event we can check they are really different
- if (ctx.opencgaSession?.project?.id !== hashProject) {
- // eslint-disable-next-line no-param-reassign
- ctx.opencgaSession.project.id = hashProject;
- }
- if (ctx.opencgaSession?.study && arr.length > 2 && ctx.opencgaSession.study !== hashStudy) {
- for (let i = 0; i < ctx.opencgaSession.projects.length; i++) {
- if (ctx.opencgaSession.projects[i].name === ctx.opencgaSession.project.name ||
- ctx.opencgaSession.projects[i].id === ctx.opencgaSession.project.id) {
- for (let j = 0; j < ctx.opencgaSession.projects[i].studies.length; j++) {
- if (ctx.opencgaSession.projects[i].studies[j].name === hashStudy || ctx.opencgaSession.projects[i].studies[j].id === hashStudy) {
- ctx.opencgaSession.study = ctx.opencgaSession.projects[i].studies[j];
- break;
- }
- }
- break;
- }
- }
- }
- }
- ctx.tool = hashTool;
- }
-
- const searchArr = window.location.hash.split("?");
- if (searchArr.length > 1) {
- const search = searchArr[1];
- arr = search.split("&");
- const query = {};
- for (let i = 0; i < arr.length; i++) {
- const split = arr[i].split("=");
- query[split[0]] = split[1];
- }
- this.query = query;
- }
-
- const componentName = this.tool.replace("#", "");
- if (UtilsNew.isNotUndefined(this.config.enabledComponents[componentName])) {
- this.config.enabledComponents[componentName] = true;
- } else {
- // If the component does not exist, mark as custom page
- this.config.enabledComponents["customPage"] = true;
- }
-
- this.config = {...this.config};
-
- // TODO quickfix to avoid hash browser scroll
- $("body,html").animate({
- scrollTop: 0
- }, 1);
- }
-
- onStudySelect(e, study, project) {
- e.preventDefault(); // prevents the hash change to "#" and allows to manipulate the hash fragment as needed
- this.changeActiveStudy(study.fqn);
- }
-
- changeActiveStudy(studyFqn) {
- if (this.opencgaSession.study.fqn === studyFqn) {
- console.log("New selected study is already the current active study!");
- return;
- }
-
- // Change active study
- let studyFound = false;
- for (const project of this.opencgaSession.projects) {
- const studyIndex = project.studies.findIndex(s => s.fqn === studyFqn);
- if (studyIndex >= 0) {
- this.opencgaSession.project = project;
- this.opencgaSession.study = project.studies[studyIndex];
- studyFound = true;
- break;
- }
- }
-
- if (studyFound) {
- // Update the lastStudy in config iff has changed
- this.opencgaClient.updateUserConfig("IVA", {...this.opencgaSession.user.configs["IVA"], lastStudy: studyFqn});
-
- // Refresh the session
- this.opencgaSession = {...this.opencgaSession};
- } else {
- // TODO Convert this into a user notification
- console.error("Study not found!");
- }
- }
-
-
- onChangeApp(e, toggle) {
- // If an App ID exists we display the corresponding app. If not we just show the Suite
- if (e.currentTarget.dataset.id) {
- this.app = this.config.apps.find(app => app.id === e.currentTarget.dataset.id);
- } else {
- this.app = this.getActiveAppConfig();
- }
-
- // We only want to toggle when clicked in the sidenav
- if (toggle) {
- this.toggleSideBar(e);
- }
-
- this.changeTool(e);
- this.requestUpdate();
- }
-
- getActiveAppConfig() {
- const visibleApps = this.config.apps.filter(app => app.visibility === "public");
- // If there is only ona visible App we DO NOT need to show the Suite welcome, just the App.
- if (visibleApps.length === 1) {
- return visibleApps[0];
- } else {
- // Render the Suite welcome page.
- return {
- id: this.config.id,
- name: this.config.name,
- welcome: this.config.welcome,
- version: this.config.version,
- logo: this.config.logo,
- about: this.config.about,
- userMenu: this.config.userMenu,
- };
- }
- }
-
- isLoggedIn() {
- return !!this?.opencgaSession?.token;
- }
-
- renderCustomPage() {
- const pageName = this.tool.replace("#", "");
- const page = (this.config.pages || []).find(p => p.url === pageName);
-
- if (page) {
- return html`
-
-
-
- `;
- }
-
- return html`Not found :(`;
- }
-
- render() {
- return html`
-
-
-
- this.logout()}"
- @changeTool="${e => this.changeTool(e.detail.value)}"
- @changeApp="${e => this.onChangeApp(e.detail.event, e.detail.toggle)}"
- @studySelect="${ e => this.onStudySelect(e.detail.event, e.detail.study)}"
- @route="${this.route}">
-
-
-
- ${this.signingIn ? html`
-
-
-
-
- ` : null}
-
-
-
- ${this.config.enabledComponents.home ? html`
-
- this.onChangeApp(e.detail.e, false)}">
-
-
- ` : null}
-
- ${this.config.enabledComponents.customPage ? this.renderCustomPage() : null}
-
- ${this.config.enabledComponents.terms ? html`
-
-
-
- ` : null}
-
- ${this.config.enabledComponents.contact ? html`
-
-
-
- ` : null}
-
- ${this.config.enabledComponents.faq ? html`
-
-
-
- ` : null}
-
- ${this.config.enabledComponents.login ? html`
-
-
-
-
- ` : null}
-
- ${this.config.enabledComponents["rest-api"] ? html`
-
-
-
-
- ` : null}
-
-
-
-
- `;
- }
-
-}
-
-customElements.define("api-app", ApiApp);
diff --git a/src/sites/api/conf/config.js b/src/sites/api/conf/config.js
deleted file mode 100644
index 8ef331d783..0000000000
--- a/src/sites/api/conf/config.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright 2015-2016 OpenCB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-const hosts = [
- {
- id: "prod",
- url: "https://ws.opencb.org/opencga-prod"
- },
- {
- id: "test",
- url: "https://ws.opencb.org/opencga-test"
- }
-];
-
-const opencga = {
- host: hosts[1].url,
- version: "v2",
- cookie: {
- prefix: "iva-" + hosts[1].id
- }
-};
-
-const SUITE = {
- id: "suite",
- name: "OpenCB Suite",
- version: "",
- logo: "img/iva-white.svg",
- companyLogo: "",
- logoAlt: "img/iva.svg",
- footerLogo: "img/opencb-logo.png",
- mode: "development",
- appConfig: "opencb",
- about: {
- dropdown: true,
- links: [
- {id: "code", name: "Source code", url: "https://github.com/opencb/iva", icon: "fa fa-code"},
- {id: "documentation", name: "Documentation", url: "http://docs.opencb.org/display/iva", icon: "fa fa-book"},
- {id: "tutorial", name: "Tutorial", url: "http://docs.opencb.org/display/iva/Tutorials", icon: "fa fa-question-circle"},
- {id: "releases", name: "Releases", url: "https://github.com/opencb/iva/releases", icon: "fa fa-archive"},
- {id: "about", name: "About", url: "#about", icon: "fa fa-info-circle"},
- {id: "terms", name: "Terms", url: "#terms", icon: "fa fa-file-alt"},
- {id: "contact", name: "Contact", url: "#contact", icon: "fa fa-envelope"},
- {id: "faq", name: "FAQ", url: "#faq", icon: "fa fa-question"},
- ]
- },
- userMenu: [
- {id: "account", name: "Your Profile", url: "#account", icon: "fa fa-user", visibility: "private"},
- {id: "projects", name: "Projects", url: "#projects", icon: "fa fa-database", visibility: "private"},
- {id: "file-manager", name: "File Manager", url: "#file-manager", icon: "fas fa-folder-open", visibility: "private"}
- ],
- login: {
- visible: true
- },
- notifyEventMessage: "notifymessage",
- session: {
- checkTime: 60000, // 1 min
- minRemainingTime: 60000,
- maxRemainingTime: 600000 // 10 min
- },
- welcomePage: {
- display: {
- titleStyle: "text-align:center"
- },
- title: "OpenCGA Suite",
- logo: "",
- content: `
-
-
- Welcome to the OpenCB Suite for whole genome variant analysis.
- This interactive tool allows finding genes affected by deleterious variants that segregate along family
- pedigrees, case-controls or sporadic samples.
-
-
-
`,
- },
-
- // The order, title and nested submenus are respected
- apps: [
- {
- id: "Api",
- name: "API",
- icon: "fas fa-cog",
- visibility: "public",
- welcomePage: {
- display: {
- titleStyle: "text-align:center;",
- subtitleStyle: "text-align:center;"
- },
- title: "Documentation APIs",
- subtitle: "Interative documentation api",
- content: `
-
- Welcome to the interative documentation api.
-
- `,
- },
- menu: [{
- id: "rest-api",
- name: "OpenCGA REST API",
- icon: "fas fa-book-open",
- visibility: "public",
- featured: true,
- description: `
- This interactive tool allows explore all services from opencga.
- `,
- }],
- about: {
- dropdown: true,
- links: [
- {id: "code", name: "Source code", url: "https://github.com/opencb/iva", icon: "fa fa-code"},
- {id: "documentation", name: "Documentation", url: "http://docs.opencb.org/display/iva", icon: "fa fa-book"},
- {id: "tutorial", name: "Tutorial", url: "http://docs.opencb.org/display/iva/Tutorials", icon: "fa fa-question-circle"},
- {id: "faq", name: "FAQ", icon: "fa fa-question"}
- ]
- },
- userMenu: [
- {id: "account", name: "Your Profile", url: "#account", icon: "fa fa-user", visibility: "private"},
- {id: "projects", name: "Projects", url: "#projects", icon: "fa fa-database", visibility: "private"},
- {id: "file-manager", name: "File Manager", url: "#file-manager", icon: "fas fa-folder-open", visibility: "private"}
- ]
- }
- ]
-};
-
diff --git a/src/sites/api/conf/pages.js b/src/sites/api/conf/pages.js
deleted file mode 100644
index 4ad38e312e..0000000000
--- a/src/sites/api/conf/pages.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// Define custom pages. See https://github.com/opencb/jsorolla/issues/186
-const CUSTOM_PAGES = [
- {
- url: "about",
- title: "About",
- content: "Content of the About page.",
- },
- {
- url: "contact",
- title: "Contact",
- content: "Content of the Contact page.",
- },
- {
- url: "terms",
- title: "Terms of service",
- content: "Content of the Terms of service page.",
- },
- {
- url: "faq",
- title: "Frequently Asked Questions",
- content: "Content of the FAQ page.",
- },
- {
- url: "getting-started",
- title: "Getting Started",
- content: `
- Login
- Welcome to ....
-
- Menu
- ...
- `,
- },
-];
diff --git a/src/sites/api/favicon.ico b/src/sites/api/favicon.ico
deleted file mode 100644
index 97e6fc47d7..0000000000
Binary files a/src/sites/api/favicon.ico and /dev/null differ
diff --git a/src/sites/api/img/opencb-logo.png b/src/sites/api/img/opencb-logo.png
deleted file mode 100644
index 77b2cba6a6..0000000000
Binary files a/src/sites/api/img/opencb-logo.png and /dev/null differ
diff --git a/src/sites/api/index.html b/src/sites/api/index.html
deleted file mode 100644
index ea8cc338e7..0000000000
--- a/src/sites/api/index.html
+++ /dev/null
@@ -1,135 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/sites/iva/LICENSE b/src/sites/iva/LICENSE
deleted file mode 100644
index 8dada3edaf..0000000000
--- a/src/sites/iva/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright {yyyy} {name of copyright owner}
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/src/sites/iva/README.md b/src/sites/iva/README.md
deleted file mode 100644
index 1e05c875e6..0000000000
--- a/src/sites/iva/README.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# README
-
-## Overview
-
-IVA is a generic Interactive Variant Analysis browser that can be used for the visualization of biological information from various data sources. IVA uses data from [OpenCGA](https://github.com/opencb/opencga) which is an OpenCB project.
-
-### Documentation
-
-You can find IVA documentation and tutorials at: [https://github.com/opencb/iva/wiki](https://github.com/opencb/iva/wiki).
-
-### Issues Tracking
-
-You can report bugs or request new features at [GitHub issue tracking](https://github.com/opencb/iva/issues).
-
-### Release Notes and Roadmap
-
-Releases notes are available at [GitHub releases](https://github.com/opencb/iva/releases).
-
-Roadmap is available at [GitHub milestones](https://github.com/opencb/iva/milestones). You can report bugs or request new features at [GitHub issue tracking](https://github.com/opencb/iva/issues).
-
-### Versioning
-
-IVA is versioned following the rules from [Semantic versioning](http://semver.org/).
-
-### Maintainers
-
-We recommend to contact IVA developers by writing to OpenCB mailing list opencb@googlegroups.com. The main developers and maintainers are:
-
-* Ignacio Medina \(im411@cam.ac.uk\) \(_Founder and Project Leader_\)
-* Javier Perez Florido \(javier.perez.florido.ext@juntadeandalucia.es\)
-* Alexis MartÃnez \(alexis.martinez@juntadeandalucia.es\)
-
-#### Former Contributors
-
-* Swaathi Kandasaamy \(sk913@cam.ac.uk\)
-* Asuncion Gallego \(agallego@cipf.es\)
-
-#### Contributing
-
-IVA is an open-source and collaborative project, currently developement is mainly carried out by Stefan Gräf and Ignacio Medina teams from the University of Cambridge and Joaquin Dopazo team from CIBERER. We appreciate any help and feedback from users, you can contribute in many different ways such as simple bug reporting and feature request. Dependending on your skills you are more than welcome to develop client tools, new features or even fixing bugs.
-
-## How to build
-
-IVA is developed in HTML5, therefore it is mainly developed in JavaScript and makes a heavy usage of HTML and CSS. It uses Grunt as building tool. IVA also requires of OpenCB JSorolla project to be built, this is a JavaScript library developed for several OpenCB web-based projects, this can be found as Git submodule in IVA.
-
-Stable releases are merged and tagged at _master_ branch, you are encourage to use latest stable release for production. Current active development is carried out at _develop_ branch, only building is guaranteed and bugs are expected, use this branch for development or for testing new functionalities. The only dependency of IVA from OpenCB is JSorolla.
-
-### Prerequisites
-
-The following technologies are needed to build IVA: [Node.js](https://nodejs.org/en/), [npm](https://www.npmjs.com/) and [Grunt](http://gruntjs.com/getting-started).
-
-#### Installing Node.js and npm
-
-To install [Node.js](https://nodejs.org/en/) you can visit [this link](http://blog.teamtreehouse.com/install-node-js-npm-linux).
-
-[npm](https://www.npmjs.com/) stands for _node packaged modules_ and it is the dependency manager of [Node.js](https://nodejs.org/en/).
-
-### Cloning
-
-IVA is an open-source project and can be downloaded either as package\(tar.gz\) from GitHub releases or source code by cloning the repository.
-
-Default _**develop**_ branch can be downloaded by executing:
-
-```text
-$ git clone https://github.com/opencb/iva.git
-Cloning into 'iva'...
-remote: Counting objects: 624, done.
-remote: Total 624 (delta 0), reused 0 (delta 0), pack-reused 624
-Receiving objects: 100% (624/624), 139.37 KiB | 0 bytes/s, done.
-Resolving deltas: 100% (356/356), done.
-Checking connectivity... done.
-```
-
-Latest stable release at _**master**_ branch can be downloaded by executing:
-
-```text
-$ git clone -b master https://github.com/opencb/iva.git
-Cloning into 'iva'...
-remote: Counting objects: 624, done.
-remote: Total 624 (delta 0), reused 0 (delta 0), pack-reused 624
-Receiving objects: 100% (624/624), 139.37 KiB | 191.00 KiB/s, done.
-Resolving deltas: 100% (356/356), done.
-Checking connectivity... done.
-```
-
-After this, in both cases, you **must** execute the following command to fetch the JSorolla submodule \(only the first time\):
-
-```text
-git submodule update --init
-```
-
-Go to lib/jsorolla and checkout to _**develop**_ branch of Jsorolla by
-
-```text
-cd lib/jsorolla
-git checkout develop
-```
-
-### Build
-
-First, you must update JSorolla dependencies, from the root folder execute:
-
-```text
-cd lib/jsorolla
-npm install
-```
-
-Finally, to build IVA execute:
-
-We have to install npm packages for IVA, from the the root folder execute:
-
-```text
-npm install
-```
-
-And now execute:
-
-```text
-npm run build
-```
-
-when completed, all compiled files will be located under the _build_ folder.
-
-### Testing
-
-You can copy build content to a web server such as Apache HTTP Server and open your favourite internet browser to open IVA.
-
-### Execute Tests in development with nightwatch\([http://nightwatchjs.org/](http://nightwatchjs.org/)\)
-
-Prerequisite: make sure you have JDK installed, with at least version 8. If you don't have it, you can grab it from [http://www.oracle.com/technetwork/java/javase/downloads/index.html](http://www.oracle.com/technetwork/java/javase/downloads/index.html).
-
-1. npm install --dev
-2. Selenium server. Download the latest release .jar from [http://selenium-release.storage.googleapis.com/index.html](http://selenium-release.storage.googleapis.com/index.html). i.e. selenium-server-standalone-3.7.0.jar
-3. Chromedriver. Download from [https://sites.google.com/a/chromium.org/chromedriver/downloads](https://sites.google.com/a/chromium.org/chromedriver/downloads) that version which supports your chrome versión. You can review what version fits your browser here [https://chromedriver.storage.googleapis.com/2.33/notes.txt](https://chromedriver.storage.googleapis.com/2.33/notes.txt).
-4. Create a bin folder inside your test folder in root path
-5. Move selenium bin and chrome bin inside that bin folder.
-6. npm run test-e2e \( or ./node\_modules/.bin/nightwatch test/e2e/clinical-prioritization.js if you want execute just one\)
-
diff --git a/src/sites/iva/conf/browsers.settings.js b/src/sites/iva/conf/browsers.settings.js
index 8ed956c77c..8b995e5d82 100644
--- a/src/sites/iva/conf/browsers.settings.js
+++ b/src/sites/iva/conf/browsers.settings.js
@@ -293,7 +293,7 @@ const CATALOG_SETTINGS = {
},
// merge criterium: uses this array as filter for internal 1D array.
// It is supported either details[] or hiddenDetails[].
- details: ["annotationSummary", "annotationConsType", "annotationPropFreq", "annotationClinical", "cohortStats", "samples", "beacon", "json-view"]
+ details: ["annotationSummary", "annotationConsType", "annotationPropFreq", "annotationClinical", "annotationPharmacogenomics", "cohortStats", "samples", "beacon", "json-view"]
// hiddenDetails: ["json-view"]
},
DISEASE_PANEL_BROWSER: {
diff --git a/src/sites/iva/conf/config.js b/src/sites/iva/conf/config.js
index c47814fff3..82dcb03b17 100644
--- a/src/sites/iva/conf/config.js
+++ b/src/sites/iva/conf/config.js
@@ -141,6 +141,11 @@ const CATALOG_NAVBAR_MENU = {
name: "Cohort Browser",
visibility: "public"
},
+ {
+ id: "note-browser",
+ name: "Note Browser",
+ visibility: "public",
+ },
{
separator: true,
visibility: "public"
@@ -757,9 +762,18 @@ const SUITE = {
`
},
menu: [
+ {
+ id: "organization-admin",
+ name: "Organizations Admin",
+ fa_icon: "fas fa-file-invoice",
+ icon: "img/tools/icons/variant_browser.svg",
+ description: "",
+ visibility: "public",
+ featured: true,
+ },
{
id: "study-admin",
- name: "Study admin",
+ name: "Study Admin",
fa_icon: "fas fa-file-invoice",
icon: "img/tools/icons/variant_browser.svg",
description: "",
@@ -777,7 +791,7 @@ const SUITE = {
// },
{
id: "study-admin-iva",
- name: "IVA configuration",
+ name: "IVA Configuration",
fa_icon: "fas fa-file-invoice",
icon: "img/tools/icons/variant_browser.svg",
description: "",
@@ -793,24 +807,33 @@ const SUITE = {
// visibility: "public",
// featured: false,
// },
+ // {
+ // id: "study-variant-admin",
+ // name: "Study Variant Admin",
+ // fa_icon: "fas fa-file-invoice",
+ // icon: "img/tools/icons/variant_browser.svg",
+ // description: "",
+ // visibility: "public",
+ // featured: true,
+ // },
{
- id: "study-variant-admin",
- name: "Study Variant Admin",
- fa_icon: "fas fa-file-invoice",
- icon: "img/tools/icons/variant_browser.svg",
- description: "",
- visibility: "public",
- featured: true,
- },
- {
- id: "projects-admin",
- name: "Project Manager",
+ id: "operations-admin",
+ name: "Operations Admin",
fa_icon: "fas fa-file-invoice",
icon: "img/tools/icons/variant_browser.svg",
description: "",
visibility: "public",
featured: true,
},
+ // {
+ // id: "projects-admin",
+ // name: "Project Manager",
+ // fa_icon: "fas fa-file-invoice",
+ // icon: "img/tools/icons/variant_browser.svg",
+ // description: "",
+ // visibility: "public",
+ // featured: true,
+ // },
],
fileExplorer: {
visibility: "private"
diff --git a/src/sites/iva/conf/opencga-variant-browser.settings.js b/src/sites/iva/conf/opencga-variant-browser.settings.js
index cd44032a00..d65c80ccf1 100644
--- a/src/sites/iva/conf/opencga-variant-browser.settings.js
+++ b/src/sites/iva/conf/opencga-variant-browser.settings.js
@@ -88,6 +88,6 @@ const OPENCGA_VARIANT_BROWSER_SETTINGS = {
},
// merge criterium: uses this array as filter for internal 1D array.
// It is supported either details[] or hiddenDetails[].
- details: ["annotationSummary", "annotationConsType", "annotationPropFreq", "annotationClinical", "cohortStats", "samples", "beacon", "json-view"]
+ details: ["annotationSummary", "annotationConsType", "annotationPropFreq", "annotationClinical", "annotationPharmacogenomics", "cohortStats", "samples", "beacon", "json-view"]
// hiddenDetails: ["json-view"]
};
diff --git a/src/sites/iva/index.html b/src/sites/iva/index.html
index b9289a309e..617a72ee0f 100644
--- a/src/sites/iva/index.html
+++ b/src/sites/iva/index.html
@@ -1,226 +1,44 @@
-
-
-
-
-
-
-
-
+
OpenCB Suite
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/src/sites/iva/iva-app.js b/src/sites/iva/iva-app.js
index bd6d64bacb..c343891b24 100644
--- a/src/sites/iva/iva-app.js
+++ b/src/sites/iva/iva-app.js
@@ -19,13 +19,16 @@
*/
import {html, LitElement, nothing} from "lit";
-import "./getting-started.js";
-import "./iva-settings.js";
+// import "./getting-started.js";
+// import "./iva-settings.js";
+
+// import jsorolla styles
+import "../../../styles/css/global.css";
+import "../../genome-browser/css/genome-browser.css";
+import "../../core/visualisation/viz-styles.css";
-// @dev[jsorolla]
import {OpenCGAClient} from "../../core/clients/opencga/opencga-client.js";
import {CellBaseClient} from "../../core/clients/cellbase/cellbase-client.js";
-import {ReactomeClient} from "../../core/clients/reactome/reactome-client.js";
import UtilsNew from "../../core/utils-new.js";
import NotificationUtils from "../../webcomponents/commons/utils/notification-utils.js";
@@ -38,7 +41,6 @@ import "../../webcomponents/variant/variant-beacon.js";
import "../../webcomponents/opencga/opencga-gene-view.js";
import "../../webcomponents/opencga/opencga-transcript-view.js";
import "../../webcomponents/opencga/opencga-protein-view.js";
-// import "../../webcomponents/user/opencga-projects.js";
import "../../webcomponents/sample/sample-browser.js";
import "../../webcomponents/sample/sample-view.js";
import "../../webcomponents/sample/sample-variant-stats-browser.js";
@@ -47,7 +49,6 @@ import "../../webcomponents/sample/sample-update.js";
import "../../webcomponents/disease-panel/disease-panel-browser.js";
import "../../webcomponents/disease-panel/disease-panel-update.js";
import "../../webcomponents/file/file-browser.js";
-// import "../../webcomponents/file/file-update.js";
import "../../webcomponents/family/family-browser.js";
import "../../webcomponents/family/family-update.js";
import "../../webcomponents/individual/individual-browser.js";
@@ -81,15 +82,15 @@ import "../../webcomponents/clinical/clinical-analysis-create.js";
import "../../webcomponents/file/file-manager.js";
import "../../webcomponents/job/job-monitor.js";
import "../../webcomponents/loading-spinner.js";
+import "../../webcomponents/organization/admin/organization-admin.js";
import "../../webcomponents/project/projects-admin.js";
import "../../webcomponents/study/admin/study-admin.js";
import "../../webcomponents/study/admin/study-admin-iva.js";
import "../../webcomponents/study/admin/catalog-admin.js";
-import "../../webcomponents/study/admin/variant/study-variant-admin.js";
-import "../../webcomponents/user/user-login.js";
+import "../../webcomponents/study/admin/variant/operations-admin.js";
import "../../webcomponents/user/user-profile.js";
-// import "../../webcomponents/user/user-password-reset.js";
import "../../webcomponents/api/rest-api.js";
+import "../../webcomponents/note/note-browser.js";
import "../../webcomponents/commons/layouts/custom-footer.js";
import "../../webcomponents/commons/layouts/custom-navbar.js";
@@ -99,6 +100,7 @@ import "../../webcomponents/commons/layouts/custom-welcome.js";
import "../../webcomponents/commons/layouts/custom-landing.js";
import "../../webcomponents/clinical/rga/rga-browser.js";
+
import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils";
import ExtensionsManager from "../../webcomponents/extensions-manager.js";
@@ -158,7 +160,6 @@ class IvaApp extends LitElement {
const components = [
"home",
"gettingstarted",
- "login",
"aboutzetta",
// "reset-password",
"settings",
@@ -229,15 +230,18 @@ class IvaApp extends LitElement {
"diseasePanelUpdate",
"clinicalAnalysis",
// Admin
+ "organization-admin",
"study-admin",
"study-admin-iva",
// "catalog-admin",
- "study-variant-admin",
+ "operations-admin",
"opencga-admin",
"variants-admin",
- "projects-admin",
+ // "projects-admin",
// REST-API
"rest-api",
+ // note
+ "note-browser",
];
// Add custom tools
@@ -823,39 +827,6 @@ class IvaApp extends LitElement {
// this.hashFragmentListener();
}
- // renderHashFragments(tool) {
- // console.log(`Update hash fragment URL with tool: '${tool ? `#${tool}` : this.tool}'`);
- //
- // // Keep global 'tool' param updated.
- // if (tool && this.tool !== `#${tool}`) {
- // this.tool = `#${tool}`;
- // }
- //
- // // Build hash fragment URL as: #tool/projectId/studyId
- // let newHashFragmentUrl = tool ? `#${tool}` : this.tool;
- // if (this.opencgaSession?.project) {
- // newHashFragmentUrl += "/" + this.opencgaSession.project.id;
- // if (this.opencgaSession.study) {
- // newHashFragmentUrl += "/" + this.opencgaSession.study.id;
- // }
- // }
- //
- // if (window.location.hash === newHashFragmentUrl) { // || newHashFragmentUrl === "#interpreter"
- // this.hashFragmentListener();
- // } else {
- // window.location.hash = newHashFragmentUrl;
- // }
- // }
-
- route(e) {
- this.tool = e.detail.hash;
- if (e.detail?.resource) {
- this.queries = {...this.queries, [e.detail.resource]: e.detail?.query};
- }
- // this.renderHashFragments();
- this.hashFragmentListener();
- }
-
hashFragmentListener() {
console.log("hashFragmentListener - Hide all enabled elements");
@@ -984,14 +955,14 @@ class IvaApp extends LitElement {
}
changeActiveStudy(studyFqn) {
- if (this.opencgaSession.study.fqn === studyFqn) {
+ if (this.opencgaSession?.study?.fqn === studyFqn) {
console.log("New selected study is already the current active study!");
return;
}
// Change active study
let studyFound = false;
- for (const project of this.opencgaSession.projects) {
+ for (const project of (this.opencgaSession?.projects || [])) {
const studyIndex = project.studies.findIndex(s => s.fqn === studyFqn);
if (studyIndex >= 0) {
this.opencgaSession.project = project;
@@ -1171,6 +1142,10 @@ class IvaApp extends LitElement {
// }
onSessionUpdateRequest() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: "Refresh Session: Session Update Request",
+ message: "Session updated correctly",
+ });
this._createOpenCGASession();
}
@@ -1249,8 +1224,7 @@ class IvaApp extends LitElement {
+ @login="${this.onLogin}">
`;
}
@@ -1290,11 +1264,9 @@ class IvaApp extends LitElement {
@changeTool="${e => this.changeTool(e.detail.value)}"
@changeApp="${e => this.onChangeApp(e.detail.event, e.detail.toggle)}"
@studySelect="${ e => this.onStudySelect(e.detail.event, e.detail.study)}"
- @jobSelected="${e => this.onJobSelected(e)}"
- @route="${this.route}">
+ @jobSelected="${e => this.onJobSelected(e)}">
-
+
${ this.isCreatingSession ? html`
- ${this.config.enabledComponents.home && !this.isCreatingSession ? html`
-
- this.onChangeApp(e.detail.e, false)}">
-
-
- ` : nothing}
-
-
- ${this.config.enabledComponents.customPage ? this.renderCustomPage() : nothing}
+ ${!this.isCreatingSession ? html`
+ ${this.config.enabledComponents.home ? html`
+
+ this.onChangeApp(e.detail.e, false)}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents.terms ? html`
-
-
-
- ` : nothing}
+
+ ${this.config.enabledComponents.customPage ? this.renderCustomPage() : nothing}
- ${this.config.enabledComponents.contact ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.terms ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.faq ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.contact ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.gettingstarted ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.faq ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents?.aboutzetta ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.gettingstarted ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.login ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents?.aboutzetta ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["variant-browser"] ? html`
-
- this.onQueryFilterSearch(e, "variant-browser")}"
- onqueryChange="${e => this.onQueryChange(e, "variant")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "variant-browser")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents["variant-browser"] ? html`
+
+ this.onQueryFilterSearch(e, "variant-browser")}"
+ onqueryChange="${e => this.onQueryChange(e, "variant")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "variant-browser")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents["clinicalAnalysisPortal"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["clinicalAnalysisPortal"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["rga"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["rga"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["rd-interpreter"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["rd-interpreter"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cancer-interpreter"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cancer-interpreter"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.beacon ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.beacon ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.genomeBrowser ? html`
-
- Not available yet...
-
- ` : nothing}
+ ${this.config.enabledComponents.genomeBrowser ? html`
+
+ Not available yet...
+
+ ` : nothing}
- ${this.config.enabledComponents.sample ? html`
-
- this.onQueryFilterSearch(e, "sample")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "sample")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents.sample ? html`
+
+ this.onQueryFilterSearch(e, "sample")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "sample")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents.panel ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.panel ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.file ? html`
-
- this.onQueryFilterSearch(e, "file")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "file")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents.file ? html`
+
+ this.onQueryFilterSearch(e, "file")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "file")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents["disease-panel"] ? html`
-
- this.onQueryFilterSearch(e, "disease-panel")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "disease-panel")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents["disease-panel"] ? html`
+
+ this.onQueryFilterSearch(e, "disease-panel")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "disease-panel")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents["diseasePanelUpdate"] ? html`
-
-
+
-
-
- ` : nothing}
+ }>
+
+
+ ` : nothing}
-
- ${this.config.enabledComponents["sample-view"] ? html`
-
-
-
-
- ` : nothing}
+
+ ${this.config.enabledComponents["sample-view"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["fileUpdate"] ? html`
- ${this.fileId} ` }" icon="fas fa-vial icon-padding">
-
-
${this.fileId} ` }" icon="fas fa-vial icon-padding">
+
+
-
-
- ` : nothing}
+ }>
+
+
+ ` : nothing}
- ${this.config.enabledComponents["sampleUpdate"] ? html`
- ${this.sampleId} ` }" icon="fas fa-vial icon-padding">
-
-
${this.sampleId} ` }" icon="fas fa-vial icon-padding">
+
+
-
-
- ` : nothing}
+ }>
+
+
+ ` : nothing}
- ${this.config.enabledComponents["individualUpdate"] ? html`
- ${this.individualId} ` }" icon="fas fa-vial icon-padding">
-
-
${this.individualId} ` }" icon="fas fa-vial icon-padding">
+
+
-
-
- ` : nothing}
+ }>
+
+
+ ` : nothing}
- ${this.config.enabledComponents["familyUpdate"] ? html`
- ${this.familyId} ` }" icon="fas fa-vial icon-padding">
-
-
${this.familyId} ` }" icon="fas fa-vial icon-padding">
+
+
-
-
- ` : nothing}
-
- ${this.config.enabledComponents.gene ? html`
-
- this.onQueryFilterSearch(e, "variant")}">
-
-
- ` : nothing}
+ }>
+
+
+ ` : nothing}
- ${this.config.enabledComponents.transcript ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.gene ? html`
+
+ this.onQueryFilterSearch(e, "variant")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents.protein ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.transcript ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.individual ? html`
-
- this.onQueryFilterSearch(e, "individual")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "individual")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents.protein ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.family ? html`
-
- this.onQueryFilterSearch(e, "family")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "family")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents.individual ? html`
+
+ this.onQueryFilterSearch(e, "individual")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "individual")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents.cohort ? html`
-
- this.onQueryFilterSearch(e, "cohort")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "cohort")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents.family ? html`
+
+ this.onQueryFilterSearch(e, "family")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "family")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents.clinicalAnalysis ? html`
-
- this.onQueryFilterSearch(e, "clinical-analysis")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "clinical-analysis")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents.cohort ? html`
+
+ this.onQueryFilterSearch(e, "cohort")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "cohort")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents.job ? html`
-
- this.onQueryFilterSearch(e, "job")}"
- @activeFilterChange="${e => this.onQueryFilterSearch(e, "job")}">
-
-
- ` : nothing}
+ ${this.config.enabledComponents.clinicalAnalysis ? html`
+
+ this.onQueryFilterSearch(e, "clinical-analysis")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "clinical-analysis")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cat-browser"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.job ? html`
+
+ this.onQueryFilterSearch(e, "job")}"
+ @activeFilterChange="${e => this.onQueryFilterSearch(e, "job")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cat-analysis"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["note-browser"] ? html`
+
+ this.onQueryFilterSearch(e, "note-browser")}">
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cat-clinical"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cat-browser"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cat-tools"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cat-analysis"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cat-catalog"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cat-clinical"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cat-alignment"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cat-tools"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cat-ga4gh"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cat-catalog"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["sampleVariantStatsBrowser"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cat-alignment"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["sampleCancerVariantStatsBrowser"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cat-ga4gh"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["sample-variant-stats"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["sampleVariantStatsBrowser"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["cohort-variant-stats"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["sampleCancerVariantStatsBrowser"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["eligibility"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["sample-variant-stats"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["sample-eligibility"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["cohort-variant-stats"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["knockout"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["eligibility"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["inferred-sex"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["sample-eligibility"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["individual-relatedness"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["knockout"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["mendelian-error"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["inferred-sex"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["sample-qc"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["individual-relatedness"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["individual-qc"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["mendelian-error"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["family-qc"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["sample-qc"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["plink"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["individual-qc"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["gatk"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["family-qc"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["variant-export"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["plink"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["variant-stats-exporter"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["gatk"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["mutational-signature"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["variant-export"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["gwas"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["variant-stats-exporter"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["rd-tiering"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["mutational-signature"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["clinical-analysis-create"] ? html`
-
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["gwas"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.account ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["rd-tiering"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["file-manager"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["clinical-analysis-create"] ? html`
+
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents.settings ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.account ? html`
+
+
+
+
+ ` : nothing}
+ ${this.config.enabledComponents["file-manager"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["interpreter"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents.settings ? html`
+
+
+
+ ` : nothing}
-
- ${this.config.enabledComponents["alignment-index"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["interpreter"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["coverage-index"] ? html`
-
-
-
- ` : nothing}
+
+ ${this.config.enabledComponents["alignment-index"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["alignment-stats"] ? html`
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["coverage-index"] ? html`
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["job-view"] ? html`
-
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["alignment-stats"] ? html`
+
+
+
+ ` : nothing}
-
- ${this.config.enabledComponents["projects-admin"] ? html`
-
-
- ` : nothing}
+ ${this.config.enabledComponents["job-view"] ? html`
+
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["catalog-admin"] ? html`
-
-
-
-
- ` : nothing}
+
+ ${this.config.enabledComponents["organization-admin"] ? html`
+
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["opencga-admin"] ? html`
-
-
- ` : nothing}
+ ${this.config.enabledComponents["catalog-admin"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["study-admin"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["projects-admin"] ? html`
+
+
+ ` : nothing}
-
-
- ${this.config.enabledComponents["study-admin-iva"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["study-admin"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["study-variant-admin"] ? html`
-
-
-
-
- ` : nothing}
+
+
+ ${this.config.enabledComponents["study-admin-iva"] ? html`
+
+
+
+
+ ` : nothing}
- ${this.config.enabledComponents["rest-api"] ? html`
-
-
-
-
- ` : nothing}
+ ${this.config.enabledComponents["operations-admin"] ? html`
+
+
+
+
+ ` : nothing}
- ${ExtensionsManager.getTools().map(tool => html`
- ${this.config.enabledComponents[tool.id] ? html`
+ ${this.config.enabledComponents["rest-api"] ? html`
+
- ${tool.render(this.opencgaSession)}
+
` : nothing}
- `)}
+
+ ${ExtensionsManager.getTools().map(tool => html`
+ ${this.config.enabledComponents[tool.id] ? html`
+
+ ${tool.render(this.opencgaSession)}
+
+ ` : nothing}
+ `)}
+ ` : nothing}
-
-
-
-
-
-
-
- Test App
-
-
+ Test App
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
diff --git a/src/sites/test-app/test-app.js b/src/sites/test-app/test-app.js
index a47cea98ea..e4eb233d50 100644
--- a/src/sites/test-app/test-app.js
+++ b/src/sites/test-app/test-app.js
@@ -20,6 +20,11 @@
import {html, LitElement, nothing} from "lit";
+// import jsorolla styles
+import "../../../styles/css/global.css";
+import "../../genome-browser/css/genome-browser.css";
+import "../../core/visualisation/viz-styles.css";
+
import UtilsNew from "../../core/utils-new.js";
import NotificationUtils from "../../webcomponents/commons/utils/notification-utils.js";
import NotificationManager from "../../core/notification-manager.js";
diff --git a/src/webcomponents/api/opencga-rest-input.js b/src/webcomponents/api/opencga-rest-input.js
index 72bd0d3923..296a819e9f 100644
--- a/src/webcomponents/api/opencga-rest-input.js
+++ b/src/webcomponents/api/opencga-rest-input.js
@@ -363,7 +363,6 @@ export default class OpencgaRestInput extends LitElement {
url = url.replace("{apiVersion}", this.opencgaSession.opencgaClient._config.version);
if (this.endpoint.method === "GET" || this.endpoint.method === "DELETE") {
- url += "sid=" + this.opencgaSession.opencgaClient._config.token;
this.#getOrDeleteEndpoint(url);
}
@@ -388,8 +387,12 @@ export default class OpencgaRestInput extends LitElement {
});
let error, result;
+ const options = {
+ method: this.endpoint.method,
+ token: this.opencgaSession.opencgaClient._config.token,
+ };
this.#setLoading(true);
- this.restClient.call(url, {method: this.endpoint.method})
+ this.restClient.call(url, options)
.then(response => {
result = UtilsNew.objectClone(response.responses[0].results);
})
@@ -405,7 +408,8 @@ export default class OpencgaRestInput extends LitElement {
#postEndpoint(url, isForm) {
// Add Study
- url += "study=" + encodeURIComponent(this.opencgaSession.study.fqn);
+ // Fixme: not all endpoints require study. E.g. POST /users/password/
+ url += "study=" + encodeURIComponent(this.opencgaSession?.study?.fqn);
// Replace PATH params
this.endpoint.parameters
@@ -431,7 +435,7 @@ export default class OpencgaRestInput extends LitElement {
const _options = {
sid: this.opencgaSession.opencgaClient._config.token,
token: this.opencgaSession.opencgaClient._config.token,
- data: isForm ? this.formatBody(this.data?.body) : JSON.parse(this.dataJson?.body),
+ data: isForm ? this.formatBody(this.data?.body) : JSON.parse(this.dataJson?.body || "{}"),
method: "POST",
};
diff --git a/src/webcomponents/api/rest-utils.js b/src/webcomponents/api/rest-utils.js
index 288717b7da..5d300398f8 100644
--- a/src/webcomponents/api/rest-utils.js
+++ b/src/webcomponents/api/rest-utils.js
@@ -1,7 +1,8 @@
// ********************************************************************
// 0. Functions for checking if the param is: enum | primitive | object
// ********************************************************************
-import UtilsNew from "../../core/utils-new";
+import UtilsNew from "../../core/utils-new.js";
+import CatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
export default {
@@ -39,7 +40,7 @@ export default {
},
isAdministrator(opencgaSession) {
- return opencgaSession?.user?.account?.type === "ADMINISTRATOR" || opencgaSession?.user.id === "OPENCGA";
+ return opencgaSession?.user?.id?.toUpperCase() === "OPENCGA" || CatalogUtils.isOrganizationAdmin(opencgaSession?.organization, opencgaSession?.user?.id);
},
sortArray(elements) {
diff --git a/src/webcomponents/clinical/analysis/hrdetect-analysis.js b/src/webcomponents/clinical/analysis/hrdetect-analysis.js
index 39cdd5890e..6b4aa6a19e 100644
--- a/src/webcomponents/clinical/analysis/hrdetect-analysis.js
+++ b/src/webcomponents/clinical/analysis/hrdetect-analysis.js
@@ -115,10 +115,6 @@ export default class HRDetectAnalysis extends LitElement {
sampleId: this.toolParams.query?.sample,
snvFittingId: this.toolParams.snvFittingId,
svFittingId: this.toolParams.svFittingId,
- snv3CustomName: this.toolParams.snv3CustomName || "",
- snv8CustomName: this.toolParams.snv8CustomName || "",
- sv3CustomName: this.toolParams.sv3CustomName || "",
- sv8CustomName: this.toolParams.sv8CustomName || "",
bootstrap: !!this.toolParams.bootstrap,
};
@@ -290,48 +286,8 @@ export default class HRDetectAnalysis extends LitElement {
],
},
{
- title: "Advanced Parameters",
+ title: "HRDetect Parameters",
elements: [
- {
- title: "SNV3 Custom Name",
- field: "snv3CustomName",
- type: "input-text",
- display: {
- help: {
- text: "Custom signature name that will be considered as SNV3 input for HRDetect.",
- },
- },
- },
- {
- title: "SNV8 Custom Name",
- field: "snv8CustomName",
- type: "input-text",
- display: {
- help: {
- text: "Custom signature name that will be considered as SNV8 input for HRDetect.",
- },
- },
- },
- {
- title: "SV3 Custom Name",
- field: "sv3CustomName",
- type: "input-text",
- display: {
- help: {
- text: "Custom signature name that will be considered as SV3 input for HRDetect.",
- },
- },
- },
- {
- title: "SV8 Custom Name",
- field: "sv8CustomName",
- type: "input-text",
- display: {
- help: {
- text: "Custom signature name that will be considered as SV8 input for HRDetect.",
- },
- },
- },
{
title: "Bootstrap",
field: "bootstrap",
diff --git a/src/webcomponents/clinical/analysis/hrdetect-view.js b/src/webcomponents/clinical/analysis/hrdetect-view.js
index a8ac5468d9..e3178df7de 100644
--- a/src/webcomponents/clinical/analysis/hrdetect-view.js
+++ b/src/webcomponents/clinical/analysis/hrdetect-view.js
@@ -169,12 +169,24 @@ class HRDetectView extends LitElement {
},
},
{
- name: "Scores",
+ name: "Probability",
+ field: "scores",
+ display: {
+ format: scores => {
+ return scores.find(row => row.key?.toLowerCase?.() === "probability")?.value?.toFixed?.(2) || "-";
+ },
+ },
+ },
+ {
+ name: "Contributions",
field: "scores",
type: "table",
display: {
- // style: "width:auto",
showHeader: false,
+ filter: data => (data || []).filter(row => {
+ return !["intercept", "probability"].includes(row?.key?.toLowerCase?.());
+ }),
+ transform: data => data.toSorted((a, b) => b.value - a.value),
columns: [
{
title: "key",
@@ -182,7 +194,10 @@ class HRDetectView extends LitElement {
},
{
title: "value",
- field: "value"
+ field: "value",
+ display: {
+ format: value => value.toFixed(2),
+ },
},
],
},
diff --git a/src/webcomponents/clinical/analysis/mutational-signature-analysis.js b/src/webcomponents/clinical/analysis/mutational-signature-analysis.js
index ba4ccac2ae..0ca4931d3a 100644
--- a/src/webcomponents/clinical/analysis/mutational-signature-analysis.js
+++ b/src/webcomponents/clinical/analysis/mutational-signature-analysis.js
@@ -163,8 +163,6 @@ export default class MutationalSignatureAnalysis extends LitElement {
fitNBoot: this.toolParams.fitNBoot,
fitThresholdPerc: this.toolParams.fitThresholdPerc,
fitThresholdPval: this.toolParams.fitThresholdPval,
- fitSignaturesFile: this.toolParams.fitSignaturesFile,
- fitRareSignaturesFile: this.toolParams.fitRareSignaturesFile,
};
// Check if we have provided an existing signature list
@@ -411,42 +409,6 @@ export default class MutationalSignatureAnalysis extends LitElement {
field: "fitNBoot",
type: "input-text",
},
- {
- title: "Fit Signatures File",
- field: "fitSignaturesFile",
- type: "custom",
- display: {
- render: (fitSignaturesFile, dataFormFilterChange) => html`
- dataFormFilterChange(e.detail.value)}">
-
- `,
- },
- },
- {
- title: "Fit Rare Signatures File",
- field: "fitRareSignaturesFile",
- type: "custom",
- display: {
- render: (fitRareSignaturesFile, dataFormFilterChange) => html`
- dataFormFilterChange(e.detail.value)}">
-
- `,
- },
- },
]
}
];
diff --git a/src/webcomponents/clinical/analysis/rd-tiering-analysis.js b/src/webcomponents/clinical/analysis/rd-tiering-analysis.js
index c92ee9adbd..4ac8966af2 100644
--- a/src/webcomponents/clinical/analysis/rd-tiering-analysis.js
+++ b/src/webcomponents/clinical/analysis/rd-tiering-analysis.js
@@ -20,7 +20,6 @@ import UtilsNew from "../../../core/utils-new.js";
import "../../commons/forms/data-form.js";
import "../../commons/filters/catalog-search-autocomplete.js";
-
export default class RdTieringAnalysis extends LitElement {
constructor() {
@@ -161,7 +160,7 @@ export default class RdTieringAnalysis extends LitElement {
display: {
render: (panels, dataFormFilterChange) => {
// Get whether disease panels can be modified or are fixed
- const casePanelLock = !!this.clinicalAnalysis;
+ const casePanelLock = !!this.clinicalAnalysis?.panelLocked;
// Get the list of disease panels for the dropdown
let diseasePanels = [];
if (casePanelLock) {
diff --git a/src/webcomponents/clinical/clinical-analysis-browser.js b/src/webcomponents/clinical/clinical-analysis-browser.js
index c12512e229..fc25fcadc3 100644
--- a/src/webcomponents/clinical/clinical-analysis-browser.js
+++ b/src/webcomponents/clinical/clinical-analysis-browser.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
+import {LitElement, html, nothing} from "lit";
import UtilsNew from "../../core/utils-new.js";
import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
@@ -141,16 +141,18 @@ export default class ClinicalAnalysisBrowser extends LitElement {
.query="${params.executedQuery}"
.active="${params.active}"
@selectanalysis="${params.onSelectClinicalAnalysis}"
- @selectrow="${e => params.onClickRow(e, "clinicalAnalysis")}"
- @rowUpdate="${e => params.onComponentUpdate(e, "clinicalAnalysis")}"
- @clinicalAnalysisUpdate="${e => params.onComponentUpdate(e, "clinicalAnalysis")}"
+ @selectrow="${e => params.onClickRow(e)}"
+ @rowUpdate="${e => params.onComponentUpdate(e)}"
+ @clinicalAnalysisUpdate="${e => params.onComponentUpdate(e)}"
@settingsUpdate="${() => this.onSettingsUpdate()}">
-
-
+ ${params?.detail ? html`
+
+
+ ` : nothing}
`,
},
{
diff --git a/src/webcomponents/clinical/clinical-analysis-create.js b/src/webcomponents/clinical/clinical-analysis-create.js
index bd7dc66d52..38dd40ad98 100644
--- a/src/webcomponents/clinical/clinical-analysis-create.js
+++ b/src/webcomponents/clinical/clinical-analysis-create.js
@@ -108,7 +108,7 @@ export default class ClinicalAnalysisCreate extends LitElement {
// id: this.opencgaSession?.user?.id
// },
comments: [],
- panelLock: false,
+ panelLocked: false,
samples: [],
};
}
@@ -358,7 +358,35 @@ export default class ClinicalAnalysisCreate extends LitElement {
delete data.dueDate;
}
- this.opencgaSession.opencgaClient.clinical().create(data, {study: this.opencgaSession.study.fqn, createDefaultInterpretation: true})
+ this.opencgaSession.opencgaClient.clinical()
+ .create(data, {
+ study: this.opencgaSession.study.fqn,
+ includeResult: true
+ })
+ .then(response => {
+ const interpretationId = response?.responses?.[0]?.results?.[0]?.interpretation?.id;
+ const interpretationData = {
+ method: {
+ name: "iva-default",
+ version: this.opencgaSession?.about?.Version || "-",
+ dependencies: [
+ {
+ name: "OpenCGA",
+ version: this.opencgaSession?.about?.Version || "-",
+ },
+ {
+ name: "Cellbase",
+ version: this.opencgaSession.project?.cellbase?.version || "-",
+ },
+ ],
+ },
+ };
+ return this.opencgaSession.opencgaClient.clinical()
+ .updateInterpretation(data.id, interpretationId, interpretationData, {
+ study: this.opencgaSession.study.fqn,
+ methodsAction: "SET",
+ });
+ })
.then(() => {
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
title: "Clinical analysis created",
@@ -478,7 +506,7 @@ export default class ClinicalAnalysisCreate extends LitElement {
},
{
title: "Disease Panel Lock",
- field: "panelLock",
+ field: "panelLocked",
type: "toggle-switch",
display: {
helpMessage: "You must select at least one of the Clinical Analysis panels to enable Disease Panel Lock.",
@@ -502,7 +530,7 @@ export default class ClinicalAnalysisCreate extends LitElement {
return html`
handleFlagsFilterChange(e, "flags.id")}">
diff --git a/src/webcomponents/clinical/clinical-analysis-grid.js b/src/webcomponents/clinical/clinical-analysis-grid.js
index f0f4977dfc..801b7fd76b 100644
--- a/src/webcomponents/clinical/clinical-analysis-grid.js
+++ b/src/webcomponents/clinical/clinical-analysis-grid.js
@@ -19,10 +19,11 @@ import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-util
import UtilsNew from "../../core/utils-new.js";
import GridCommons from "../commons/grid-commons.js";
import CatalogGridFormatter from "../commons/catalog-grid-formatter.js";
-import "../commons/opencb-grid-toolbar.js";
import LitUtils from "../commons/utils/lit-utils.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-import ModalUtils from "../commons/modal/modal-utils";
+import ModalUtils from "../commons/modal/modal-utils.js";
+import WebUtils from "../commons/utils/web-utils.js";
+import "../commons/opencb-grid-toolbar.js";
export default class ClinicalAnalysisGrid extends LitElement {
@@ -304,55 +305,41 @@ export default class ClinicalAnalysisGrid extends LitElement {
// TODO remove this code as soon as new OpenCGA configuration is in place
const _priorities = this.opencgaSession?.study?.internal?.configuration?.clinical?.priorities || [];
- // Priorities classes
- const priorityMap = {
- URGENT: "text-bg-danger",
- HIGH: "text-bg-warning",
- MEDIUM: "text-bg-primary",
- LOW: "text-bg-info"
- };
- const priorityRankToColor = [
- "text-bg-danger",
- "text-bg-warning",
- "text-bg-primary",
- "text-bg-info",
- "text-bg-success",
- "text-bg-light"
- ];
-
- const hasWriteAccess = OpencgaCatalogUtils.checkPermissions(this.opencgaSession.study, this.opencgaSession.user.id, "WRITE_CLINICAL_ANALYSIS");
+ const hasWriteAccess = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ "WRITE_CLINICAL_ANALYSIS",
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
const isEditable = !this._config.readOnlyMode && hasWriteAccess && !row.locked; // priority is editable
-
// Dropdown button styles and classes
const btnClassName = "btn btn-light btn-block dropdown-toggle";
const btnStyle = "display:inline-flex;align-items:center;";
// Current priority
const currentPriorityText = value?.id ?? value ?? "-";
- const currentPriorityLabel = priorityRankToColor[value?.rank ?? ""] ?? priorityMap[value ?? ""] ?? "";
+ const currentPriorityColor = WebUtils.getClinicalAnalysisPriorityColour(value?.rank);
return `
-
-
+
+
${currentPriorityText}
-
${isEditable ? `
- ${interpretation.id}
+ ${WebUtils.getDisplayName(interpretation)}
version ${interpretation.version}
diff --git a/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js b/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js
index 19e25ac386..3a2e001956 100644
--- a/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js
+++ b/src/webcomponents/clinical/interpretation/clinical-interpretation-update.js
@@ -149,6 +149,11 @@ export default class ClinicalInterpretationUpdate extends LitElement {
disabled: true,
},
},
+ {
+ title: "Interpretation Name",
+ field: "name",
+ type: "input-text",
+ },
{
title: "Assigned To",
field: "analyst.id",
@@ -166,7 +171,7 @@ export default class ClinicalInterpretationUpdate extends LitElement {
return html`
{
// CAUTION: check if the panelLock condition is the same as clinical-analysis-update.js
- const panelLock = !!this.clinicalAnalysis?.panelLock;
+ const panelLock = !!this.clinicalAnalysis?.panelLocked;
const panelList = panelLock ? this.clinicalAnalysis?.panels : this.opencgaSession.study?.panels;
const handlePanelsFilterChange = e => {
const panelList = (e.detail?.value?.split(",") || [])
diff --git a/src/webcomponents/cohort/cohort-browser.js b/src/webcomponents/cohort/cohort-browser.js
index b5aad11135..c3e2442497 100644
--- a/src/webcomponents/cohort/cohort-browser.js
+++ b/src/webcomponents/cohort/cohort-browser.js
@@ -15,7 +15,7 @@
*/
-import {LitElement, html} from "lit";
+import {LitElement, html, nothing} from "lit";
import UtilsNew from "../../core/utils-new.js";
import "../commons/opencga-browser.js";
import "./cohort-grid.js";
@@ -125,15 +125,18 @@ export default class CohortBrowser extends LitElement {
.config="${params.config.filter.result.grid}"
.eventNotifyName="${params.eventNotifyName}"
.active="${true}"
- @selectrow="${e => params.onClickRow(e, "cohort")}"
- @cohortUpdate="${e => params.onComponentUpdate(e, "cohort")}"
+ @selectrow="${e => params.onClickRow(e)}"
+ @cohortUpdate="${e => params.onComponentUpdate(e)}"
@settingsUpdate="${() => this.onSettingsUpdate()}">
-
- `
+ ${params?.detail ? html`
+
+
+ ` : nothing}
+ `,
},
{
id: "facet-tab",
diff --git a/src/webcomponents/cohort/cohort-create.js b/src/webcomponents/cohort/cohort-create.js
index 5f93b0a0f0..5969b812d5 100644
--- a/src/webcomponents/cohort/cohort-create.js
+++ b/src/webcomponents/cohort/cohort-create.js
@@ -204,14 +204,6 @@ export default class CohortCreate extends LitElement {
placeholder: "Add an ID",
}
},
- {
- title: "Name",
- field: "status.name",
- type: "input-text",
- display: {
- placeholder: "Add source name"
- }
- },
{
title: "Description",
field: "status.description",
diff --git a/src/webcomponents/cohort/cohort-grid.js b/src/webcomponents/cohort/cohort-grid.js
index 441f39d74b..913390c391 100644
--- a/src/webcomponents/cohort/cohort-grid.js
+++ b/src/webcomponents/cohort/cohort-grid.js
@@ -22,8 +22,9 @@ import CatalogGridFormatter from "../commons/catalog-grid-formatter.js";
import PolymerUtils from "../PolymerUtils.js";
import "../commons/opencb-grid-toolbar.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-import ModalUtils from "../commons/modal/modal-utils";
-import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils";
+import ModalUtils from "../commons/modal/modal-utils.js";
+import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import WebUtils from "../commons/utils/web-utils.js";
export default class CohortGrid extends LitElement {
@@ -78,7 +79,6 @@ export default class CohortGrid extends LitElement {
super.update(changedProperties);
}
-
updated(changedProperties) {
if (changedProperties.size > 0 && this.active) {
this.renderTable();
@@ -145,6 +145,8 @@ export default class CohortGrid extends LitElement {
// `
// }
};
+
+ this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE");
}
renderTable() {
@@ -343,27 +345,34 @@ export default class CohortGrid extends LitElement {
id: "actions",
title: "Actions",
field: "actions",
- formatter: () => `
-
-
-
- Actions
-
-
-
- `,
align: "center",
+ formatter: () => {
+ const hasWritePermission = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ this.permissionID,
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
+ return `
+
+
+
+ Actions
+
+
+
+ `;
+ },
events: {
"click a": this.onActionClick.bind(this),
},
@@ -413,7 +422,7 @@ export default class CohortGrid extends LitElement {
_.id,
_.samples ? _.samples.map(_ => `${_.id}`).join(",") : "",
_.creationDate ? CatalogGridFormatter.dateFormatter(_.creationDate) : "-",
- _.status.name,
+ _.status.id,
_.type
].join("\t"))];
UtilsNew.downloadData(dataString, "cohort_" + this.opencgaSession.study.id + ".tsv", "text/plain");
diff --git a/src/webcomponents/cohort/cohort-update.js b/src/webcomponents/cohort/cohort-update.js
index fb4522054e..f16613bca1 100644
--- a/src/webcomponents/cohort/cohort-update.js
+++ b/src/webcomponents/cohort/cohort-update.js
@@ -156,14 +156,6 @@ export default class CohortUpdate extends LitElement {
placeholder: "Add an ID",
}
},
- {
- title: "Name",
- field: "status.name",
- type: "input-text",
- display: {
- placeholder: "Add source name"
- }
- },
{
title: "Description",
field: "status.description",
diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js
index 8a49ddc4e7..3154dbd91d 100644
--- a/src/webcomponents/cohort/cohort-view.js
+++ b/src/webcomponents/cohort/cohort-view.js
@@ -239,7 +239,7 @@ export default class CohortView extends LitElement {
title: "Status",
type: "complex",
display: {
- template: "${internal.status.name} (${internal.status.date})",
+ template: "${internal.status.id} (${internal.status.date})",
format: {
"internal.status.date": date => UtilsNew.dateFormatter(date),
}
diff --git a/src/webcomponents/commons/analysis/analysis-utils.js b/src/webcomponents/commons/analysis/analysis-utils.js
index d5f1e3a1be..5d24d7ad6a 100644
--- a/src/webcomponents/commons/analysis/analysis-utils.js
+++ b/src/webcomponents/commons/analysis/analysis-utils.js
@@ -3,6 +3,7 @@ import NotificationUtils from "../utils/notification-utils";
import UtilsNew from "../../../core/utils-new";
import "../filters/feature-filter.js";
import "../filters/disease-panel-filter.js";
+import LitUtils from "../utils/lit-utils";
export default class AnalysisUtils {
@@ -14,7 +15,7 @@ export default class AnalysisUtils {
// }
static submit(id, promise, context) {
- promise
+ return promise
.then(response => {
console.log(response);
NotificationUtils.dispatch(context, NotificationUtils.NOTIFY_SUCCESS, {
@@ -22,7 +23,10 @@ export default class AnalysisUtils {
message: `${id} has been launched successfully`,
});
// Call to analysis onClear() method
- context.onClear();
+ if (typeof context.onClear === "function") {
+ context.onClear();
+ }
+ return response;
})
.catch(response => {
console.log(response);
@@ -114,13 +118,14 @@ export default class AnalysisUtils {
static getAnalysisConfiguration(id, title, description, paramSections, check, config = {}) {
return {
id: id,
- icon: config.icon,
+ icon: config.icon || "",
title: config.title || title,
description: config.description || description,
display: {
// defaultLayout: "vertical"
- ...config.display
+ ...config?.display
},
+ buttons: config?.buttons || {},
sections: [
{
display: {},
@@ -139,6 +144,9 @@ export default class AnalysisUtils {
...paramSections,
{
title: "Job Info",
+ display: {
+ visible: config.isJob !== undefined ? config.isJob : true,
+ },
elements: [
{
title: "Job ID",
diff --git a/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js b/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js
index b52131aeb5..5931632b0a 100644
--- a/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js
+++ b/src/webcomponents/commons/analysis/opencga-analysis-tool-form.js
@@ -101,7 +101,11 @@ export default class OpencgaAnalysisToolForm extends LitElement {
async updated(changedProperties) {
if (changedProperties.has("opencgaSession")) {
this.params.study = this.opencgaSession.study.fqn;
- this.runnable = OpencgaCatalogUtils.checkPermissions(this.opencgaSession.study, this.opencgaSession.user.id, "EXECUTE_JOBS");
+ this.runnable = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ "EXECUTE_JOBS",
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
this.requestUpdate();
await this.updateComplete;
// await this.updateComplete;
diff --git a/src/webcomponents/commons/catalog-grid-formatter.js b/src/webcomponents/commons/catalog-grid-formatter.js
index 5de4d88496..019df35779 100644
--- a/src/webcomponents/commons/catalog-grid-formatter.js
+++ b/src/webcomponents/commons/catalog-grid-formatter.js
@@ -19,6 +19,16 @@ import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js";
export default class CatalogGridFormatter {
+ static userStatusFormatter(status, config) {
+ const _config = config || [];
+ const currentStatus = status.id || status.name || "UNDEFINED"; // Get current status
+ const displayCurrentStatus = _config.find(status => status.id === currentStatus);
+ return `
+
+ ${displayCurrentStatus.displayLabel}
+
+ `;
+ }
static sexFormatter(value, row) {
let sexHtml = `${UtilsNew.isEmpty(row?.sex) ? "Not specified" : row.sex.id || row.sex}`;
if (row?.karyotypicSex && row.karyotypicSex !== "UNKNOWN") {
diff --git a/src/webcomponents/commons/filters/catalog-search-autocomplete.js b/src/webcomponents/commons/filters/catalog-search-autocomplete.js
index f087965bdd..d2ac72c217 100644
--- a/src/webcomponents/commons/filters/catalog-search-autocomplete.js
+++ b/src/webcomponents/commons/filters/catalog-search-autocomplete.js
@@ -80,8 +80,9 @@ export default class CatalogSearchAutocomplete extends LitElement {
this.RESOURCES = {
"PROJECT": {
searchField: "id",
- placeholder: "project...",
- client: this.opencgaSession.opencgaClient.projects(),
+ placeholder: "Project...",
+ // client: this.opencgaSession.opencgaClient.projects(),
+ fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.projects().search(params),
fields: item => ({
"name": item.id,
}),
@@ -90,24 +91,25 @@ export default class CatalogSearchAutocomplete extends LitElement {
}
},
"STUDY": {
- searchField: "id",
- placeholder: "study...",
- client: this.opencgaSession.opencgaClient.studies(),
+ searchField: "fqn",
+ placeholder: "Study...",
+ // client: this.opencgaSession.opencgaClient.studies(),
+ fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.studies().search(this.opencgaSession.project.id, params),
fields: item => ({
"name": item.id,
}),
query: {
- project: this.opencgaSession.project.id,
- include: "id,name"
+ include: "id,name,fqn"
}
},
"SAMPLE": {
searchField: "id",
placeholder: "HG01879, HG01880, HG01881...",
- client: this.opencgaSession.opencgaClient.samples(),
+ // client: this.opencgaSession.opencgaClient.samples(),
+ fetch: filters => this.opencgaSession.opencgaClient.samples().search(filters),
fields: item => ({
"name": item.id,
- "Individual ID": item?.individualId
+ "Individual ID": item?.individualId,
}),
query: {
include: "id,individualId"
@@ -116,7 +118,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"INDIVIDUAL": {
searchField: "id",
placeholder: "Start typing",
- client: this.opencgaSession.opencgaClient.individuals(),
+ // client: this.opencgaSession.opencgaClient.individuals(),
+ fetch: filters => this.opencgaSession.opencgaClient.individuals().search(filters),
fields: item => ({
"name": item.id
}),
@@ -127,7 +130,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"FAMILY": {
searchField: "id",
placeholder: "Start typing",
- client: this.opencgaSession.opencgaClient.families(),
+ // client: this.opencgaSession.opencgaClient.families(),
+ fetch: filters => this.opencgaSession.opencgaClient.families().search(filters),
fields: item => ({
"name": item.id
}),
@@ -139,7 +143,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"CLINICAL_ANALYSIS": {
searchField: "id",
placeholder: "Start typing",
- client: this.opencgaSession.opencgaClient.clinical(),
+ // client: this.opencgaSession.opencgaClient.clinical(),
+ fetch: filters => this.opencgaSession.opencgaClient.clinical().search(filters),
fields: item => ({
"name": item.id,
"Proband Id": item?.proband?.id
@@ -151,7 +156,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"DISEASE_PANEL": {
searchField: "id",
placeholder: "Start typing",
- client: this.opencgaSession.opencgaClient.panels(),
+ // client: this.opencgaSession.opencgaClient.panels(),
+ fetch: filters => this.opencgaSession.opencgaClient.panels().search(filters),
fields: item => ({
"name": item.id,
}),
@@ -162,7 +168,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"JOB": {
searchField: "id",
placeholder: "Start typing",
- client: this.opencgaSession.opencgaClient.jobs(),
+ // client: this.opencgaSession.opencgaClient.jobs(),
+ fetch: filters => this.opencgaSession.opencgaClient.jobs().search(filters),
fields: item => ({
"name": item.id,
}),
@@ -173,7 +180,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"COHORT": {
searchField: "id",
placeholder: "Start typing",
- client: this.opencgaSession.opencgaClient.cohorts(),
+ // client: this.opencgaSession.opencgaClient.cohorts(),
+ fetch: filters => this.opencgaSession.opencgaClient.cohorts().search(filters),
fields: item => ({
"name": item.id
}),
@@ -184,7 +192,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"FILE": {
searchField: "name",
placeholder: "eg. samples.tsv, phenotypes.vcf...",
- client: this.opencgaSession.opencgaClient.files(),
+ // client: this.opencgaSession.opencgaClient.files(),
+ fetch: filters => this.opencgaSession.opencgaClient.files().search(filters),
fields: item => ({
name: item.name,
Format: item.format ?? "N/A",
@@ -198,7 +207,8 @@ export default class CatalogSearchAutocomplete extends LitElement {
"DIRECTORY": {
searchField: "path",
placeholder: "eg. /data/platinum-grch38...",
- client: this.opencgaSession.opencgaClient.files(),
+ // client: this.opencgaSession.opencgaClient.files(),
+ fetch: filters => this.opencgaSession.opencgaClient.files().search(filters),
fields: item => ({
name: item.name,
path: `/${item.path.replace(`/${item.name}`, "")}`
@@ -207,7 +217,31 @@ export default class CatalogSearchAutocomplete extends LitElement {
type: "DIRECTORY",
include: "id,path",
}
- }
+ },
+ "NOTE_ORGANIZATION": {
+ searchField: "id",
+ placeholder: "Start typing",
+ // eslint-disable-next-line no-unused-vars
+ fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.organization().searchNotes(params),
+ fields: item => ({
+ "name": item.id,
+ }),
+ query: {
+ include: "id",
+ scope: "ORGANIZATION",
+ },
+ },
+ "NOTE_STUDY": {
+ searchField: "id",
+ placeholder: "Start typing",
+ fetch: ({study, ...params}) => this.opencgaSession.opencgaClient.studies().searchNotes(study, params),
+ fields: item => ({
+ "name": item.id,
+ }),
+ query: {
+ include: "id",
+ },
+ },
};
this._config = this.getDefaultConfig();
}
@@ -252,7 +286,7 @@ export default class CatalogSearchAutocomplete extends LitElement {
...attr,
};
- this.RESOURCES[this.resource].client.search(filters)
+ this.RESOURCES[this.resource].fetch(filters)
.then(response => success(response))
.catch(error => failure(error));
},
diff --git a/src/webcomponents/commons/filters/clinical-annotation-filter.js b/src/webcomponents/commons/filters/clinical-annotation-filter.js
index 4b9ec5325b..c41d19ef0a 100644
--- a/src/webcomponents/commons/filters/clinical-annotation-filter.js
+++ b/src/webcomponents/commons/filters/clinical-annotation-filter.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
+import {html, LitElement} from "lit";
import "../../commons/forms/select-field-filter.js";
import "../../commons/forms/checkbox-field-filter.js";
@@ -24,7 +24,7 @@ export default class ClinicalAnnotationFilter extends LitElement {
super();
// Set status and init private properties
- this._init();
+ this.#init();
}
createRenderRoot() {
@@ -48,7 +48,7 @@ export default class ClinicalAnnotationFilter extends LitElement {
};
}
- _init() {
+ #init() {
this.query = {};
}
@@ -98,7 +98,7 @@ export default class ClinicalAnnotationFilter extends LitElement {
render() {
return html`
-
+
Select Clinical Database
@@ -112,7 +112,7 @@ export default class ClinicalAnnotationFilter extends LitElement {
-
+
Select Clinical Significance
@@ -126,7 +126,7 @@ export default class ClinicalAnnotationFilter extends LitElement {
-
+
Check Status
diff --git a/src/webcomponents/commons/filters/disease-panel-filter.js b/src/webcomponents/commons/filters/disease-panel-filter.js
index b20e5d05ba..3a7a3fbc9f 100644
--- a/src/webcomponents/commons/filters/disease-panel-filter.js
+++ b/src/webcomponents/commons/filters/disease-panel-filter.js
@@ -48,7 +48,7 @@ export default class DiseasePanelFilter extends LitElement {
super();
// Set status and init private properties
- this._init();
+ this.#init();
}
createRenderRoot() {
@@ -97,7 +97,7 @@ export default class DiseasePanelFilter extends LitElement {
};
}
- _init() {
+ #init() {
this.query = {};
this.genes = [];
// this.panelFeatureType = "";
@@ -203,7 +203,7 @@ export default class DiseasePanelFilter extends LitElement {
return html`
-
+
${this.showExtendedFilters ? html`
Select Disease Panels
@@ -233,13 +233,14 @@ export default class DiseasePanelFilter extends LitElement {
}
${this.showExtendedFilters ? html`
-
+
Panel Intersection
this.filterChange(e, "panelIntersection")}">
@@ -249,7 +250,7 @@ export default class DiseasePanelFilter extends LitElement {
-
+
Filter by Feature Type
@@ -264,7 +265,7 @@ export default class DiseasePanelFilter extends LitElement {
-
+
Filter Genes by Mode of Inheritance
@@ -279,7 +280,7 @@ export default class DiseasePanelFilter extends LitElement {
-
+
Filter Genes by Confidence
@@ -294,7 +295,7 @@ export default class DiseasePanelFilter extends LitElement {
-
+
Filter Genes by Role in Cancer
diff --git a/src/webcomponents/commons/filters/protein-substitution-score-filter.js b/src/webcomponents/commons/filters/protein-substitution-score-filter.js
index 95fab66aa6..045be9464f 100644
--- a/src/webcomponents/commons/filters/protein-substitution-score-filter.js
+++ b/src/webcomponents/commons/filters/protein-substitution-score-filter.js
@@ -177,6 +177,7 @@ export default class ProteinSubstitutionScoreFilter extends LitElement {
+
+ `;
+ }
+
getDefaultConfig() {
// HOM_REF, HOM_ALT, HET, HET_REF, HET_ALT and MISS e.g. HG0097:HOM_REF;HG0098:HET_REF,HOM_ALT . 3)
return {
genotypes: [
{
- id: "0/1", name: "HET"
+ id: "0/1", name: "Heterozygous (0/1)"
},
{
- id: "1/1", name: "HOM_ALT"
+ id: "1/1", name: "Homozygous Alternate (1/1)"
},
{
separator: true
},
{
- id: "1/2", name: "BIALLELIC (1/2)"
+ id: "1", name: "Haploid (1)"
+ },
+ {
+ id: "1/2", name: "Biallelic (1/2)"
},
+ {
+ id: "NA", name: "NA"
+ }
// {
// id: "./.", name: "Missing"
// },
- // {
- // id: "NA", name: "NA"
- // }
]
};
}
- render() {
- const genotypes = this.genotypes ?? [];
- return html`
-
-
- `;
- }
-
}
customElements.define("sample-genotype-filter", SampleGenotypeFilter);
diff --git a/src/webcomponents/commons/filters/study-filter.js b/src/webcomponents/commons/filters/study-filter.js
index c12a27aed3..f6442c4de2 100644
--- a/src/webcomponents/commons/filters/study-filter.js
+++ b/src/webcomponents/commons/filters/study-filter.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {LitElement, html, nothing} from "lit";
+import {html, LitElement, nothing} from "lit";
import UtilsNew from "../../../core/utils-new.js";
import LitUtils from "../utils/lit-utils.js";
import "../forms/select-field-filter.js";
@@ -33,11 +33,14 @@ export default class StudyFilter extends LitElement {
static get properties() {
return {
opencgaSession: {
- type: Object
+ type: Object,
},
value: {
type: String,
},
+ config: {
+ type: Object,
+ },
};
}
@@ -68,10 +71,10 @@ export default class StudyFilter extends LitElement {
opencgaSessionObserver() {
this._studies = [];
if (this.opencgaSession?.project?.studies?.length) {
- // 1. Add current study as the first element and mark it as disabled
+ // 1. Add current active study as the first element and mark it as disabled
this._studies.push({
- name: this.opencgaSession.study.name,
id: this.opencgaSession.study.fqn,
+ name: this.opencgaSession.study.name,
selected: true,
disabled: true,
});
@@ -115,7 +118,7 @@ export default class StudyFilter extends LitElement {
}
render() {
- if (!this.opencgaSession && !this.opencgaSession.project) {
+ if (!this.opencgaSession || !this.opencgaSession.project) {
return nothing;
}
diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js
index 8d2fc1d19a..7177f89055 100644
--- a/src/webcomponents/commons/forms/data-form.js
+++ b/src/webcomponents/commons/forms/data-form.js
@@ -816,7 +816,7 @@ export default class DataForm extends LitElement {
` : nothing}
${hasErrorMessages ? html`
-
+
@@ -837,7 +837,7 @@ export default class DataForm extends LitElement {
const content = html`
${element.display?.icon ? html`
-
+
` : nothing}
${value || ""}
@@ -930,14 +930,16 @@ export default class DataForm extends LitElement {
const content = html`
- this.onFilterChange(element, e.currentTarget.checked)}">
- ${element.text}
+
+ this.onFilterChange(element, e.currentTarget.checked)}">
+ ${element.text}
+
`;
@@ -972,17 +974,14 @@ export default class DataForm extends LitElement {
_createToggleButtonsElement(element) {
const value = this.getValue(element.field);
- const names = element.allowedValues;
- const activeClassName = element.display?.activeClassName ?? element.display?.activeClass ?? "";
- const inactiveClassName = element.display?.inactiveClassName ?? element.display?.inactiveClass ?? "";
-
+ const allowedValues = element.allowedValues || [];
+ const disabled = this._getBooleanValue(element.display?.disabled, false, element);
const content = html`
this.onFilterChange(element, e.detail.value)}">
`;
@@ -1355,7 +1354,8 @@ export default class DataForm extends LitElement {
content = this._createImageElement(elem);
break;
case "custom":
- content = elem.display?.render(this.getValue(elem.field, row));
+ // content = elem.display?.render(this.getValue(elem.field, row));
+ content = elem.display?.render(this.getValue(elem.field, row), value => this.onFilterChange(elem, value), this.updateParams, this.data, row);
break;
default:
content = this.getValue(elem.field, row, this._getDefaultValue(element, section), elem.display);
diff --git a/src/webcomponents/commons/forms/deprecated/select-field-filter.js b/src/webcomponents/commons/forms/deprecated/select-field-filter.js
deleted file mode 100644
index ac45b76bb1..0000000000
--- a/src/webcomponents/commons/forms/deprecated/select-field-filter.js
+++ /dev/null
@@ -1,292 +0,0 @@
-/**
- * Copyright 2015-2019 OpenCB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {html, LitElement, nothing} from "lit";
-import UtilsNew from "../../../../core/utils-new.js";
-import LitUtils from "../../utils/lit-utils.js";
-
-// TODO reorganize props multiple/forceSelection
-
-/** NOTE - Design choice: to allow deselection, the single mode (this.multiple=false), has been implemented with the multiple flag in bootstrap-select, but forcing 1 selection with data-max-options=1
- * (this has no consequences for the developer point of view). This behaviour can be over overridden using "forceSelection" prop.
- *
- * NOTE putting names in data-content attr instead of as
content itself allows HTML entities to be correctly decoded.
- *
- * Usage:
- * console.log(e)}">
- * console.log(e)}">
- */
-
-// DEPRECATED
-export default class SelectFieldFilter extends LitElement {
-
- constructor() {
- super();
-
- // Set status and init private properties
- this._init();
- }
-
- createRenderRoot() {
- return this;
- }
-
- static get properties() {
- return {
- // NOTE value (default Values) can be either a single value as string or a comma separated list (in case of multiple=true we support array of strings)
- value: {
- type: String
- },
- title: {
- type: String
- },
- placeholder: {
- type: String
- },
- multiple: {
- type: Boolean
- },
- all: {
- type: Boolean
- },
- disabled: {
- type: Boolean
- },
- required: {
- type: Boolean
- },
- maxOptions: {
- type: Number
- },
- liveSearch: {
- type: Boolean
- },
- forceSelection: {
- type: Boolean
- },
- classes: {
- type: String
- },
- size: {
- type: Number,
- },
- separator: {
- type: String,
- },
- // the expected format is either an array of string or an array of objects {id, name}
- data: {
- type: Object
- }
- };
- }
-
- _init() {
- this._prefix = UtilsNew.randomString(8);
- $.fn.selectpicker.Constructor.BootstrapVersion = "5";
- this.multiple = false;
- this.all = false;
- this.data = [];
- this.classes = "";
- this.elm = this._prefix + "selectpicker";
- this.size = 20; // Default size
- this.separator = ","; // Default separator
- }
-
- firstUpdated() {
- this.selectPicker = $("#" + this.elm, this);
- this.selectPicker.selectpicker({
- iconBase: "fas",
- tickIcon: "fa-check",
- val: "",
- multipleSeparator: this.separator,
- style: "",
- styleBase: this.all ? "form-control rounded-end-0" : "form-control"
- // actionsBox: this.all,
- });
- // this.selectPicker.selectpicker("val", "");
- }
-
- updated(changedProperties) {
- if (changedProperties.has("data")) {
- // TODO check why lit-element execute this for all existing select-field-filter instances..wtf
- this.data = this.data ?? [];
- // 20230604 rodiel: The latest version of bootstrap-select is causing a duplication issue with the list.
- // To prevent this, it's necessary to disable refresh.
- // Further investigation is necessary to ensure a solution
- // this.selectPicker.selectpicker("refresh");
- // https://developer.snapappointments.com/bootstrap-select/methods/#:~:text=%27deselectAll%27)%3B-,.selectpicker(%27render%27),-You%20can%20force
- // Solution:
- this.selectPicker.selectpicker("render");
- }
-
- if (changedProperties.has("disabled")) {
- this.selectPicker.selectpicker("render");
- }
-
- if (changedProperties.has("value") || changedProperties.has("data") || changedProperties.has("disabled")) {
- let val = "";
- if (this.value) {
- if (this.multiple) {
- if (Array.isArray(this.value)) {
- val = this.value;
- } else {
- val = this.value.split(",");
- }
- } else {
- val = this.value;
- }
- }
- // CAUTION 20230309 Vero: bug reported where selected disabled option is not stored in val.
- // this.selectPicker.selectpicker("val", val), it is not setting the array val if val is disabled
- // https://github.com/snapappointments/bootstrap-select/issues/1823#event-4943462544
- this.selectPicker.selectpicker("val", val);
- }
-
- if (changedProperties.has("classes")) {
- if (this.classes) {
- this.selectPicker.selectpicker("setStyle", this.classes, "add");
- } else {
- // if classes os removed then we need to removed the old assigned classes
- this.selectPicker.selectpicker("setStyle", changedProperties.get("classes"), "remove");
- this.selectPicker.selectpicker("setStyle", "border-secondary-subtle", "add");
- }
- }
- }
-
- filterChange(e) {
- // CAUTION 20230309 Vero: bug reported where selected disabled option is not stored in val.
- // https://github.com/snapappointments/bootstrap-select/issues/1823#event-4943462544
- // Possible solution:
- const disabled = Object.values(e.target.options).filter(data => data.disabled === true).map(data => {
- if (data.selected) {
- return data.value;
- }
- });
- // const selection = this.selectPicker.selectpicker("val");
- const selection = Array.isArray(this.selectPicker.selectpicker("val")) ?
- [...this.selectPicker.selectpicker("val"), ...disabled] :
- this.selectPicker.selectpicker("val");
-
- let val = null;
- if (selection && selection.length) {
- if (this.multiple) {
- val = selection.join(",");
- } else {
- if (this.forceSelection) {
- // single mode that DOESN'T allow deselection
- // forceSelection means multiple flag in selectpicker is false, this is the only case `selection` is not an array
- val = selection;
- } else {
- // single mode that allows deselection
- val = selection[0];
- }
- }
- }
-
- LitUtils.dispatchCustomEvent(this, "filterChange", val, {
- data: this.data,
- }, null, {bubbles: false, composed: false});
- }
-
- selectAll(e) {
- if (e.currentTarget.checked) {
- if (this.data[0].fields) {
- this.value = this.data.map(data => data.fields).flat().map(data => data.id ?? data.name);
- } else {
- this.value = this.data.map(data => data.id ?? data.name);
- }
- } else {
- if (this.data[0].fields) {
- this.value = this.data.map(data => data.fields).flat().filter(data => data.disabled === true).map(data => data.id ?? data.name);
- } else {
- this.value = this.data.filter(data => data.disabled === true).map(data => data.id ?? data.name);
- }
- }
-
- // Notify to event to allow parent components to react
- LitUtils.dispatchCustomEvent(this, "filterChange", this.value?.length > 0 ? this.value.join(",") : "", {
- data: this.data,
- }, null, {bubbles: false, composed: false});
- }
-
- renderOption(option) {
- let dataContent;
- if (option.description) {
- dataContent = `${option.name ?? option.id} `;
- } else {
- dataContent = `${option.name ?? option.id} `;
- }
- return html`
-
-
- `;
- }
-
- renderShowSelectAll() {
- return html `
-
-
- All
-
- `;
- }
-
- render() {
- return html`
-
-
-
- ${this.data?.map(opt => html`
- ${opt?.separator ? html` ` : html`
- ${opt?.fields?.length > 0 ? html`
-
- ${opt.fields.map(subopt => html`
- ${UtilsNew.isObject(subopt) ? this.renderOption(subopt) : html`${subopt} `}
- `)}
-
- ` : html`
- ${UtilsNew.isObject(opt) ? this.renderOption(opt) : html`${opt} `}
- `}
- `}
- `)}
-
-
- ${this.all ? this.renderShowSelectAll() : nothing}
-
-
- `;
- }
-
-}
-
-customElements.define("select-field-filter", SelectFieldFilter);
diff --git a/src/webcomponents/commons/forms/number-field-filter.js b/src/webcomponents/commons/forms/number-field-filter.js
index d8dd6255a2..fd8a417cee 100644
--- a/src/webcomponents/commons/forms/number-field-filter.js
+++ b/src/webcomponents/commons/forms/number-field-filter.js
@@ -123,18 +123,6 @@ export default class NumberFieldFilter extends LitElement {
this.dispatchEvent(event);
}
- getDefaultConfig() {
- const wantedComparators = (this.comparators || "<,<=,=,>=,>").split(",");
- return {
- layout: [3, 4, 5], // in case the label is not needed the expected value of the first element is 0
- comparator: true,
- comparatorForceSelection: true,
- values: this.defaultComparators.filter(item => {
- return wantedComparators.includes(item.id);
- }),
- };
- }
-
render() {
return html`
@@ -150,6 +138,7 @@ export default class NumberFieldFilter extends LitElement {
=,>").split(",");
+ return {
+ layout: [3, 4, 5], // in case the label is not needed the expected value of the first element is 0
+ comparator: true,
+ comparatorForceSelection: true,
+ values: this.defaultComparators.filter(item => {
+ return wantedComparators.includes(item.id);
+ }),
+ };
+ }
+
}
customElements.define("number-field-filter", NumberFieldFilter);
diff --git a/src/webcomponents/commons/forms/select-field-filter.js b/src/webcomponents/commons/forms/select-field-filter.js
index 701de93bd4..1b67ae6ed5 100644
--- a/src/webcomponents/commons/forms/select-field-filter.js
+++ b/src/webcomponents/commons/forms/select-field-filter.js
@@ -53,6 +53,9 @@ export default class SelectFieldFilter extends LitElement {
separator: {
type: String,
},
+ forceSelection: {
+ type: Boolean,
+ },
config: {
type: Object
}
@@ -81,8 +84,7 @@ export default class SelectFieldFilter extends LitElement {
}
updated(changedProperties) {
- if (changedProperties.has("data")) {
- // this._config = {...this.getDefaultConfig(), ...this.config};
+ if (changedProperties.has("data") || changedProperties.has("config")) {
this.loadData();
}
@@ -106,6 +108,11 @@ export default class SelectFieldFilter extends LitElement {
}
this.select.empty();
+
+ // Force to unbind 'select2:select' and 'select2:unselect' listeners
+ this.select.off("select2:select");
+ this.select.off("select2:unselect");
+
const options = this.data.map(item => this.getOptions(item));
const selectConfig = {
@@ -115,7 +122,7 @@ export default class SelectFieldFilter extends LitElement {
selectionCssClass: this._config?.selectionClass ? this.config?.selectionClass : "",
multiple: !!this._config?.multiple,
placeholder: this._config?.placeholder ?? "Select an option",
- allowClear: !!this._config?.multiple,
+ allowClear: !this.forceSelection,
disabled: this._config?.disabled ?? false,
width: "80%",
data: options,
@@ -301,29 +308,15 @@ export default class SelectFieldFilter extends LitElement {
return {
id: item.id,
- text: item?.name || item?.id
+ text: item?.name || item?.id,
+ selected: item.selected ?? false,
+ disabled: item.disabled ?? false
};
}
- filterChange(e) {
- const disabled = Object.values(e.target.options)
- .filter(data => data.disabled === true)
- .map(data => {
- if (data.selected) {
- return data.value;
- }
- });
-
- const selection = Array.isArray(this.select.select2("data")) ?
- [...this.select.select2("data").map(el => el.id), ...disabled] :
- this.select.select2("data").map(el => el.id);
+ filterChange() {
+ const selection = this.select.select2("data").map(el => el.id);
- let val = "";
- if (selection && selection.length) {
- if (this._config?.multiple) {
- val = selection.join(",");
- }
- }
LitUtils.dispatchCustomEvent(this, "filterChange", selection.join(","),
{}, null, {bubbles: false, composed: false});
}
diff --git a/src/webcomponents/commons/forms/toggle-buttons.js b/src/webcomponents/commons/forms/toggle-buttons.js
index 09191b4414..437ce85b25 100644
--- a/src/webcomponents/commons/forms/toggle-buttons.js
+++ b/src/webcomponents/commons/forms/toggle-buttons.js
@@ -18,17 +18,11 @@ import {LitElement, html} from "lit";
import UtilsNew from "../../../core/utils-new.js";
import LitUtils from "../utils/lit-utils.js";
-/**
- * Usage:
- *
- */
export default class ToggleButtons extends LitElement {
constructor() {
super();
-
- // Set status and init private properties
- this._init();
+ this.#init();
}
createRenderRoot() {
@@ -37,18 +31,12 @@ export default class ToggleButtons extends LitElement {
static get properties() {
return {
- names: {
+ data: {
type: Array
},
value: {
type: String
},
- activeClass: {
- type: String
- },
- inactiveClass: {
- type: String
- },
disabled: {
type: Boolean
},
@@ -58,106 +46,37 @@ export default class ToggleButtons extends LitElement {
};
}
- _init() {
+ #init() {
this._prefix = UtilsNew.randomString(8);
- this._nameClass = {};
-
- // Default values
- this.activeClass = "btn-primary";
- this.inactiveClass = "btn-light";
this.classes = "";
}
- updated(changedProperties) {
- if (changedProperties.has("names")) {
- this._propertyObserver();
- }
- if (changedProperties.has("value")) {
- this._propertyObserver();
- this._value = this.value;
- }
- if (changedProperties.has("activeClass")) {
- this.activeClass = this.activeClass ? this.activeClass : "btn-primary";
- this._propertyObserver();
- }
- if (changedProperties.has("inactiveClass")) {
- this.inactiveClass = this.inactiveClass ? this.inactiveClass : "btn-light";
- this._propertyObserver();
- }
- }
-
- _propertyObserver() {
- if (this.names && this.value && this.activeClass && this.inactiveClass) {
- this._nameClass = {};
- for (const name of this.names) {
- if (name === this.value) {
- this._nameClass[name] = this.activeClass + " active";
- } else {
- this._nameClass[name] = this.inactiveClass;
- }
- }
- this.requestUpdate();
- }
- }
-
- onToggleClick(buttonName, e) {
- // Check if there is anything to do
- if (this.value === buttonName) {
- return;
- }
-
- // Support several classes
- const activeClasses = this.activeClass.split(" ");
- const inactiveClasses = this.inactiveClass.split(" ");
-
- // Fetch and reset buttons status
- const buttons = this.getElementsByClassName("btn-toggle-" + this._prefix);
- buttons.forEach(button => button.classList.remove(...activeClasses, ...inactiveClasses, "active"));
-
- // Set proper classes
- this.value = buttonName;
- for (const button of buttons) {
- if (button.dataset.id === this.value) {
- button.classList.add(...activeClasses, "active");
- } else {
- button.classList.add(...inactiveClasses);
- }
- }
-
- // Set the field status
- this.filterChange();
- }
-
filterChange(BtnName) {
LitUtils.dispatchCustomEvent(this, "filterChange", BtnName);
}
- render() {
- if (!this.names) {
- return;
- }
+ renderButtonItem(item) {
+ const value = item?.id || item;
+ return html`
+ this.filterChange(value)}">
+
+ ${item?.name ?? item?.text ?? item?.id ?? item}
+
+ `;
+ }
+ render() {
return html`
- ${
- this.names?.map(name => html`
- this.filterChange(name)}" id="${this._prefix + name}" type="radio"
- name="${this._prefix}BtnRadio" data-id="${name}">
-
- ${name}
-
- `)
- }
+ ${(this.data || []).map(item => this.renderButtonItem(item))}
-
`;
}
diff --git a/src/webcomponents/commons/forms/toggle-radio.js b/src/webcomponents/commons/forms/toggle-radio.js
index 7d6299295c..cbcf846949 100644
--- a/src/webcomponents/commons/forms/toggle-radio.js
+++ b/src/webcomponents/commons/forms/toggle-radio.js
@@ -18,10 +18,6 @@ import {LitElement, html} from "lit";
import UtilsNew from "../../../core/utils-new.js";
import LitUtils from "../utils/lit-utils.js";
-/**
- * Usage:
- *
- */
export default class ToggleRadio extends LitElement {
constructor() {
@@ -35,58 +31,49 @@ export default class ToggleRadio extends LitElement {
static get properties() {
return {
+ data: {
+ type: Array,
+ },
value: {
- type: Boolean
+ type: Boolean,
},
- config: {
- type: Object
+ disabled: {
+ type: Boolean,
},
};
}
#init() {
this._prefix = UtilsNew.randomString(8);
- this._config = {...this.getDefaultConfig()};
- }
-
- update(changedProperties) {
- if (changedProperties.has("value")) {
- this._value = this.value;
- }
- if (changedProperties.has("config")) {
- this._config = {...this.getDefaultConfig(), ...this.config};
- }
- super.update(changedProperties);
}
- filterChange(val) {
- LitUtils.dispatchCustomEvent(this, "filterChange", val === "ON");
+ onFilterChange(value) {
+ LitUtils.dispatchCustomEvent(this, "filterChange", value);
}
render() {
- return html`
-
- this.filterChange("ON")}"
- ?disabled="${this._config.disabled}">
- ${this._config.onText}
-
-
- this.filterChange("OFF")}" ?checked="${!this.value}"
- name="inlineRadioOptions" id="${this._prefix}offToggle" value="OFF" ?disabled="${this._config.disabled}">
- ${this._config.offText}
-
- `;
- }
-
- getDefaultConfig() {
- return {
- onText: "ON",
- offText: "OFF",
- disabled: false
- };
+ return (this.data || []).map(item => {
+ // Allowed values for data property
+ // 1. Array of objects: [{id: "", name: ""}, ...]
+ // 2. Array of values: ["ON", "OFF"]
+ const value = item?.id ?? item;
+ return html`
+
+ this.onFilterChange(value)}">
+
+ ${item?.name ?? item?.text ?? item?.id ?? item}
+
+
+ `;
+ });
}
}
diff --git a/src/webcomponents/commons/grid-commons.js b/src/webcomponents/commons/grid-commons.js
index 4c7d0a6cf0..85d43d56ad 100644
--- a/src/webcomponents/commons/grid-commons.js
+++ b/src/webcomponents/commons/grid-commons.js
@@ -339,13 +339,12 @@ export default class GridCommons {
return columns;
}
- displayResponseWarningEvents(response) {
+ displayResponseWarningEvents(response, maxVisibleEvents = 3) {
const eventsContainer = this.context.querySelector(`div#${this.gridId}WarningEvents`);
if (eventsContainer && (response?.events?.length > 0 || response?.responses?.[0]?.events?.length > 0)) {
const events = [...(response?.events || []), ...(response?.responses?.[0]?.events || [])]
- .filter(event => event && event.type === "WARNING" && !!event.message);
- if (events.length > 0) {
- const eventsMessages = events.map(event => {
+ .filter(event => event && event.type === "WARNING" && !!event.message)
+ .map(event => {
return `
@@ -353,7 +352,43 @@ export default class GridCommons {
`;
});
- eventsContainer.replaceChildren(UtilsNew.renderHTML(eventsMessages.join("")));
+ if (events.length > 0) {
+ const defaultVisibleEvents = events.length > maxVisibleEvents ? events.slice(0, maxVisibleEvents) : events;
+ const defaultHiddenEvents = events.length > maxVisibleEvents ? events.slice(maxVisibleEvents) : [];
+ const eventsMessages = UtilsNew.renderHTML(`
+
+ ${defaultVisibleEvents.join("")}
+ ${defaultHiddenEvents.length > 0 ? `
+
${defaultHiddenEvents.join("")}
+
+
+ Show more warning events (${defaultHiddenEvents.length} )
+
+
+ Show less warning events
+
+
+ ` : ""}
+
+ `).querySelector("div");
+ eventsContainer.replaceChildren(eventsMessages);
+ if (defaultHiddenEvents.length > 0) {
+ const hiddenEventsElement = eventsMessages.querySelector(`div[data-role="hidden-events"]`);
+ const showMoreEventsElement = eventsMessages.querySelector(`div[data-role="show-more-events"]`);
+ const showLessEventsElement = eventsMessages.querySelector(`div[data-role="show-less-events"]`);
+ // Show more events click
+ showMoreEventsElement.addEventListener("click", () => {
+ hiddenEventsElement.style.display = "block";
+ showLessEventsElement.style.display = "inline-block";
+ showMoreEventsElement.style.display = "none";
+ });
+ // Show less events click
+ showLessEventsElement.addEventListener("click", () => {
+ hiddenEventsElement.style.display = "none";
+ showLessEventsElement.style.display = "none";
+ showMoreEventsElement.style.display = "inline-block";
+ });
+ }
}
}
}
diff --git a/src/webcomponents/commons/json-editor.js b/src/webcomponents/commons/json-editor.js
index 75048b3ec1..8284098d00 100644
--- a/src/webcomponents/commons/json-editor.js
+++ b/src/webcomponents/commons/json-editor.js
@@ -50,10 +50,17 @@ export default class JsonEditor extends LitElement {
this._prefix = UtilsNew.randomString(8);
this.jsonEditor = null;
this.jsonEditorId = this._prefix + "jsoneditor";
- this._data = "";
+ this._data = {};
this._config = this.getDefaultConfig();
}
+ firstUpdated(changedProperties) {
+ if (changedProperties.has("config")) {
+ console.log("init data...");
+ this._data = this._config?.initAsArray ? [] : {};
+ }
+ }
+
update(changedProperties) {
if (changedProperties.has("config")) {
this._config = {...this.getDefaultConfig(), ...this.config};
@@ -141,6 +148,7 @@ export default class JsonEditor extends LitElement {
mode: "text", // Two accepted values: text, tree.
indentation: 4,
readOnly: false,
+ initAsArray: false,
showDownloadButton: true
};
}
diff --git a/src/webcomponents/commons/layouts/custom-footer.js b/src/webcomponents/commons/layouts/custom-footer.js
index df27763fad..336d984f49 100644
--- a/src/webcomponents/commons/layouts/custom-footer.js
+++ b/src/webcomponents/commons/layouts/custom-footer.js
@@ -151,7 +151,7 @@ export default class CustomFooter extends LitElement {
${this.host?.opencga ? html `
diff --git a/src/webcomponents/commons/layouts/custom-vertical-navbar.js b/src/webcomponents/commons/layouts/custom-vertical-navbar.js
index aa88e9c141..cc837dfc53 100644
--- a/src/webcomponents/commons/layouts/custom-vertical-navbar.js
+++ b/src/webcomponents/commons/layouts/custom-vertical-navbar.js
@@ -35,6 +35,9 @@ export default class CustomVerticalNavBar extends LitElement {
// --- PROPERTIES ---
static get properties() {
return {
+ organization: {
+ type: Object,
+ },
studyId: {
type: String,
},
@@ -80,7 +83,7 @@ export default class CustomVerticalNavBar extends LitElement {
// --- LIT LIFECYCLE ---
update(changedProperties) {
- if (changedProperties.has("studyId") || changedProperties.has("opencgaSession")) {
+ if (changedProperties.has("studyId")) {
this.studyIdObserver();
}
if (changedProperties.has("activeMenuItem")) {
@@ -99,11 +102,13 @@ export default class CustomVerticalNavBar extends LitElement {
// --- OBSERVERS ---
studyIdObserver() {
- for (const project of this.opencgaSession?.projects) {
- for (const study of project.studies) {
- if (study.id === this.studyId || study.fqn === this.studyId) {
- this.study = study;
- break;
+ if (this.studyId && this.opencgaSession) {
+ for (const project of this.opencgaSession?.projects) {
+ for (const study of project.studies) {
+ if (study.id === this.studyId || study.fqn === this.studyId) {
+ this.study = study;
+ break;
+ }
}
}
}
@@ -301,7 +306,11 @@ export default class CustomVerticalNavBar extends LitElement {
- ${subItem.render(this.opencgaSession, this.study)}
+ ${
+ (this.organization) ?
+ subItem.render(this.opencgaSession, this.organization) :
+ subItem.render(this.opencgaSession, this.study)
+ }
`)
@@ -339,9 +348,7 @@ export default class CustomVerticalNavBar extends LitElement {
// --- DEFAULT CONFIG ---
getDefaultConfig() {}
-
}
customElements.define("custom-vertical-navbar", CustomVerticalNavBar);
-
diff --git a/src/webcomponents/commons/opencb-grid-toolbar.js b/src/webcomponents/commons/opencb-grid-toolbar.js
index b83fb2239e..87d527a5de 100644
--- a/src/webcomponents/commons/opencb-grid-toolbar.js
+++ b/src/webcomponents/commons/opencb-grid-toolbar.js
@@ -21,6 +21,7 @@ import LitUtils from "./utils/lit-utils";
import ModalUtils from "./modal/modal-utils.js";
import "./opencga-export.js";
import "../variant/interpretation/variant-interpreter-grid-config.js";
+import WebUtils from "./utils/web-utils.js";
export default class OpencbGridToolbar extends LitElement {
@@ -74,6 +75,8 @@ export default class OpencbGridToolbar extends LitElement {
...this.getDefaultConfig(),
...this.config,
};
+
+ this.permissionID = WebUtils.getPermissionID(this._config.resource, "WRITE");
}
super.update(changedProperties);
@@ -92,7 +95,9 @@ export default class OpencbGridToolbar extends LitElement {
const action = e.currentTarget.dataset.action;
switch (action) {
case "create":
- ModalUtils.show(`${this._prefix}CreateModal`);
+ this._config.create?.modalId ?
+ ModalUtils.show(this._config.create.modalId) :
+ ModalUtils.show(`${this._prefix}CreateModal`);
break;
case "export":
ModalUtils.show(`${this._prefix}ExportModal`);
@@ -111,16 +116,22 @@ export default class OpencbGridToolbar extends LitElement {
rightButtons.push(rightButton.render());
}
}
+ // Button create text
+ const buttonCreateText = this._settings?.buttonCreateText || "New...";
// Check 'Create' permissions
let isCreateDisabled = false;
let isCreateDisabledTooltip = "";
+ const hasPermissions = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession?.study,
+ this.opencgaSession?.user?.id,
+ this.permissionID,
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
+
if (this._config?.create?.display?.disabled) {
isCreateDisabled = true;
isCreateDisabledTooltip = this._config?.create?.display?.disabledTooltip;
} else {
- const hasPermissions = OpencgaCatalogUtils
- .checkPermissions(this.opencgaSession?.study, this.opencgaSession?.user?.id, `WRITE_${this._config.resource}`);
if (!hasPermissions) {
isCreateDisabled = true;
isCreateDisabledTooltip = "Creating a new instance requires write permissions on the study. Please, contact your administrator if you need different access rights.";
@@ -129,7 +140,7 @@ export default class OpencbGridToolbar extends LitElement {
return html`
`);
@@ -316,7 +317,7 @@ export default class OpencgaBrowser extends LitElement {
renderButtonViews() {
return html `
-
+
${(this._config.views || []).map(view => html`
-
- ${this._config.searchButtonText || "Search"}
+
+ ${this._config.searchButtonText || "Search"}
@@ -361,7 +362,7 @@ export default class OpencgaBrowser extends LitElement {
role="tab"
type="button"
data-bs-target="#filters_tab"
- data-bs-toggle="tab">Filters
+ data-bs-toggle="tab">Filters
${this._config.aggregation ? html`
@@ -374,7 +375,7 @@ export default class OpencgaBrowser extends LitElement {
role="tab"
type="button"
data-bs-toggle="tab"
- data-bs-target="#facet_tab">Aggregation
+ data-bs-target="#facet_tab">
Aggregation
` : null}
diff --git a/src/webcomponents/commons/opencga-update.js b/src/webcomponents/commons/opencga-update.js
index 632bdb6a49..f8af50458d 100644
--- a/src/webcomponents/commons/opencga-update.js
+++ b/src/webcomponents/commons/opencga-update.js
@@ -323,6 +323,14 @@ export default class OpencgaUpdate extends LitElement {
},
];
break;
+ case "NOTE":
+ this.endpoint = this.component?.scope === "ORGANIZATION" ?
+ this.opencgaSession.opencgaClient.organization() :
+ this.opencgaSession.opencgaClient.studies();
+ this.methodUpdate = "updateNotes";
+ this.resourceUpdateParams = {
+ tagsAction: "SET",
+ };
}
}
}
@@ -345,21 +353,23 @@ export default class OpencgaUpdate extends LitElement {
onFieldChange(e, field) {
const param = field || e.detail.param;
- this.updatedFields = FormUtils.getUpdatedFields(
- this.component,
- this.updatedFields,
- param,
- e.detail.value,
- e.detail.action);
-
- // Notify to parent components in case the want to perform any other action, for instance, get the gene info in the disease panels.
- LitUtils.dispatchCustomEvent(this, "componentFieldChange", e.detail.value, {
- component: this._component,
- updatedFields: this.updatedFields,
- action: e.detail.action,
- param: param,
- });
- this.requestUpdate();
+ if (param) {
+ this.updatedFields = FormUtils.getUpdatedFields(
+ this.component,
+ this.updatedFields,
+ param,
+ e.detail.value,
+ e.detail.action);
+
+ // Notify to parent components in case the want to perform any other action, for instance, get the gene info in the disease panels.
+ LitUtils.dispatchCustomEvent(this, "componentFieldChange", e.detail.value, {
+ component: this._component,
+ updatedFields: this.updatedFields,
+ action: e.detail.action,
+ param: param,
+ });
+ this.requestUpdate();
+ }
}
onClear() {
@@ -415,9 +425,28 @@ export default class OpencgaUpdate extends LitElement {
this.#setLoading(true);
const endpointMethod = this.methodUpdate || "update";
// CAUTION: workaround for clinical-interpreation singular API
- const updateFunction = (this.resource === "CLINICAL_INTERPRETATION") ?
- this.endpoint[endpointMethod](this.component.clinicalAnalysisId, this.component.id, updateParams, params) :
- this.endpoint[endpointMethod](this.component.id, updateParams, params);
+
+ // updateFunction
+ let updateFunction = "";
+ switch (this.resource) {
+ case "NOTE":
+ if (this.component.scope === "ORGANIZATION") {
+ updateFunction = this.endpoint[endpointMethod](this.component.id, updateParams, params);
+ } else {
+ const {study, ...noteParams} = params;
+ updateFunction = this.endpoint[endpointMethod](study, this.component.id, updateParams, noteParams);
+ }
+ break;
+ case "CLINICAL_INTERPRETATION":
+ updateFunction = this.endpoint[endpointMethod](this.component.clinicalAnalysisId, this.component.id, updateParams, params);
+ break;
+ default:
+ updateFunction = this.endpoint[endpointMethod](this.component.id, updateParams, params);
+ break;
+ }
+ // updateFunction = (this.resource === "CLINICAL_INTERPRETATION") ?
+ // this.endpoint[endpointMethod](this.component.clinicalAnalysisId, this.component.id, updateParams, params) :
+ // this.endpoint[endpointMethod](this.component.id, updateParams, params);
updateFunction
.then(response => {
this.component = UtilsNew.objectClone(response.responses[0].results[0]);
diff --git a/src/webcomponents/commons/tool-settings-editor.js b/src/webcomponents/commons/tool-settings-editor.js
index 42e71d0ffa..24e3cc9178 100644
--- a/src/webcomponents/commons/tool-settings-editor.js
+++ b/src/webcomponents/commons/tool-settings-editor.js
@@ -130,6 +130,7 @@ export default class ToolSettingsEditor extends LitElement {
${status.description}` : "";
+ const statusId = status.id;
+ switch (statusId) {
+ case "PENDING":
+ case "QUEUED":
+ return ` ${statusId}${description} `;
+ case "RUNNING":
+ return ` ${statusId}${description} `;
+ case "DONE":
+ return ` ${statusId}${description} `;
+ case "ERROR":
+ return ` ${statusId}${description} `;
+ case "UNKNOWN":
+ return ` ${statusId}${description} `;
+ case "ABORTED":
+ return ` ${statusId}${description} `;
+ case "DELETED":
+ return ` ${statusId}${description} `;
+ }
+ return "-";
+ }
+
+ static getClinicalAnalysisPriorityColour(rank) {
+ const priorityRankToColor = {
+ 1: "bg-danger", // URGENT
+ 2: "bg-warning", // HIGH
+ 3: "bg-primary", // NORMAL
+ 4: "bg-info", // LOW
+ 5: "bg-light text-dark", // UNKNOWN
+ };
+
+ return priorityRankToColor[rank] ?? "";
+ }
+
}
diff --git a/src/webcomponents/commons/view/detail-tabs.js b/src/webcomponents/commons/view/detail-tabs.js
index 3825b0b9af..0b6f1d06f2 100644
--- a/src/webcomponents/commons/view/detail-tabs.js
+++ b/src/webcomponents/commons/view/detail-tabs.js
@@ -110,9 +110,7 @@ export default class DetailTabs extends LitElement {
changeTab(e) {
this._activeTab = e.currentTarget.dataset.id;
- LitUtils.dispatchCustomEvent(this, "activeTabChange", this._activeTab, null, null, {
- bubbles: false,
- });
+ LitUtils.dispatchCustomEvent(this, "activeTabChange", this._activeTab, null, null, {bubbles: false});
this.requestUpdate();
}
@@ -131,13 +129,10 @@ export default class DetailTabs extends LitElement {
}
renderTitle() {
- const title = typeof this._config.title === "function" ? this._config.title(this.data) : this._config.title + " " + (this.data?.id || "");
+ const title = typeof this._config.title === "function" ? this._config.title(this.data) : this._config.title + " " + UtilsNew.substring(this.data?.id || "", this._config.display?.idMaxLength);
return html`
-
+
`;
}
@@ -174,14 +169,16 @@ export default class DetailTabs extends LitElement {
}
render() {
- // If data is undefined or null
+ // 1. Check If data is undefined or null
if (!this.data) {
return html`
${this._config?.errorMessage || "No data found"} `;
}
+
// 2. Check the 'mode' is correct
if (this.mode !== DetailTabs.TABS_MODE && this.mode !== DetailTabs.PILLS_MODE && this.mode !== DetailTabs.PILLS_VERTICAL_MODE) {
return html`
No valid mode: '${this.mode || ""}' `;
}
+
// 3. Check tabs exist
if (this._config?.items?.length === 0) {
return html`
No tab items provided `;
@@ -244,6 +241,9 @@ export default class DetailTabs extends LitElement {
contentClass: "p-3",
contentStyle: "",
+
+ // maximum length of the displayed id in the title
+ idMaxLength: 100,
},
items: [],
// Example:
diff --git a/src/webcomponents/disease-panel/disease-panel-browser.js b/src/webcomponents/disease-panel/disease-panel-browser.js
index 6d16a27ee6..a6a28b91b5 100644
--- a/src/webcomponents/disease-panel/disease-panel-browser.js
+++ b/src/webcomponents/disease-panel/disease-panel-browser.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
+import {LitElement, html, nothing} from "lit";
import UtilsNew from "../../core/utils-new.js";
import {construction} from "../commons/under-construction.js";
import "./disease-panel-gene-view.js";
@@ -136,15 +136,18 @@ export default class DiseasePanelBrowser extends LitElement {
.config="${params.config.filter.result.grid}"
.eventNotifyName="${params.eventNotifyName}"
.active="${true}"
- @selectrow="${e => params.onClickRow(e, "diseasePanel")}"
- @diseasePanelUpdate="${e => params.onComponentUpdate(e, "diseasePanel")}"
+ @selectrow="${e => params.onClickRow(e)}"
+ @diseasePanelUpdate="${e => params.onComponentUpdate(e)}"
@settingsUpdate="${() => this.onSettingsUpdate()}">
-
- `
+ ${params?.detail ? html`
+
+
+ ` : nothing}
+ `,
},
],
filter: {
diff --git a/src/webcomponents/disease-panel/disease-panel-grid.js b/src/webcomponents/disease-panel/disease-panel-grid.js
index 7af7fea167..d57152f62c 100644
--- a/src/webcomponents/disease-panel/disease-panel-grid.js
+++ b/src/webcomponents/disease-panel/disease-panel-grid.js
@@ -23,8 +23,9 @@ import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-util
import LitUtils from "../commons/utils/lit-utils.js";
import "../commons/catalog-browser-grid-config.js";
import "../commons/opencb-grid-toolbar.js";
-import ModalUtils from "../commons/modal/modal-utils";
-import CatalogGridFormatter from "../commons/catalog-grid-formatter";
+import ModalUtils from "../commons/modal/modal-utils.js";
+import CatalogGridFormatter from "../commons/catalog-grid-formatter.js";
+import WebUtils from "../commons/utils/web-utils.js";
export default class DiseasePanelGrid extends LitElement {
@@ -143,6 +144,8 @@ export default class DiseasePanelGrid extends LitElement {
// `
// }
};
+
+ this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE");
}
renderTable() {
@@ -463,7 +466,17 @@ export default class DiseasePanelGrid extends LitElement {
field: "actions",
align: "center",
formatter: () => {
- const isAdmin = OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id);
+ const simplifyPermissions = this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions;
+ const hasWritePermission = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ this.permissionID,
+ simplifyPermissions);
+ const hasDeletePermission = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ "DELETE_PANELS",
+ simplifyPermissions);
return `
@@ -473,29 +486,29 @@ export default class DiseasePanelGrid extends LitElement {
diff --git a/src/webcomponents/family/family-browser.js b/src/webcomponents/family/family-browser.js
index 67fb50c1d3..1394be5e75 100644
--- a/src/webcomponents/family/family-browser.js
+++ b/src/webcomponents/family/family-browser.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
+import {LitElement, html, nothing} from "lit";
import UtilsNew from "../../core/utils-new.js";
import "../commons/opencga-browser.js";
import "../commons/facet-filter.js";
@@ -129,15 +129,17 @@ export default class FamilyBrowser extends LitElement {
.config="${params.config.filter.result.grid}"
.active="${true}"
.eventNotifyName="${params.eventNotifyName}"
- @selectrow="${e => params.onClickRow(e, "family")}"
- @familyUpdate="${e => params.onComponentUpdate(e, "family")}"
+ @selectrow="${e => params.onClickRow(e)}"
+ @familyUpdate="${e => params.onComponentUpdate(e)}"
@settingsUpdate="${() => this.onSettingsUpdate()}">
-
-
+ ${params?.detail ? html`
+
+
+ ` : nothing}
`,
},
{
diff --git a/src/webcomponents/family/family-create.js b/src/webcomponents/family/family-create.js
index 81d22bae0e..e23480f13b 100644
--- a/src/webcomponents/family/family-create.js
+++ b/src/webcomponents/family/family-create.js
@@ -203,14 +203,6 @@ export default class FamilyCreate extends LitElement {
placeholder: "Add an ID",
}
},
- {
- title: "Name",
- field: "status.name",
- type: "input-text",
- display: {
- placeholder: "Add source name"
- }
- },
{
title: "Description",
field: "status.description",
diff --git a/src/webcomponents/family/family-grid.js b/src/webcomponents/family/family-grid.js
index 1fde5b9e28..00cd59b602 100644
--- a/src/webcomponents/family/family-grid.js
+++ b/src/webcomponents/family/family-grid.js
@@ -19,9 +19,10 @@ import UtilsNew from "../../core/utils-new.js";
import GridCommons from "../commons/grid-commons.js";
import CatalogGridFormatter from "../commons/catalog-grid-formatter.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils";
-import ModalUtils from "../commons/modal/modal-utils";
+import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import ModalUtils from "../commons/modal/modal-utils.js";
import "../commons/opencb-grid-toolbar.js";
+import WebUtils from "../commons/utils/web-utils.js";
export default class FamilyGrid extends LitElement {
@@ -139,6 +140,8 @@ export default class FamilyGrid extends LitElement {
// `
// }
};
+
+ this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE");
}
fetchClinicalAnalysis(rows, casesLimit) {
@@ -430,7 +433,7 @@ export default class FamilyGrid extends LitElement {
case "download-json":
UtilsNew.downloadData([JSON.stringify(row, null, "\t")], row.id + ".json");
break;
- case "qualityControl":
+ case "quality-control":
alert("Not implemented yet");
break;
}
@@ -521,57 +524,66 @@ export default class FamilyGrid extends LitElement {
title: "Actions",
field: "actions",
align: "center",
- formatter: (value, row) => `
-
-
-
- Actions
-
-
+
+ `;
+ },
events: {
"click a": this.onActionClick.bind(this)
},
diff --git a/src/webcomponents/family/family-update.js b/src/webcomponents/family/family-update.js
index 1c14d1478e..5d5ef5d571 100644
--- a/src/webcomponents/family/family-update.js
+++ b/src/webcomponents/family/family-update.js
@@ -170,14 +170,6 @@ export default class FamilyUpdate extends LitElement {
placeholder: "Add an ID",
}
},
- {
- title: "Name",
- field: "status.name",
- type: "input-text",
- display: {
- placeholder: "Add a name"
- }
- },
{
title: "Description",
field: "status.description",
diff --git a/src/webcomponents/file/file-browser.js b/src/webcomponents/file/file-browser.js
index 9a1de86065..238069bf6e 100644
--- a/src/webcomponents/file/file-browser.js
+++ b/src/webcomponents/file/file-browser.js
@@ -15,7 +15,7 @@
*/
-import {LitElement, html} from "lit";
+import {LitElement, html, nothing} from "lit";
import UtilsNew from "../../core/utils-new.js";
import "./file-preview.js";
import "./file-view.js";
@@ -132,15 +132,18 @@ export default class FileBrowser extends LitElement {
.query="${params.executedQuery}"
.config="${params.config.filter.result.grid}"
.eventNotifyName="${params.eventNotifyName}"
- @selectrow="${e => params.onClickRow(e, "file")}"
- @fileUpdate="${e => params.onComponentUpdate(e, "file")}"
+ @selectrow="${e => params.onClickRow(e)}"
+ @fileUpdate="${e => params.onComponentUpdate(e)}"
@settingsUpdate="${() => this.onSettingsUpdate()}">
-
- `
+ ${params?.detail ? html`
+
+
+ ` : nothing}
+ `,
},
{
id: "facet-tab-file",
diff --git a/src/webcomponents/file/file-grid.js b/src/webcomponents/file/file-grid.js
index 5f500ad459..4b401b79d5 100644
--- a/src/webcomponents/file/file-grid.js
+++ b/src/webcomponents/file/file-grid.js
@@ -21,8 +21,8 @@ import CatalogGridFormatter from "../commons/catalog-grid-formatter.js";
import "../commons/opencb-grid-toolbar.js";
import "../loading-spinner.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils";
-
+import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import WebUtils from "../commons/utils/web-utils.js";
export default class OpencgaFileGrid extends LitElement {
@@ -118,6 +118,8 @@ export default class OpencgaFileGrid extends LitElement {
`,
},
};
+
+ this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE");
}
renderTable() {
@@ -304,7 +306,7 @@ export default class OpencgaFileGrid extends LitElement {
case "download-json":
UtilsNew.downloadData([JSON.stringify(row, null, "\t")], row.id + ".json");
break;
- case "qualityControl":
+ case "quality-control":
alert("Not implemented yet");
break;
}
@@ -416,50 +418,60 @@ export default class OpencgaFileGrid extends LitElement {
title: "Actions",
field: "actions",
align: "center",
- formatter: (value, row) => `
-
-
-
- Actions
-
-
-
- `,
+ formatter: (value, row) => {
+ const hasWritePermission = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ this.permissionID,
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
+ return `
+
+
+
+ Actions
+
+
+
+ `;
+ },
events: {
"click a": this.onActionClick.bind(this)
},
@@ -490,7 +502,7 @@ export default class OpencgaFileGrid extends LitElement {
if (results) {
// Check if user clicked in Tab or JSON format
if (e.detail.option.toUpperCase() === "TAB") {
- const fields = ["id", "name", "path", "format", "bioformat", "size", "creationDate", "modificationDate", "internal.status.name"];
+ const fields = ["id", "name", "path", "format", "bioformat", "size", "creationDate", "modificationDate", "internal.status.id"];
const data = UtilsNew.toTableString(results, fields);
UtilsNew.downloadData(data, "files_" + this.opencgaSession.study.id + ".tsv", "text/plain");
} else {
diff --git a/src/webcomponents/file/file-view.js b/src/webcomponents/file/file-view.js
index e2272b2fe3..3715570151 100644
--- a/src/webcomponents/file/file-view.js
+++ b/src/webcomponents/file/file-view.js
@@ -240,7 +240,7 @@ export default class FileView extends LitElement {
title: "Status",
type: "complex",
display: {
- template: "${internal.status.name} (${internal.status.date})",
+ template: "${internal.status.id} (${internal.status.date})",
format: {
"internal.status.date": date => UtilsNew.dateFormatter(date),
}
diff --git a/src/webcomponents/individual/individual-browser.js b/src/webcomponents/individual/individual-browser.js
index e6e79d66cc..8b47a89e5a 100644
--- a/src/webcomponents/individual/individual-browser.js
+++ b/src/webcomponents/individual/individual-browser.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
+import {LitElement, html, nothing} from "lit";
import UtilsNew from "../../core/utils-new.js";
import "./qc/individual-qc-inferred-sex.js";
import "./qc/individual-qc-mendelian-errors.js";
@@ -136,15 +136,18 @@ export default class IndividualBrowser extends LitElement {
.eventNotifyName="${params.eventNotifyName}"
.query="${params.executedQuery}"
.active="${true}"
- @selectrow="${e => params.onClickRow(e, "individual")}"
- @individualUpdate="${e => params.onComponentUpdate(e, "individual")}"
+ @selectrow="${e => params.onClickRow(e)}"
+ @individualUpdate="${e => params.onComponentUpdate(e)}"
@settingsUpdate="${() => this.onSettingsUpdate()}">
-
- `
+ ${params?.detail ? html`
+
+
+ ` : nothing}
+ `
},
{
id: "facet-tab",
diff --git a/src/webcomponents/individual/individual-grid.js b/src/webcomponents/individual/individual-grid.js
index 84b69405d9..d51fb188a7 100644
--- a/src/webcomponents/individual/individual-grid.js
+++ b/src/webcomponents/individual/individual-grid.js
@@ -19,9 +19,10 @@ import UtilsNew from "../../core/utils-new.js";
import GridCommons from "../commons/grid-commons.js";
import CatalogGridFormatter from "../commons/catalog-grid-formatter.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils";
-import ModalUtils from "../commons/modal/modal-utils";
+import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import ModalUtils from "../commons/modal/modal-utils.js";
import "../commons/opencb-grid-toolbar.js";
+import WebUtils from "../commons/utils/web-utils.js";
export default class IndividualGrid extends LitElement {
@@ -143,6 +144,8 @@ export default class IndividualGrid extends LitElement {
// `
// }
};
+
+ this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE");
}
fetchClinicalAnalysis(rows, casesLimit) {
@@ -401,7 +404,7 @@ export default class IndividualGrid extends LitElement {
${preparationMethod}
${cellLine}
${creationDate}
- ${sample.status ? sample.status.name : ""}
+ ${sample?.status?.id || ""}
`;
}
@@ -429,7 +432,7 @@ export default class IndividualGrid extends LitElement {
case "download-json":
UtilsNew.downloadData([JSON.stringify(row, null, "\t")], row.id + ".json");
break;
- case "qualityControl":
+ case "quality-control":
alert("Not implemented yet");
break;
}
@@ -558,57 +561,66 @@ export default class IndividualGrid extends LitElement {
title: "Actions",
field: "actions",
align: "center",
- formatter: (value, row) => `
-
-
-
- Actions
-
-
+
+ `;
+ },
events: {
"click a": this.onActionClick.bind(this),
},
diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js
index 15e3548db0..eb3e61457f 100644
--- a/src/webcomponents/individual/individual-view.js
+++ b/src/webcomponents/individual/individual-view.js
@@ -332,8 +332,7 @@ export default class IndividualView extends LitElement {
title: "Status",
type: "complex",
display: {
- // render: field => field ? `${field.name} (${UtilsNew.dateFormatter(field.date)})` : "-"
- template: "${internal.status.name} (${internal.status.date})",
+ template: "${internal.status.id} (${internal.status.date})",
format: {
"internal.status.date": date => UtilsNew.dateFormatter(date)
}
diff --git a/src/webcomponents/job/job-browser.js b/src/webcomponents/job/job-browser.js
index f5b6fc18c0..889c8867ea 100644
--- a/src/webcomponents/job/job-browser.js
+++ b/src/webcomponents/job/job-browser.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
+import {LitElement, html, nothing} from "lit";
import UtilsNew from "../../core/utils-new.js";
import {guardPage} from "../commons/html-utils.js";
import "../commons/opencga-browser.js";
@@ -137,15 +137,17 @@ export default class JobBrowser extends LitElement {
.search="${params.executedQuery}"
.eventNotifyName="${params.eventNotifyName}"
.files="${params.files}"
- @selectrow="${e => params.onClickRow(e, "job")}"
- @jobUpdate="${e => params.onComponentUpdate(e, "job")}"
+ @selectrow="${e => params.onClickRow(e)}"
+ @jobUpdate="${e => params.onComponentUpdate(e)}"
@settingsUpdate="${() => this.onSettingsUpdate()}">
-
-
+ ${params?.detail ? html`
+
+
+ ` : nothing}
`,
},
{
diff --git a/src/webcomponents/job/job-detail-log.js b/src/webcomponents/job/job-detail-log.js
index aac9525a85..5452385784 100644
--- a/src/webcomponents/job/job-detail-log.js
+++ b/src/webcomponents/job/job-detail-log.js
@@ -18,7 +18,6 @@ import {LitElement, html} from "lit";
import UtilsNew from "../../core/utils-new.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-
export default class JobDetailLog extends LitElement {
constructor() {
@@ -93,7 +92,7 @@ export default class JobDetailLog extends LitElement {
// setInterval makes sense only in case of Tail log
setReloadInterval() {
- if (this.active && this.command === "tail" && this.job.internal.status.name === "RUNNING") {
+ if (this.active && this.command === "tail" && this.job.internal.status.id === "RUNNING") {
this.requestUpdate();
this.interval = setInterval(() => {
if ($(".jobs-details-log", this).is(":visible")) {
@@ -115,7 +114,7 @@ export default class JobDetailLog extends LitElement {
}
async fetchContent(job, params = {}, append = false) {
- const statusWithoutLogs = ["PENDING", "ABORTED", "QUEUED"];
+ const statusWithoutLogs = ["PENDING", "QUEUED"];
if (!append) {
this.content = "";
}
@@ -170,29 +169,9 @@ export default class JobDetailLog extends LitElement {
render() {
return html`
-
-
-
- this.setCommand("head")}">
- Head
-
- this.setCommand("tail")}">
-
- Tail
-
-
-
+
+
+ this.setCommand("head")}">
+ Head
+
+ this.setCommand("tail")}">
+
+ Tail
+
+
-
-
- this.setType("stderr")}">
- Stderr
-
- this.setType("stdout")}">
- Stdout
-
-
+
+ this.setType("stderr")}">
+ Stderr
+
+ this.setType("stdout")}">
+ Stdout
+
${this.content}\n${this.renderCursor()}
diff --git a/src/webcomponents/job/job-grid.js b/src/webcomponents/job/job-grid.js
index 446a84b624..cb80384498 100644
--- a/src/webcomponents/job/job-grid.js
+++ b/src/webcomponents/job/job-grid.js
@@ -21,8 +21,9 @@ import CatalogGridFormatter from "../commons/catalog-grid-formatter.js";
import "../commons/opencb-grid-toolbar.js";
import "../loading-spinner.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils";
-import ModalUtils from "../commons/modal/modal-utils";
+import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import ModalUtils from "../commons/modal/modal-utils.js";
+import WebUtils from "../commons/utils/web-utils.js";
export default class JobGrid extends LitElement {
@@ -148,6 +149,8 @@ export default class JobGrid extends LitElement {
// `
// }
};
+
+ this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE");
}
renderTable() {
@@ -367,7 +370,7 @@ export default class JobGrid extends LitElement {
${job.id}
${job.tool.id}
- ${UtilsNew.jobStatusFormatter(job.internal.status)}
+ ${WebUtils.jobStatusFormatter(job.internal.status)}
${job.priority}
${moment(job.creationDate, "YYYYMMDDHHmmss").format("D MMM YYYY, h:mm:ss a")}
${job.visited}
@@ -403,6 +406,12 @@ export default class JobGrid extends LitElement {
// await this.updateComplete;
ModalUtils.show(`${this._prefix}RetryModal`);
break;
+ case "kill":
+ this.jobKillObj = row;
+ this.requestUpdate();
+ // await this.updateComplete;
+ ModalUtils.show(`${this._prefix}KillModal`);
+ break;
case "edit":
this.jobUpdateId = row.id;
this.requestUpdate();
@@ -538,7 +547,7 @@ export default class JobGrid extends LitElement {
id: "status",
title: "Status",
field: "internal.status",
- formatter: status => UtilsNew.jobStatusFormatter(status),
+ formatter: status => WebUtils.jobStatusFormatter(status),
visible: this.gridCommons.isColumnVisible("status")
},
{
@@ -579,43 +588,55 @@ export default class JobGrid extends LitElement {
title: "Actions",
field: "actions",
align: "center",
- formatter: (value, row) => `
-
-
-
- Actions
-
-
-
- `,
+ formatter: (value, row) => {
+ const hasWritePermission = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ this.permissionID,
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
+ return `
+
+
+
+ Actions
+
+
+
+ `;
+ },
events: {
"click a": this.onActionClick.bind(this),
},
@@ -646,7 +667,7 @@ export default class JobGrid extends LitElement {
if (results) {
// Check if user clicked in Tab or JSON format
if (e.detail.option.toLowerCase() === "tab") {
- const fields = ["id", "tool.id", "priority", "tags", "creationDate", "internal.status.name", "visited"];
+ const fields = ["id", "tool.id", "priority", "tags", "creationDate", "internal.status.id", "visited"];
const data = UtilsNew.toTableString(results, fields);
UtilsNew.downloadData(data, "job_" + this.opencgaSession.study.id + ".tsv", "text/plain");
} else {
@@ -670,12 +691,8 @@ export default class JobGrid extends LitElement {
const params = {
study: this.opencgaSession.study.fqn
};
- let error;
this.opencgaSession.opencgaClient.jobs()
- .retry(
- {
- job: this.jobRetryObj?.id
- }, params)
+ .retry({job: this.jobRetryObj?.id}, params)
.then(() => {
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
title: "Job Retry",
@@ -683,7 +700,23 @@ export default class JobGrid extends LitElement {
});
})
.catch(reason => {
- error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ });
+ }
+
+ onJobKill() {
+ const params = {
+ study: this.opencgaSession.study.fqn
+ };
+ this.opencgaSession.opencgaClient.jobs()
+ .kill(this.jobKillObj?.id, params)
+ .then(() => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: "Job Kill",
+ message: "Job killed correctly"
+ });
+ })
+ .catch(reason => {
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
});
}
@@ -693,7 +726,7 @@ export default class JobGrid extends LitElement {
{
render: () => html`
this.table.bootstrapTable("refresh")}">
- Refresh
+ Refresh
`,
}
@@ -720,6 +753,24 @@ export default class JobGrid extends LitElement {
});
}
+ renderModalKill() {
+ return ModalUtils.create(this, `${this._prefix}KillModal`, {
+ display: {
+ modalTitle: "Job Kill",
+ modalDraggable: true,
+ modalbtnsVisible: true,
+ modalSize: "modal-lg",
+ okButtonText: "Kill Job",
+ },
+ render: () => {
+ return html`
+ This will kill a queued or running Job. Are you sure do you want to kill ${this.jobRetryObj?.id} ?
+ `;
+ },
+ onOk: e => this.onJobKill(e)
+ });
+ }
+
renderModalUpdate() {
return ModalUtils.create(this, `${this._prefix}UpdateModal`, {
display: {
@@ -760,6 +811,7 @@ export default class JobGrid extends LitElement {
${this.renderModalRetry()}
+ ${this.renderModalKill()}
${this.renderModalUpdate()}
`;
}
diff --git a/src/webcomponents/job/job-monitor.js b/src/webcomponents/job/job-monitor.js
index f3fd728f21..54a247f48b 100644
--- a/src/webcomponents/job/job-monitor.js
+++ b/src/webcomponents/job/job-monitor.js
@@ -14,17 +14,18 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
-import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import {LitElement, html, nothing} from "lit";
+import CatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
import UtilsNew from "../../core/utils-new.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
+import WebUtils from "../commons/utils/web-utils.js";
+import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
export class JobMonitor extends LitElement {
constructor() {
super();
-
- this._init();
+ this.#init();
}
createRenderRoot() {
@@ -42,214 +43,223 @@ export class JobMonitor extends LitElement {
};
}
- _init() {
- this.iconMap = {
- info: "fa fa-info-circle fa-2x",
- success: "fa fa-thumbs-up fa-2x",
- warning: "fa fa-exclamation-triangle fa-2x",
- danger: "fa ffa fa-exclamation-circle fa-2x",
- error: "fa ffa fa-exclamation-circle fa-2x"
+ #init() {
+ this.JOBS_TYPES = {
+ ALL: {
+ title: "All",
+ jobsTypes: [],
+ },
+ RUNNING: {
+ title: "Running",
+ jobsTypes: ["PENDING", "QUEUED", "RUNNING"],
+ },
+ FINISHED: {
+ title: "Finished",
+ jobsTypes: ["DONE", "ERROR", "ABORTED"],
+ },
};
+ this._interval = -1;
this._jobs = [];
- this.jobs = [];
- this.filteredJobs = [];
- this.updatedCnt = 0;
- this.restCnt = 0;
-
+ this._addedJobs= new Set(); // Used for displaying the NEW label in each new job
+ this._updatedJobsCount = 0; // To store the number of changes (new jobs, state changes)
+ this._visibleJobsType = "ALL"; // Current visible jobs types (one of JOB_TYPES)
this._config = this.getDefaultConfig();
}
- updated(changedProperties) {
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ // Make sure we stop calling fetchLastJobs when the component is removed from DOM
+ clearInterval(this._interval);
+ }
+
+ update(changedProperties) {
if (changedProperties.has("opencgaSession")) {
- this.launchMonitor();
+ this._jobs = [];
+ this._updatedJobsCount = 0;
+ this._addedJobs = new Set();
+ this._visibleJobsType = "ALL";
}
if (changedProperties.has("config")) {
- this._config = {...this.getDefaultConfig(), ...this.config};
- this.launchMonitor();
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.config,
+ };
}
+ super.update();
}
- launchMonitor() {
- if (OpencgaCatalogUtils.checkPermissions(this.opencgaSession.study, this.opencgaSession.user.id, "VIEW_JOBS")) {
- // Make a first query
- clearInterval(this.interval);
- this._jobs = [];
- this.jobs = [];
- this.filteredJobs = [];
- this.fetchLastJobs();
- // and then every 'interval' ms
- this.interval = setInterval(() => {
- this.fetchLastJobs();
- }, this._config.interval);
+ updated(changedProperties) {
+ if (changedProperties.has("opencgaSession") || changedProperties.has("config")) {
+ this.launchMonitor();
}
}
- async applyUpdated() {
- // oldList and newList are always the same length
- const oldList = this._jobs;
- const newList = this.jobs;
- // `index` is the position of the first job of oldList in newList (newly added jobs are index < k)
- const index = newList.findIndex(job => job.id === oldList[0].id);
- const k = index > -1 ? index : newList.length; // -1 occurs iff the whole list is made of new jobs
- this.jobs = newList.map((job, i) => {
- if (i < k) {
- // handle the new jobs
- NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_INFO, {
- message: `${job.id}, "The job has been added`,
- });
- return {...job, updated: true};
- } else {
- // handle the change of state
- // FIXME remove this in v2.3
- const statusId = job.internal.status.id || job.internal.status.name;
- const oldStatusId = oldList[i - k].internal.status.id || oldList[i - k].internal.status.name;
- if (statusId !== oldStatusId) {
- NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_INFO, {
- message: `${job.id} The job has now status ${job?.internal?.status?.id || job?.internal?.status?.name}`,
- });
- return {...job, updated: true};
- } else {
- // if the ids are the same I want to keep the `updated` status
- // return {...job, updated: false};
- return {...oldList[i - k]};
- }
+ launchMonitor() {
+ clearInterval(this._interval);
+ if (this.opencgaSession) {
+ // Check if the user has VIEW_JOBS permission in the current study
+ const hasViewPermission = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ "VIEW_JOBS",
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
+ if (hasViewPermission) {
+ this.fetchLastJobs();
+ this._interval = setInterval(() => this.fetchLastJobs(), this._config.interval);
}
- });
- // accumulate all the updated (not visited) status
- this.updatedCnt = this.jobs.reduce((acc, job) => job.updated && !job._visited ? acc + 1 : acc, 0);
- this.requestUpdate();
- await this.updateComplete;
- this._jobs = this.jobs;
+ }
}
fetchLastJobs() {
- if (!this?.opencgaSession?.token || !$("#job-monitor").is(":visible")) {
- clearInterval(this.interval);
- return;
- }
-
- const query = {
- study: this.opencgaSession.study.fqn,
- internalStatus: "PENDING,QUEUED,RUNNING,DONE,ERROR,ABORTED",
- limit: this._config.limit || 10,
- sort: "creationDate",
- include: "id,internal.status,tool,creationDate",
- order: -1
- };
- this.opencgaSession.opencgaClient.jobs().search(query)
- .then(async restResponse => {
- // console.log("restResponse", restResponse);
- // first call
- if (!this._jobs.length) {
- this._jobs = restResponse.getResults();
+ this.opencgaSession.opencgaClient.jobs()
+ .search({
+ study: this.opencgaSession.study.fqn,
+ internalStatus: "PENDING,QUEUED,RUNNING,DONE,ERROR,ABORTED",
+ limit: this._config.limit || 10,
+ sort: "creationDate",
+ include: "id,internal.status,tool,creationDate",
+ order: -1,
+ })
+ .then(response => {
+ const newJobsList = response?.responses?.[0]?.results || [];
+ // 1. Process the list of new jobs returned by OpenCGA
+ // Note: we check if the previous list of jobs is not empty, to prevent marking all jobs as new jobs
+ if (this._jobs.length > 0) {
+ newJobsList.forEach(job => {
+ const oldJob = this._jobs.find(j => j.id === job.id);
+ if (oldJob) {
+ const statusId = job?.internal?.status?.id || "-";
+ const oldStatusId = oldJob?.internal?.status?.id || "-";
+ // If this job exists in the previous list, and now it has a different status, display a confirmation message
+ if (statusId !== oldStatusId) {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_INFO, {
+ message: `The job ${job?.id} has now status ${statusId}.`,
+ });
+ this._updatedJobsCount = this._updatedJobsCount + 1;
+ }
+ } else {
+ // This is a new job, so we display an info notification to the user
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_INFO, {
+ message: `The job ${job?.id} has been added.`,
+ });
+ this._updatedJobsCount = this._updatedJobsCount + 1;
+ this._addedJobs.add(job.id);
+ }
+ });
}
- this.jobs = restResponse.getResults();
- await this.applyUpdated();
- this.filteredJobs = this.jobs.filter(job => this.filterTypes?.includes(job.internal.status.id || job.internal.status.name) ?? 1);
+ // 2. Save the new jobs list
+ this._jobs = newJobsList;
this.requestUpdate();
})
- .catch(restResponse => {
- console.error(restResponse);
+ .catch(response => {
+ console.error(response);
});
}
- filterJobs(e) {
- e.stopPropagation();
- this.filterTypes = e.currentTarget.dataset?.type?.split(",");
- this.filteredJobs = this.jobs.filter(job => this.filterTypes?.includes(job.internal.status.id || job.internal.status.name) ?? 1);
- this.requestUpdate();
+ getJobUrl(jobId) {
+ return `#job/${this.opencgaSession.project.id}/${this.opencgaSession.study.id}/${jobId}`;
}
- openJob(jobId) {
- // -> e.stopPropagation();
- const job = this.jobs.find(job => job.id === jobId);
- job._visited = true;
- this.jobs = [...this.jobs];
- this.requestUpdate();
-
- this.dispatchEvent(new CustomEvent("route", {
- detail: {
- hash: "#job",
- resource: "job",
- query: {id: jobId}
- },
- bubbles: true, // this is necessary as the event is handled in iva-app
- composed: true
- }));
+ onRefresh(event) {
+ event.stopPropagation();
+ this.fetchLastJobs();
}
- forceRefresh(e) {
- e.stopPropagation();
- this.fetchLastJobs();
+ onJobTypeChange(event, newJobType) {
+ event.stopPropagation();
+ this._visibleJobsType = newJobType;
+ this.requestUpdate();
}
- toggleDropdown() {
- this.dropdown = !this.dropdown;
+ renderJobsButtons() {
+ return Object.keys(this.JOBS_TYPES).map(type => html`
+ this.onJobTypeChange(e, type)}">
+ ${this.JOBS_TYPES[type].title}
+
+ `);
}
- getDefaultConfig() {
- return {
- limit: 10,
- interval: 30000
- };
+ renderVisibleJobsList() {
+ // Get the list of visible jobs with the selected type
+ const visibleJobs = this._jobs.filter(job => {
+ return this._visibleJobsType === "ALL" || this.JOBS_TYPES[this._visibleJobsType].jobsTypes.includes(job?.internal?.status?.id);
+ });
+ if (visibleJobs.length > 0) {
+ return visibleJobs.map(job => html`
+
+
+
+
+
+
+
+ ${this._addedJobs.has(job?.id) ? html`
+
NEW
+ ` : nothing}
+
+ ${job?.id || "-"}
+
+
+ ${job?.tool?.id || "-"}
+
+ ${moment(job.creationDate, "YYYYMMDDHHmmss").format("D MMM YYYY, h:mm:ss a")}
+
+
+ ${UtilsNew.renderHTML(WebUtils.jobStatusFormatter(job?.internal?.status))}
+
+
+
+
+
+ `);
+ } else {
+ return html`
+
+
+ No jobs on this category.
+
+
+ `;
+ }
}
render() {
return html`
-
+
-
- ${this.updatedCnt}
-
+ ${this._updatedJobsCount > 0 ? html`
+
+ ${this._updatedJobsCount}
+
+ ` : nothing}
-
`;
}
+ getDefaultConfig() {
+ return {
+ limit: 10,
+ interval: 30000,
+ };
+ }
+
}
customElements.define("job-monitor", JobMonitor);
diff --git a/src/webcomponents/job/job-view.js b/src/webcomponents/job/job-view.js
index 859287cd3d..b635b4e693 100644
--- a/src/webcomponents/job/job-view.js
+++ b/src/webcomponents/job/job-view.js
@@ -17,6 +17,7 @@
import {LitElement, html} from "lit";
import UtilsNew from "../../core/utils-new.js";
import LitUtils from "../commons/utils/lit-utils.js";
+import WebUtils from "../commons/utils/web-utils.js";
import "../commons/forms/data-form.js";
import "./job-detail-log.js";
import "../loading-spinner.js";
@@ -273,7 +274,7 @@ export default class JobView extends LitElement {
display: {
template: "${internal.status}",
format: {
- "internal.status": status => UtilsNew.jobStatusFormatter(status, true),
+ "internal.status": status => WebUtils.jobStatusFormatter(status, true),
},
}
},
@@ -448,7 +449,7 @@ export default class JobView extends LitElement {
},
{
title: "Status",
- field: "internal.status.name"
+ field: "internal.status.id"
}
],
},
diff --git a/src/webcomponents/note/note-browser.js b/src/webcomponents/note/note-browser.js
new file mode 100644
index 0000000000..67424d7d84
--- /dev/null
+++ b/src/webcomponents/note/note-browser.js
@@ -0,0 +1,270 @@
+/**
+ * Copyright 2015-2019 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import UtilsNew from "../../core/utils-new.js";
+import CatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import "../commons/opencga-browser.js";
+import "../commons/forms/toggle-radio.js";
+import "../commons/filters/catalog-search-autocomplete.js";
+import "./note-grid.js";
+import "./note-detail.js";
+
+export default class NoteBrowser extends LitElement {
+
+ constructor() {
+ super();
+ // Set status and init private properties
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ opencgaSession: {
+ type: Object
+ },
+ query: {
+ type: Object
+ },
+ settings: {
+ type: Object
+ }
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "note-browser";
+ this._config = this.getDefaultConfig();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("settings")) {
+ this.settingsObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ settingsObserver() {
+ this._config = this.getDefaultConfig();
+
+ // Apply Study settings
+ if (this.settings?.menu) {
+ this._config.filter = UtilsNew.mergeFiltersAndDetails(this._config?.filter, this.settings);
+ }
+
+ // Grid configuration and take out toolbar admin/user settings to grid level.
+ if (this.settings?.table) {
+ const {toolbar, ...otherTableProps} = this.settings.table;
+ UtilsNew.setObjectValue(this._config, "filter.result.grid", {
+ ...this._config.filter.result.grid,
+ ...otherTableProps,
+ ...toolbar,
+ });
+ }
+ // this._config = UtilsNew.mergeTableSetting(this._config, this.settings);
+
+ // Apply User grid configuration. Only 'pageSize' and 'columns' are set
+ UtilsNew.setObjectValue(this._config, "filter.result.grid", {
+ ...this._config.filter?.result?.grid,
+ ...this.opencgaSession.user?.configs?.IVA?.settings?.[this.COMPONENT_ID]?.grid
+ });
+
+ }
+
+ onSettingsUpdate() {
+ this.settingsObserver();
+ this.requestUpdate();
+ }
+
+ onNoteUpdate() {
+ this.settingsObserver();
+ this.requestUpdate();
+ }
+
+ render() {
+ if (!this.opencgaSession) {
+ return html`Not valid session
`;
+ }
+ return html`
+
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ title: "Note Browser",
+ icon: "fab fa-searchengin",
+ views: [
+ {
+ id: "table-tab",
+ name: "Table result",
+ icon: "fa fa-table",
+ active: true,
+ render: params => {
+ return html`
+ params.onClickRow(e, "note")}"
+ @noteUpdate="${e => params.onComponentUpdate(e, "note")}"
+ @settingsUpdate="${() => this.onSettingsUpdate()}">
+
+ ${params?.detail?.note ? html`
+
+
+ ` : nothing}
+ `;
+ }
+ },
+ ],
+ filter: {
+ searchButton: false,
+ sections: [
+ {
+ title: "Section title",
+ collapsed: false,
+ filters: [
+ {
+ id: "scope",
+ name: "Scope",
+ render: (onFilterChange, query, opencgaSession) => {
+ const value = (query?.scope || "study").toLowerCase();
+ const allowedValues = [
+ {id: "STUDY", text: "Study"},
+ {id: "ORGANIZATION", text: "Organization"},
+ ];
+ return html`
+
+ ${value === "study" && !CatalogUtils.isAdmin(opencgaSession.study, opencgaSession.user.id) ? html`
+
+ You are allowed to see only PUBLIC notes from current study.
+
+ ` : nothing}
+ ${value === "organization" && !CatalogUtils.isOrganizationAdmin(opencgaSession.organization, opencgaSession.user.id) ? html`
+
+ You are allowd to see only PUBLIC notes fron current organization.
+
+ ` : nothing}
+
+ onFilterChange("scope", e.detail.value)}">
+
+
+
+ `;
+ },
+ },
+ {
+ id: "id",
+ name: "Note ID",
+ type: "string",
+ render: (onFilterChange, query, opencgaSession) => {
+ const resource = query?.scope === "ORGANIZATION" ? "NOTE_ORGANIZATION" : "NOTE_STUDY";
+ return html`
+ onFilterChange("id", e.detail.value)}">
+
+ `;
+ },
+ },
+ {
+ id: "visibility",
+ name: "Visibility",
+ allowedValues: ["PUBLIC", "PRIVATE"],
+ multiple: true,
+ description: ""
+ },
+ {
+ id: "date",
+ name: "Creation Date",
+ description: ""
+ },
+ ]
+ }
+ ],
+ examples: [],
+ result: {
+ grid: {
+ pageSize: 10,
+ pageList: [5, 10, 25],
+ multiSelection: false,
+ showSelectCheckbox: false,
+ showToolbar: true,
+ showCreate: true,
+ showExport: false,
+ showSettings: true,
+ exportTabs: ["download", "link", "code"]
+ }
+ },
+ detail: {
+ title: "Note",
+ showTitle: true,
+ items: [
+ {
+ id: "note-view",
+ name: "Overview",
+ active: true,
+ render: (note, active, opencgaSession) => html`
+
+
+ `,
+ },
+ {
+ id: "json-view",
+ name: "JSON Data",
+ render: (note, active, opencgaSession) => html`
+
+
+ `,
+ }
+ ]
+ }
+ }
+ };
+ }
+
+}
+
+customElements.define("note-browser", NoteBrowser);
diff --git a/src/webcomponents/note/note-create.js b/src/webcomponents/note/note-create.js
new file mode 100644
index 0000000000..feb1145688
--- /dev/null
+++ b/src/webcomponents/note/note-create.js
@@ -0,0 +1,277 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html} from "lit";
+import Types from "../commons/types.js";
+import NotificationUtils from "../commons/utils/notification-utils.js";
+import LitUtils from "../commons/utils/lit-utils.js";
+import UtilsNew from "../../core/utils-new.js";
+import CatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import "../commons/tool-header.js";
+import "../commons/filters/catalog-search-autocomplete.js";
+import "../commons/json-editor.js";
+
+export default class NoteCreate extends LitElement {
+
+ constructor() {
+ super();
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object,
+ },
+ };
+ }
+
+ #init() {
+ this.initNote();
+ this.isLoading = false;
+ this.displayConfigDefault = {
+ buttonsVisible: true,
+ buttonOkText: "Create",
+ style: "margin: 10px",
+ titleWidth: 3,
+ defaultLayout: "horizontal",
+ defaultValue: "",
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ initNote() {
+ this.note = {
+ valueType: "STRING",
+ scope: "STUDY",
+ visibility: "PUBLIC",
+ };
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("displayConfig") || changedProperties.has("opencgaSession")) {
+ this._config = this.getDefaultConfig();
+ }
+ super.update(changedProperties);
+ }
+
+ onFieldChange(e) {
+ const field = e.detail.param;
+ // 1. If we have changed the valueType, reset the value of the note
+ if (field === "valueType") {
+ delete this.note?.value;
+ }
+ // 2. If we have changed the scope or the valueType, force render data-form
+ if (field === "scope" || field === "valueType") {
+ this.note = {...this.note};
+ this.requestUpdate();
+ }
+ }
+
+ createNote(scope, data) {
+ return scope === "STUDY" ? this.opencgaSession.opencgaClient.studies().createNotes(this.opencgaSession.study.fqn, data) :
+ this.opencgaSession.opencgaClient.organization().createNotes(data);
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Clear note",
+ message: "Are you sure to clear?",
+ ok: () => {
+ this.initNote();
+ this._config = this.getDefaultConfig();
+ this.requestUpdate();
+ },
+ });
+ }
+
+ onSubmit() {
+ let error;
+ this.#setLoading(true);
+ const {scope, ...data} = this.note;
+ this.createNote(scope, data)
+ .then(() => {
+ this.initNote();
+ this._config = this.getDefaultConfig();
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ message: `Note '${data.id}' created.`,
+ });
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "noteCreate", this.note, {}, error);
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ return html`
+ this.onClear()}"
+ @fieldChange="${e => this.onFieldChange(e)}"
+ @submit="${() => this.onSubmit()}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return Types.dataFormConfig({
+ display: {
+ ...this.displayConfigDefault,
+ ...(this.displayConfig || {}),
+ },
+ sections: [
+ {
+ title: "General Information",
+ elements: [
+ {
+ title: "Scope",
+ field: "scope",
+ type: "toggle-buttons",
+ required: true,
+ defaultValue: "STUDY",
+ allowedValues: [
+ {id: "STUDY", disabled: false},
+ {id: "ORGANIZATION", disabled: !CatalogUtils.isOrganizationAdmin(this.opencgaSession?.organization, this.opencgaSession?.user?.id)},
+ ],
+ display: {
+ helpMessage: () => {
+ // If the user is not an organization admin, display a help message explaining why can not select the
+ // organization scope.
+ if (!CatalogUtils.isOrganizationAdmin(this.opencgaSession?.organization, this.opencgaSession?.user?.id)) {
+ return "You cannot create a note with scope ORGANIZATION because you are not an organization admin.";
+ }
+ return "";
+ },
+ },
+ },
+ {
+ title: "Note ID",
+ field: "id",
+ type: "input-text",
+ required: true,
+ },
+ {
+ title: "Visibility",
+ field: "visibility",
+ type: "toggle-buttons",
+ required: true,
+ defaultValue: "PUBLIC",
+ allowedValues: ["PUBLIC", "PRIVATE"],
+ },
+ {
+ title: "Tags",
+ field: "tags",
+ type: "custom",
+ display: {
+ render: (data, dataFormFieldChange) => {
+ const handleTagsChange = e => {
+ dataFormFieldChange(e.detail.value ? e.detail.value?.split(",") : []);
+ };
+ return html`
+ handleTagsChange(e)}">
+
+ `;
+ },
+ },
+ },
+ {
+ title: "Type",
+ field: "valueType",
+ type: "select",
+ required: true,
+ defaultValue: "STRING",
+ allowedValues: ["OBJECT", "ARRAY", "STRING", "INTEGER", "DOUBLE"],
+ },
+ {
+ title: "Content",
+ field: "value",
+ type: "custom",
+ display: {
+ visible: data => ["OBJECT", "ARRAY"].includes(data?.valueType),
+ render: (content, dataFormFieldChange) => {
+ const handleValuesChange = (content, valueType) => {
+ if (valueType === "ARRAY") {
+ // Josemi 2024-07-15 NOTE jsonEditor returns content as object, so we have to parse the returned
+ // object and get only the values in the object as array
+ dataFormFieldChange(Object.values(content?.json || {}));
+ } else {
+ dataFormFieldChange(content?.json || {});
+ }
+ };
+ const val = this.note?.valueType === "ARRAY" ? content || [] : content || {};
+ return html`
+ handleValuesChange(e.detail?.value, this.note?.valueType)}">
+
+ `;
+ },
+ },
+ },
+ {
+ title: "Content",
+ field: "value",
+ type: "input-text",
+ display: {
+ visible: data => data?.valueType === "STRING",
+ rows: 3,
+ placeholder: "Add a note content...",
+ },
+ },
+ {
+ title: "Content",
+ field: "value",
+ type: "input-num",
+ display: {
+ visible: data => ["INTEGER", "DOUBLE"].includes(data?.valueType),
+ placeholder: "0",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+}
+
+customElements.define("note-create", NoteCreate);
diff --git a/src/webcomponents/note/note-detail.js b/src/webcomponents/note/note-detail.js
new file mode 100644
index 0000000000..efa774b416
--- /dev/null
+++ b/src/webcomponents/note/note-detail.js
@@ -0,0 +1,137 @@
+/**
+ * Copyright 2015-2019 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import ExtensionsManager from "../extensions-manager.js";
+import "./note-view.js";
+import "./../commons/view/detail-tabs.js";
+
+export default class NoteDetail extends LitElement {
+
+ constructor() {
+ super();
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ opencgaSession: {
+ type: Object
+ },
+ note: {
+ type: Object
+ },
+ noteId: {
+ type: String,
+ },
+ noteScope: {
+ type: String,
+ },
+ config: {
+ type: Object
+ }
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "note-detail";
+ this._note = null;
+ this._config = this.getDefaultConfig();
+ this.#updateDetailTabs();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("note")) {
+ this.noteObserver();
+ }
+ if (changedProperties.has("noteId") || changedProperties.has("noteScope")) {
+ this.noteIdOrScopeObserver();
+ }
+ if (changedProperties.has("config")) {
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.config,
+ };
+ this.#updateDetailTabs();
+ }
+ super.update(changedProperties);
+ }
+
+ noteObserver() {
+ // No need to create a local clone of the note object
+ this._note = this.note;
+ }
+
+ noteIdOrScopeObserver() {
+ this._note = null;
+ if (this.opencgaSession && this.noteId && this.noteScope) {
+ let noteSearchPromise = null;
+ if (this.noteScope === "STUDY") {
+ noteSearchPromise = this.opencgaSession.opencgaClient.studies()
+ .searchNotes(this.opencgaSession.study.fqn, {
+ id: this.noteId,
+ });
+ } else {
+ noteSearchPromise = this.opencgaSession.opencgaClient.organization()
+ .searchNotes({
+ id: this.noteId,
+ });
+ }
+ noteSearchPromise
+ .then(response => {
+ this._note = response?.responses[0]?.results?.[0];
+ this.requestUpdate();
+ })
+ .catch(error => {
+ console.error(error);
+ });
+ }
+ }
+
+ #updateDetailTabs() {
+ this._config.items = [
+ ...this._config.items,
+ ...ExtensionsManager.getDetailTabs(this.COMPONENT_ID),
+ ];
+ }
+
+ render() {
+ if (!this.opencgaSession || !this._note) {
+ return nothing;
+ }
+
+ return html`
+
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ items: [],
+ };
+ }
+
+}
+
+customElements.define("note-detail", NoteDetail);
diff --git a/src/webcomponents/note/note-grid.js b/src/webcomponents/note/note-grid.js
new file mode 100644
index 0000000000..1d1c565e4a
--- /dev/null
+++ b/src/webcomponents/note/note-grid.js
@@ -0,0 +1,537 @@
+/**
+ * Copyright 2015-2019 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import {ifDefined} from "lit/directives/if-defined.js";
+import UtilsNew from "../../core/utils-new.js";
+import GridCommons from "../commons/grid-commons.js";
+import CatalogUtils from "../../core/clients/opencga/opencga-catalog-utils.js";
+import NotificationUtils from "../commons/utils/notification-utils.js";
+import ModalUtils from "../commons/modal/modal-utils.js";
+import "../commons/opencb-grid-toolbar.js";
+import "./note-create.js";
+import "./note-update.js";
+
+export default class NoteGrid extends LitElement {
+
+ constructor() {
+ super();
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ toolId: {
+ type: String,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ query: {
+ type: Object
+ },
+ notes: {
+ type: Array
+ },
+ active: {
+ type: Boolean
+ },
+ config: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "note-grid";
+ this._prefix = UtilsNew.randomString(8);
+ this.gridId = this._prefix + this.COMPONENT_ID;
+ this.active = true;
+ this._config = this.getDefaultConfig();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("opencgaSession") ||
+ changedProperties.has("toolId") ||
+ changedProperties.has("query") ||
+ changedProperties.has("config")) {
+ this.propertyObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.size > 0 && this.active) {
+ this.renderTable();
+ }
+ }
+
+ propertyObserver() {
+ // With each property change we must be updated config and create the columns again. No extra checks are needed.
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.config,
+ };
+ this.gridCommons = new GridCommons(this.gridId, this, this._config);
+
+ // Config for the grid toolbar
+ this.toolbarSetting = {
+ ...this._config,
+ };
+
+ this.toolbarConfig = {
+ toolId: this.toolId,
+ resource: "NOTE",
+ columns: this._getDefaultColumns(),
+ create: {
+ display: {
+ modalTitle: "Note Create",
+ modalDraggable: true,
+ modalCyDataName: "modal-create",
+ modalSize: "modal-lg"
+ },
+ render: () => html`
+
+
+ `,
+ },
+ };
+ }
+
+ fetchNote(query) {
+ const scope = query?.scope;
+ switch (scope) {
+ case "ORGANIZATION":
+ return this.opencgaSession.opencgaClient.organization()
+ .searchNotes(query);
+ case "STUDY":
+ default:
+ return this.opencgaSession.opencgaClient.studies()
+ .searchNotes(this.opencgaSession.study.fqn, query);
+ }
+ }
+
+ renderTable() {
+ // If this.notes is provided as property we render the array directly
+ if (this.notes?.length > 0) {
+ this.renderLocalTable();
+ } else {
+ this.renderRemoteTable();
+ }
+ }
+
+ renderRemoteTable() {
+ if (this.opencgaSession?.opencgaClient && this.opencgaSession?.study?.fqn) {
+ if (this.lastFilters && JSON.stringify(this.lastFilters) === JSON.stringify(this.query)) {
+ // Abort destroying and creating again the grid. The filters have not changed
+ return;
+ }
+ this._columns = this._getDefaultColumns();
+ this.table = $("#" + this.gridId);
+ this.table.bootstrapTable("destroy");
+ this.table.bootstrapTable({
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ columns: this._columns,
+ method: "get",
+ sidePagination: "server",
+ iconsPrefix: GridCommons.GRID_ICONS_PREFIX,
+ icons: GridCommons.GRID_ICONS,
+ uniqueId: "id",
+ // Table properties
+ pagination: this._config.pagination,
+ pageSize: this._config.pageSize,
+ pageList: this._config.pageList,
+ paginationVAlign: "both",
+ formatShowingRows: this.gridCommons.formatShowingRows,
+ detailView: !!this.detailFormatter,
+ gridContext: this,
+ loadingTemplate: () => GridCommons.loadingFormatter(),
+ ajax: params => {
+ let notesResponse = null;
+ this.filters = {
+ limit: params.data.limit,
+ skip: params.data.offset || 0,
+ // include: "id,scope,tags,userId,visibility,creationDate,modificationDate,valueType,uuid,version",
+ exclude: "value",
+ count: !this.table.bootstrapTable("getOptions").pageNumber || this.table.bootstrapTable("getOptions").pageNumber === 1,
+ ...this.query
+ };
+ // remove study from query
+ const {study, ...filters} = this.filters;
+ // Store the current filters
+ this.lastFilters = {...this.filters};
+ this.fetchNote(filters)
+ .then(response => {
+ notesResponse = response;
+ // Prepare data for columns extensions
+ const rows = notesResponse?.responses?.[0]?.results || [];
+ return this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, this.filters, rows);
+ })
+ .then(() => params.success(notesResponse))
+ .catch(error => {
+ console.error(error);
+ params.error(error);
+ });
+ },
+ responseHandler: response => {
+ const result = this.gridCommons.responseHandler(response, $(this.table).bootstrapTable("getOptions"));
+ return result.response;
+ },
+ onClickRow: (row, selectedElement) => this.gridCommons.onClickRow(row.id, row, selectedElement),
+ onDblClickRow: (row, element) => {
+ // We detail view is active we expand the row automatically.
+ // FIXME: Note that we use a CSS class way of knowing if the row is expand or collapse, this is not ideal but works.
+ if (this._config.detailView) {
+ if (element[0].innerHTML.includes("fa-plus")) {
+ this.table.bootstrapTable("expandRow", element[0].dataset.index);
+ } else {
+ this.table.bootstrapTable("collapseRow", element[0].dataset.index);
+ }
+ }
+ },
+ onCheck: row => {
+ this.gridCommons.onCheck(row.id, row);
+ },
+ onCheckAll: rows => {
+ this.gridCommons.onCheckAll(rows);
+ },
+ onUncheck: row => {
+ this.gridCommons.onUncheck(row.id, row);
+ },
+ onUncheckAll: rows => {
+ this.gridCommons.onUncheckAll(rows);
+ },
+ onLoadSuccess: data => {
+ this.gridCommons.onLoadSuccess(data, 1);
+ },
+ onLoadError: (e, restResponse) => this.gridCommons.onLoadError(e, restResponse),
+ });
+ }
+ }
+
+ renderLocalTable() {
+ this.table = $("#" + this.gridId);
+ this.table.bootstrapTable("destroy");
+ this.table.bootstrapTable({
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ columns: this._getDefaultColumns(),
+ // data: this.notes,
+ sidePagination: "server",
+ // Josemi Note 2024-01-18: we have added the ajax function for local variants also to support executing async calls
+ // when getting additional data from columns extensions.
+ ajax: params => {
+ const tableOptions = $(this.table).bootstrapTable("getOptions");
+ const limit = params.data.limit || tableOptions.pageSize;
+ const skip = params.data.offset || 0;
+ const rows = this.notes.slice(skip, skip + limit);
+ // Get data for extensions
+ this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, null, rows)
+ .then(() => params.success(rows))
+ .catch(error => params.error(error));
+ },
+ // Josemi Note 2024-01-18: we use this method to tell bootstrap-table how many rows we have in our data
+ responseHandler: response => {
+ return {
+ total: this.notes.length,
+ rows: response,
+ };
+ },
+ iconsPrefix: GridCommons.GRID_ICONS_PREFIX,
+ icons: GridCommons.GRID_ICONS,
+
+ // Set table properties, these are read from config property
+ uniqueId: "id",
+ pagination: this._config.pagination,
+ pageSize: this._config.pageSize,
+ pageList: this._config.pageList,
+ detailView: this._config.detailView,
+ gridContext: this,
+ loadingTemplate: () => GridCommons.loadingFormatter(),
+ onClickRow: (row, selectedElement) => this.gridCommons.onClickRow(row.id, row, selectedElement),
+ onPostBody: data => {
+ // We call onLoadSuccess to select first row
+ this.gridCommons.onLoadSuccess({rows: data, total: data.length}, 1);
+ }
+ });
+ }
+
+ _getDefaultColumns() {
+ this._columns = [
+ {
+ id: "id",
+ title: "Note ID",
+ field: "id",
+ visible: this.gridCommons.isColumnVisible("id")
+ },
+ {
+ id: "userId",
+ title: "User ID",
+ field: "userId",
+ visible: this.gridCommons.isColumnVisible("userId")
+ },
+ {
+ id: "scope",
+ title: "Scope",
+ field: "scope",
+ visible: this.gridCommons.isColumnVisible("scope")
+ },
+ {
+ id: "tags",
+ title: "Tags",
+ field: "tags",
+ formatter: tags => {
+ return (tags || []).map(t => `${t} `).join(" ") || "-";
+ },
+ visible: this.gridCommons.isColumnVisible("tags")
+ },
+ {
+ id: "type",
+ title: "Type",
+ field: "valueType",
+ visible: this.gridCommons.isColumnVisible("valueType")
+ },
+ {
+ id: "visibility",
+ title: "Visibility",
+ field: "visibility",
+ align: "center",
+ width: "5",
+ widthUnit: "%",
+ formatter: field => {
+ return ` `;
+ },
+ visible: this.gridCommons.isColumnVisible("visibility")
+ },
+ {
+ id: "dates",
+ title: "Modification / Creation Date",
+ field: "Dates",
+ halign: this.displayConfigDefault?.header?.horizontalAlign,
+ valign: "middle",
+ formatter: (field, note) => {
+ return `
+ ${UtilsNew.dateFormatter(note.modificationDate)}
+ ${UtilsNew.dateFormatter(note.creationDate)}
+ `;
+ },
+ visible: this.gridCommons.isColumnVisible("dates")
+ },
+ ];
+
+ if (this.opencgaSession && this._config.showActions) {
+ this._columns.push({
+ id: "actions",
+ title: "Actions",
+ field: "actions",
+ align: "center",
+ formatter: (value, row) => this.actionsFormatter(value, row),
+ events: {
+ "click a": this.onActionClick.bind(this),
+ },
+ // visible: !this._config.columns?.hidden?.includes("actions")
+ });
+ }
+ this._columns = this.gridCommons.addColumnsFromExtensions(this._columns, this.COMPONENT_ID);
+ return this._columns;
+ }
+
+ actionsFormatter(value, row) {
+ const user = this.opencgaSession?.user?.id;
+ let hasAdminPermissions = false;
+ // Case 1: user is an admin organization or owner. In this case, he has permission to perform any action
+ // regardless the scope of the note
+ if (CatalogUtils.isOrganizationAdmin(this.opencgaSession?.organization, user)) {
+ hasAdminPermissions = true;
+ } else {
+ // If user is not an organization admin or owner, we will check the scope of the note
+ // If the note has STUDY scope and the user is a study admin, he has permission to perform any action to this note
+ if (row.scope === "STUDY" && CatalogUtils.isAdmin(this.opencgaSession?.study, user)) {
+ hasAdminPermissions = true;
+ }
+ }
+
+ return `
+
+
+
+ Actions
+
+
+
+ `;
+ }
+ onColumnChange(e) {
+ this.gridCommons.onColumnChange(e);
+ }
+
+ onDeleteNote(note) {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Delete Note",
+ message: `Are you sure you want to delete note '${note.id}'? This action can not be undone.`,
+ ok: () => {
+ let deleteNotePromise = null;
+ if (note.scope === "STUDY") {
+ deleteNotePromise = this.opencgaSession.opencgaClient.studies()
+ .deleteNotes(this.opencgaSession.study.fqn, note.id);
+ } else {
+ deleteNotePromise = this.opencgaSession.opencgaClient.organization()
+ .deleteNotes(note.id);
+ }
+ deleteNotePromise
+ .then(() => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ message: `Note '${note.id}' has been removed`,
+ });
+ // Force to render the table again
+ this.renderTable();
+ })
+ .catch(error => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
+ });
+ },
+ });
+ }
+
+ async onActionClick(e, _, row) {
+ const action = e.target.dataset.action?.toLowerCase() || e.detail.action;
+ switch (action) {
+ case "edit":
+ this.noteUpdate = row;
+ this.requestUpdate();
+ await this.updateComplete;
+ ModalUtils.show(`${this._prefix}UpdateModal`);
+ break;
+ case "copy-json":
+ this.fetchNote({id: row.id, scope: row.scope})
+ .then(response => {
+ UtilsNew.copyToClipboard(JSON.stringify(response.responses[0].results[0], null, "\t"));
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ message: "Note JSON copied to clipboard.",
+ });
+ })
+ .catch(error => {
+ console.error(error);
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
+ });
+ break;
+ case "download-json":
+ this.fetchNote({id: row.id, scope: row.scope})
+ .then(response => {
+ UtilsNew.downloadData([JSON.stringify(response.responses[0].results[0], null, "\t")], row.id + ".json");
+ })
+ .catch(error => {
+ console.error(error);
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
+ });
+ break;
+ case "delete":
+ this.onDeleteNote(row);
+ break;
+ }
+ }
+
+ renderModalUpdate() {
+ return ModalUtils.create(this, `${this._prefix}UpdateModal`, {
+ display: {
+ modalTitle: `Note Update: ${this.noteUpdate?.id}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-update",
+ modalSize: "modal-lg"
+ },
+ render: active => html`
+
+
+ `,
+ });
+ }
+
+ render() {
+ return html`
+ ${this._config.showToolbar ? html`
+ this.onActionClick(e)}"
+ @noteCreate="${this.renderTable}">
+
+ ` : nothing}
+
+
+
+ ${this.renderModalUpdate()}
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ pagination: true,
+ pageSize: 10,
+ pageList: [5, 10, 25],
+ multiSelection: false,
+ showSelectCheckbox: false,
+ showToolbar: true,
+ showActions: true,
+
+ showCreate: true,
+ showExport: false,
+ showSettings: true,
+ exportTabs: ["download", "link", "code"],
+ };
+ }
+
+}
+
+customElements.define("note-grid", NoteGrid);
diff --git a/src/webcomponents/note/note-update.js b/src/webcomponents/note/note-update.js
new file mode 100644
index 0000000000..ec71816fc9
--- /dev/null
+++ b/src/webcomponents/note/note-update.js
@@ -0,0 +1,230 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import Types from "../commons/types.js";
+import UtilsNew from "../../core/utils-new.js";
+import "../commons/tool-header.js";
+import "../commons/filters/catalog-search-autocomplete.js";
+import "../commons/forms/select-token-filter-static.js";
+
+export default class NoteUpdate extends LitElement {
+
+ constructor() {
+ super();
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ noteId: {
+ type: String,
+ },
+ noteScope: {
+ type: String,
+ },
+ active: {
+ type: Boolean,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.active = true;
+ this.displayConfig = {};
+ this._note = null;
+ this._config = this.getDefaultConfig();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("noteId") || changedProperties.has("noteScope") || changedProperties.has("opencgaSession" || changedProperties.has("active"))) {
+ this.noteObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this._config = this.getDefaultConfig();
+ }
+ super.update(changedProperties);
+ }
+
+ noteObserver() {
+ this._note = null;
+ if (this.noteId && this.noteScope && this.opencgaSession && this.active) {
+ let noteRequest = null;
+ if (this.noteScope === "STUDY") {
+ noteRequest = this.opencgaSession.opencgaClient.studies()
+ .searchNotes(this.opencgaSession.study.fqn, {
+ id: this.noteId,
+ });
+ } else if (this.noteScope === "ORGANIZATION") {
+ noteRequest = this.opencgaSession.opencgaClient.organization()
+ .searchNotes({
+ id: this.noteId,
+ });
+ } else {
+ console.error(`Unexpected note scope provided. Expected 'STUDY' or 'ORGANIZATION' but got ${this.noteScope}`);
+ return;
+ }
+ noteRequest
+ .then(response => {
+ this._note = response.getResult(0) || {};
+ this.requestUpdate();
+ })
+ .catch(reason => {
+ console.error(reason);
+ });
+ }
+ }
+
+ onNoteUpdate(event) {
+ this._note = event.detail.value;
+ this.requestUpdate();
+ }
+
+ render() {
+ if (!this._note) {
+ return html` `;
+ }
+
+ return html`
+ this.onNoteUpdate(e)}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return Types.dataFormConfig({
+ display: {
+ ...this.displayConfigDefault,
+ ...(this.displayConfig || {}),
+ },
+ sections: [
+ {
+ title: "General Information",
+ elements: [
+ {
+ title: "Scope",
+ field: "scope",
+ type: "toggle-buttons",
+ defaultValue: "STUDY",
+ allowedValues: ["STUDY", "ORGANIZATION"],
+ display: {
+ disabled: true,
+ }
+ },
+ {
+ title: "Note ID",
+ field: "id",
+ type: "input-text",
+ display: {
+ disabled: true,
+ }
+ },
+ {
+ title: "Visibility",
+ field: "visibility",
+ type: "toggle-buttons",
+ required: true,
+ defaultValue: "PUBLIC",
+ allowedValues: ["PUBLIC", "PRIVATE"],
+ },
+ {
+ title: "Tags",
+ field: "tags",
+ type: "custom",
+ display: {
+ render: (data, dataFormFilterChange) => {
+ const handleTagsFilterChange = e => {
+ dataFormFilterChange(e.detail.value ? e.detail.value?.split(",") : []);
+ };
+ return html`
+ handleTagsFilterChange(e)}">
+
+ `;
+ },
+ },
+ },
+ {
+ title: "Content",
+ field: "value",
+ type: "custom",
+ display: {
+ visible: data => ["OBJECT", "ARRAY"].includes(data?.valueType),
+ render: (content, dataFormFieldChange) => {
+ const handleValuesChange = (content, valueType) => {
+ if (valueType === "ARRAY") {
+ dataFormFieldChange(Object.values(content?.json || {}));
+ } else {
+ dataFormFieldChange(content?.json || {});
+ }
+ };
+ const val = this._note?.valueType === "ARRAY" ? content || [] : content || {};
+ return html`
+ handleValuesChange(e.detail?.value, this._note?.valueType)}">
+
+ `;
+ },
+ },
+ },
+ {
+ title: "Content",
+ field: "value",
+ type: "input-text",
+ display: {
+ visible: data => data?.valueType === "STRING",
+ rows: 3,
+ placeholder: "Add a note content...",
+ },
+ },
+ {
+ title: "Content",
+ field: "value",
+ type: "input-num",
+ display: {
+ visible: data => ["INTEGER", "DOUBLE"].includes(data?.valueType),
+ placeholder: "0",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+}
+
+customElements.define("note-update", NoteUpdate);
diff --git a/src/webcomponents/note/note-view.js b/src/webcomponents/note/note-view.js
new file mode 100644
index 0000000000..21076c896d
--- /dev/null
+++ b/src/webcomponents/note/note-view.js
@@ -0,0 +1,223 @@
+/**
+ * Copyright 2015-2019 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import UtilsNew from "../../core/utils-new.js";
+import Types from "../commons/types.js";
+import "../commons/forms/data-form.js";
+import "../commons/filters/catalog-search-autocomplete.js";
+import "../study/annotationset/annotation-set-view.js";
+import "../loading-spinner.js";
+
+export default class NoteView extends LitElement {
+
+ constructor() {
+ super();
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ note: {
+ type: Object,
+ },
+ noteId: {
+ type: String,
+ },
+ noteScope: {
+ type: String,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object,
+ },
+ };
+ }
+
+ #init() {
+ this.displayConfigDefault = {
+ buttonsVisible: false,
+ collapsable: true,
+ titleVisible: false,
+ titleWidth: 2,
+ defaultValue: "-",
+ pdf: false,
+ };
+ this._note = null;
+ this._config = this.getDefaultConfig();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("noteId") || changedProperties.has("noteScope")) {
+ this.noteIdOrScopeObserver();
+ }
+ if (changedProperties.has("note")) {
+ this.noteObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this._config = this.getDefaultConfig();
+ }
+ super.update(changedProperties);
+ }
+
+ noteObserver() {
+ this._note = this.note;
+ }
+
+ noteIdOrScopeObserver() {
+ this._note = null;
+ if (this.opencgaSession && this.noteId && this.noteScope) {
+ let noteSearchPromise = null;
+ if (this.noteScope === "STUDY") {
+ noteSearchPromise = this.opencgaSession.opencgaClient.studies()
+ .searchNotes(this.opencgaSession.study.fqn, {
+ id: this.noteId,
+ });
+ } else {
+ noteSearchPromise = this.opencgaSession.opencgaClient.organization()
+ .searchNotes({
+ id: this.noteId,
+ });
+ }
+ noteSearchPromise
+ .then(response => {
+ this._note = response?.responses[0]?.results?.[0];
+ this.requestUpdate();
+ })
+ .catch(error => {
+ console.error(error);
+ });
+ }
+ }
+
+ render() {
+ if (!this._note) {
+ return nothing;
+ }
+
+ return html`
+
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return Types.dataFormConfig({
+ title: "Summary",
+ icon: "",
+ display: {
+ ...this.displayConfigDefault,
+ ...(this.displayConfig || {}),
+ },
+ sections: [
+ {
+ title: "General",
+ collapsed: false,
+ elements: [
+ {
+ title: "Note ID",
+ type: "complex",
+ display: {
+ template: "${id} (UUID: ${uuid})",
+ style: {
+ id: {
+ "font-weight": "bold",
+ }
+ },
+ },
+ },
+ {
+ title: "Scope",
+ field: "scope"
+ },
+ {
+ title: "User",
+ field: "userId"
+ },
+ {
+ title: "Visibility",
+ field: "visibility"
+ },
+ {
+ title: "Type",
+ field: "valueType"
+ },
+ {
+ title: "Version",
+ field: "version",
+ },
+ {
+ title: "Tags",
+ field: "tags",
+ type: "custom",
+ display: {
+ render: tags => {
+ return (tags || []).map(t => `${t} `).join(" ") || "-";
+ },
+ },
+ },
+ {
+ title: "Creation Date",
+ field: "creationDate",
+ display: {
+ format: date => UtilsNew.dateFormatter(date),
+ },
+ },
+ {
+ title: "Modification Date",
+ field: "modificationDate",
+ display: {
+ format: date => UtilsNew.dateFormatter(date),
+ },
+ },
+ {
+ title: "Content",
+ type: "custom",
+ display: {
+ render: note => {
+ let content = "";
+ switch (note?.valueType) {
+ case "OBJECT":
+ case "ARRAY":
+ content = `${JSON.stringify(note.value, null, " ")} `;
+ break;
+ case "STRING":
+ case "INTEGER":
+ case "DOUBLE":
+ default:
+ content = note?.value;
+ }
+ return content;
+ },
+ },
+ },
+ ],
+ },
+ ],
+ });
+ }
+
+}
+
+customElements.define("note-view", NoteView);
diff --git a/src/webcomponents/organization/admin/filters/user-status-filter.js b/src/webcomponents/organization/admin/filters/user-status-filter.js
new file mode 100644
index 0000000000..0d2086c7aa
--- /dev/null
+++ b/src/webcomponents/organization/admin/filters/user-status-filter.js
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015-2016 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import "../../../commons/forms/select-field-filter.js";
+import UtilsNew from "../../../../core/utils-new.js";
+import LitUtils from "../../../commons/utils/lit-utils";
+
+export default class UserStatusFilter extends LitElement {
+
+ constructor() {
+ super();
+
+ // Set status and init private properties
+ this._init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ status: {
+ type: Object,
+ },
+ config: {
+ type: Array
+ },
+ disabled: {
+ type: Boolean
+ },
+ };
+ }
+
+ _init() {
+ this.disabled = false;
+ }
+
+ onFilterChange(e) {
+ LitUtils.dispatchCustomEvent(this, "filterChange", e.currentTarget.value);
+ }
+
+ render() {
+ debugger
+ return html`
+
+ ${this.config.map(status => html`
+ ${status.isSelectable ? html`
+ this.onFilterChange(e)}"
+ value="${status.id}"
+ ?checked="${status.id === this.status}">
+
+ ${status.displayLabel}
+
+ `: nothing}
+ `)}
+
+ `;
+ }
+
+}
+
+customElements.define("user-status-filter", UserStatusFilter);
diff --git a/src/webcomponents/organization/admin/group-admin-browser.js b/src/webcomponents/organization/admin/group-admin-browser.js
new file mode 100644
index 0000000000..f2b9fce96f
--- /dev/null
+++ b/src/webcomponents/organization/admin/group-admin-browser.js
@@ -0,0 +1,271 @@
+/**
+ * Copyright 2015-2024 OpenCB *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * The group admin browser component has two key responsibilities:
+ * 1. Retrieving groups per study
+ * 2. Rendering the graphic filters if enabled and grid.
+ * Accepted properties are:
+ * - A single study
+ * - An organization with multiple projects/studies
+ */
+
+import {LitElement, html} from "lit";
+import LitUtils from "../../commons/utils/lit-utils";
+import UtilsNew from "../../../core/utils-new.js";
+import "./group-admin-grid.js";
+
+export default class GroupAdminBrowser extends LitElement {
+
+ /* -----------------------------------------------------------------------------------------------------------------
+ CONSTRUCTOR AND PROPERTIES
+ ----------------------------------------------------------------------------------------------------------------- */
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ studyId: {
+ type: String,
+ },
+ study: {
+ type: Object,
+ },
+ organizationId: {
+ type: String,
+ },
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ // QUESTION: pending to decide if we allow browser settings here.
+ settings: {
+ type: Object,
+ },
+ };
+ }
+
+ /* -----------------------------------------------------------------------------------------------------------------
+ PRIVATE METHODS
+ ----------------------------------------------------------------------------------------------------------------- */
+ #init() {
+ this.COMPONENT_ID = "groups-admin-browser";
+ this._groups = [];
+ this._studies = [];
+ this._study = {};
+ this._config = this.getDefaultConfig();
+ this.isLoading = false;
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #prepareGroups(groups, project, study) {
+ /*
+ if (this.study) {
+ this._groups = [];
+ this._studies = [this.opencgaSession.study];
+ this.study.groups?.forEach(group => {
+ const newGroup = {
+ studyId: this.study.id,
+ fqn: this.study.fqn,
+ group: group,
+ isGroupProtected: !!(group.id === "@admins" || group.id === "@members"),
+ };
+ this._groups.push(newGroup);
+ });
+ }
+ */
+ // Get all study groups
+ const p = project || this.opencgaSession.project;
+ const s = study || this.opencgaSession.study;
+
+ this._studies.push({
+ projectId: p.id,
+ fqn: s.fqn,
+ name: s.alias,
+ });
+ this._groups = groups.map(group => ({
+ ...group,
+ fqn: s.fqn,
+ studyId: s.id,
+ projectId: p.id,
+ isProtected: !!(group.id === "@admins" || group.id === "@members"),
+ }));
+ }
+
+ /* -----------------------------------------------------------------------------------------------------------------
+ LIT LIFE-CYCLE
+ ----------------------------------------------------------------------------------------------------------------- */
+ update(changedProperties) {
+ if (changedProperties.has("organizationId")) {
+ this.organizationIdObserver();
+ }
+ if (changedProperties.has("organization")) {
+ this.organizationObserver();
+ }
+ if (changedProperties.has("studyId")) {
+ this.studyIdObserver();
+ }
+ if (changedProperties.has("study")) {
+ this.studyObserver();
+ }
+ if (changedProperties.has("settings")) {
+ this.settingsObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ /* -----------------------------------------------------------------------------------------------------------------
+ OBSERVERS
+ ----------------------------------------------------------------------------------------------------------------- */
+ organizationObserver() {
+ // Get all organization groups
+ if (this.organization) {
+ this._groups = [];
+ this._studies = [];
+ this.organization?.projects?.forEach(project => {
+ project.studies?.forEach(study => {
+ this.#prepareGroups(study.groups, project, study);
+ });
+ });
+ }
+ }
+
+ organizationIdObserver() {
+ if (this.organizationId && this.opencgaSession) {
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.organization()
+ .info(this.organizationId)
+ .then(response => {
+ this.organization = UtilsNew.objectClone(response.responses[0].results[0]);
+ })
+ .catch(reason => {
+ this.organization = {};
+ error = reason;
+ console.error(reason);
+ })
+ .finally(() => {
+ this._config = this.getDefaultConfig();
+ LitUtils.dispatchCustomEvent(this, "organizationInfo", this.organization, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ studyObserver() {
+ if (this.study) {
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.studies()
+ .groups(this.study.fqn)
+ .then(response => {
+ const groups = response.responses[0].results;
+ this.#prepareGroups(groups);
+ })
+ .catch(reason => {
+ error = reason;
+ console.error(reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ studyIdObserver() {
+ if (this.studyId && this.opencgaSession) {
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.studies()
+ .info(this.studyId)
+ .then(response => {
+ this.study = UtilsNew.objectClone(response.responses[0].results[0]);
+ })
+ .catch(reason => {
+ error = reason;
+ console.error(reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ settingsObserver() {
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.settings,
+ };
+ }
+
+ /* -----------------------------------------------------------------------------------------------------------------
+ RENDER
+ ----------------------------------------------------------------------------------------------------------------- */
+ renderFilterGraphics() {
+ if (this._config.showGraphicFilters) {
+ return html `
+
+ `;
+ }
+ }
+
+ render() {
+ if (!this.opencgaSession) {
+ return html`Not valid session
`;
+ }
+
+ if (this._groups.length > 0 && this._studies.length > 0) {
+ return html `
+
+ ${this.renderFilterGraphics()}
+
+
+
+ `;
+ }
+ }
+
+ /* -----------------------------------------------------------------------------------------------------------------
+ DEFAULT CONFIG
+ ----------------------------------------------------------------------------------------------------------------- */
+ getDefaultConfig() {
+ return {
+ showGraphicFilters: false,
+ };
+ }
+
+}
+
+customElements.define("group-admin-browser", GroupAdminBrowser);
diff --git a/src/webcomponents/organization/admin/group-admin-create.js b/src/webcomponents/organization/admin/group-admin-create.js
new file mode 100644
index 0000000000..8dbff9f051
--- /dev/null
+++ b/src/webcomponents/organization/admin/group-admin-create.js
@@ -0,0 +1,249 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html} from "lit";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import UtilsNew from "../../../core/utils-new";
+
+export default class GroupAdminCreate extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ studyFqn: {
+ type: String,
+ },
+ studies: {
+ type: Array,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object,
+ },
+ };
+ }
+
+ #init() {
+ this.isLoading = false;
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ titleWidth: 3,
+ defaultLayout: "horizontal",
+ buttonOkText: "Create"
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ #initOriginalObjects() {
+ this.group = {};
+ this.allowedValues = [];
+ if (this.studies && this.opencgaSession && Array.isArray(this.studies)) {
+ if (this.studies.length === 1) {
+ this.group.listStudies = [this.studies[0]];
+ } else {
+ // 1. Prepare structure for displaying studies per project in dropdown
+ const projects = this.studies.reduce((acc, {fqn, name, projectId}) => {
+ const study = {fqn, name};
+ const item = acc.find(y => y.projectId === projectId);
+ (item) ? item.studies.push(study) :
+ acc.push({projectId: projectId, studies: [study]});
+ return acc;
+ }, []);
+ // 2. Fill allowed values
+ this.allowedValues = projects
+ .filter(({studies}) => studies.length > 0)
+ .map(({projectId, studies}) => ({
+ name: `Project '${projectId}'`,
+ fields: studies.map(({fqn, name}) => ({id: fqn, name}))
+ }));
+ }
+ }
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("studyFqn") ||
+ changedProperties.has("opencgaSession")) {
+ this.studyFqnObserver();
+ }
+ if (changedProperties.has("studies") ||
+ changedProperties.has("opencgaSession")) {
+ this.#initOriginalObjects();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig
+ };
+ this._config = this.getDefaultConfig();
+ }
+ super.update(changedProperties);
+ }
+
+ studyFqnObserver() {
+ if (this.studyFqn && this.opencgaSession) {
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.studies()
+ .info(this.studyFqn)
+ .then(response => {
+ this.study = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.studies = [this.study];
+ })
+ .catch(reason => {
+ error = reason;
+ console.error(reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ onFieldChange(e, field) {
+ const param = field || e.detail.param;
+ // 1. Update group id
+ if (param === "id") {
+ // QUESTION 20240325 Vero: verify group name starts with @?
+ this.group.id = e.detail.data.id;
+ }
+ // 2. Update the list of studies
+ if (param === "fqn") {
+ this.group.listStudies = e.detail.value?.length > 0 ? e.detail.value?.split(",") : [];
+ }
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Clear group",
+ message: "Are you sure to clear?",
+ ok: () => {
+ this.#initOriginalObjects();
+ this.requestUpdate();
+ },
+ });
+ }
+
+ onSubmit() {
+ const params = {
+ includeResult: true,
+ action: "ADD",
+ };
+ this.#setLoading(true);
+ const groupPromises = (this.group.listStudies || [])
+ .map(study => {
+ let error;
+ return this.opencgaSession.opencgaClient.studies()
+ .updateGroups(study.fqn, {id: this.group.id}, params)
+ .then(() => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `Group Create`,
+ message: `Group ${this.group.id} in study ${study.fqn} CREATED successfully`,
+ });
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "groupCreate", {}, {
+ group: this.group,
+ studyFqn: study,
+ }, error);
+ });
+ });
+
+ Promise.all(groupPromises)
+ .finally(() => {
+ this.#setLoading(false);
+ this.#initOriginalObjects();
+ LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", {});
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ return html`
+ this.onFieldChange(e)}"
+ @clear="${e => this.onClear(e)}"
+ @submit="${e => this.onSubmit(e)}">
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ display: this.displayConfig || this.displayConfigDefault,
+ sections: [
+ {
+ // title: "General Information",
+ elements: [
+ {
+ title: "Group ID",
+ field: "id",
+ type: "input-text",
+ required: true,
+ display: {
+ placeholder: "Add a short ID...",
+ helpMessage: `The group ID must start with the character '@' [e.g.'@myNewGroup'].`,
+ },
+ },
+ {
+ title: "Study",
+ field: "fqn",
+ type: "select",
+ multiple: true,
+ all: true,
+ required: true,
+ allowedValues: this.allowedValues,
+ display: {
+ visible: !!this.allowedValues?.length,
+ placeholder: "Select study or studies..."
+ },
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("group-admin-create", GroupAdminCreate);
diff --git a/src/webcomponents/organization/admin/group-admin-delete.js b/src/webcomponents/organization/admin/group-admin-delete.js
new file mode 100644
index 0000000000..9083eb7124
--- /dev/null
+++ b/src/webcomponents/organization/admin/group-admin-delete.js
@@ -0,0 +1,160 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html} from "lit";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+
+export default class GroupAdminDelete extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ group: {
+ type: Object,
+ },
+ active: {
+ type: Boolean,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object,
+ },
+ };
+ }
+
+ #init() {
+ this.isLoading = false;
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ titleWidth: 3,
+ defaultLayout: "horizontal",
+ buttonOkText: "Delete",
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("displayConfig")) {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig,
+ };
+ this._config = this.getDefaultConfig();
+ }
+ super.update(changedProperties);
+ }
+
+ onSubmit() {
+ this.#setLoading(true);
+ let error;
+ return this.opencgaSession.opencgaClient.studies()
+ .updateGroups(this.group.fqn, {id: this.group.id}, {action: "REMOVE"})
+ .then(() => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `Group Delete`,
+ message: `Group ${this.group.id} in study ${this.group.fqn} deleted successfully`,
+ });
+ LitUtils.dispatchCustomEvent(this, "groupDelete", {});
+ LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", {});
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ return html`
+ this.onSubmit(e)}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ display: this.displayConfig || this.displayConfigDefault,
+ sections: [
+ {
+ // title: `Are you sure you want to remove group '${this.group?.id}'?`,
+ elements: [
+ {
+ type: "notification",
+ text: "The following users could have unexpected permissions if you remove this group",
+ display: {
+ visible: true,
+ icon: "fas fa-exclamation-triangle",
+ notificationType: "error",
+ },
+ },
+ {
+ // name: "UserIds",
+ field: "users",
+ type: "list",
+ display: {
+ separator: " ",
+ contentLayout: "bullets",
+ transform: users => users.length ?
+ users.map(user => ({userId: user.id})) :
+ [{userId: "This group does not have users"}],
+ template: "${userId}",
+ // FIXME: why is not working?
+ // className: {
+ // "userId": "badge badge-pill badge-primary",
+ // },
+ // style: {
+ // "userId": {
+ // "color": "white",
+ // "background-color": "blue"
+ // },
+ // }
+ },
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("group-admin-delete", GroupAdminDelete);
diff --git a/src/webcomponents/organization/admin/group-admin-grid.js b/src/webcomponents/organization/admin/group-admin-grid.js
new file mode 100644
index 0000000000..e63fdb0880
--- /dev/null
+++ b/src/webcomponents/organization/admin/group-admin-grid.js
@@ -0,0 +1,413 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import GridCommons from "../../commons/grid-commons.js";
+import UtilsNew from "../../../core/utils-new.js";
+import ModalUtils from "../../commons/modal/modal-utils.js";
+import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import "./group-admin-create.js";
+import "./group-admin-permissions-update.js";
+import "./group-admin-delete.js";
+
+export default class GroupAdminGrid extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ toolId: {
+ type: String,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ groups: {
+ type: Array,
+ },
+ studies: {
+ type: Object,
+ },
+ config: {
+ type: Object,
+ },
+ active: {
+ type: Boolean,
+ },
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "group-grid";
+ this._prefix = UtilsNew.randomString(8);
+ this.gridId = this._prefix + this.COMPONENT_ID;
+ this.active = true;
+ this._config = this.getDefaultConfig();
+ this.action = "";
+ this.studyFqn = "";
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("opencgaSession") ||
+ changedProperties.has("groups") ||
+ changedProperties.has("toolId") ||
+ changedProperties.has("studies") ||
+ changedProperties.has("config")) {
+ this.propertyObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.size > 0 && this.active) {
+ if (this.groups?.length > 0) {
+ this.renderLocalTable();
+ }
+ }
+ }
+
+ propertyObserver() {
+ // With each property change we must be updated config and create the columns again. No extra checks are needed.
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.config,
+ };
+
+ this.gridCommons = new GridCommons(this.gridId, this, this._config);
+
+ // Config for the grid toolbar
+ this.toolbarSetting = {
+ ...this._config,
+ };
+
+ this.toolbarConfig = {
+ toolId: this.toolId,
+ resource: "GROUPS",
+ columns: this._getDefaultColumns(),
+ create: {
+ display: {
+ modalTitle: "Group Create",
+ modalDraggable: true,
+ modalCyDataName: "modal-create",
+ modalSize: "modal-lg"
+ },
+ render: () => html `
+
+
+ `,
+ },
+ };
+
+ this.permissions = {
+ "organization": () => OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) ? "" : "disabled",
+ "study": () => OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) ? "" : "disabled",
+ };
+
+ this.modals = {
+ /*
+ "edit-details": {
+ label: "Edit Details",
+ icon: "fas fa-edit",
+ modalId: `${this._prefix}UpdateDetailsModal`,
+ render: () => this.renderModalDetailsUpdate(),
+ permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled",
+ },
+ */
+ "edit-permissions": {
+ label: "Edit Permissions",
+ icon: "far fa-edit",
+ modalId: `${this._prefix}UpdatePermissionsModal`,
+ render: () => this.renderModalPermissionsUpdate(),
+ permission: this.permissions["study"](),
+ divider: true,
+ },
+ "delete": {
+ label: "Delete Group",
+ icon: "far fa-trash-alt ",
+ color: "text-danger",
+ modalId: `${this._prefix}DeleteModal`,
+ render: () => this.renderModalDelete(),
+ permission: this.permissions["study"](),
+ },
+ };
+ }
+
+ renderLocalTable() {
+ this.table = $("#" + this.gridId);
+ this.table.bootstrapTable("destroy");
+ this.table.bootstrapTable({
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ columns: this._getDefaultColumns(),
+ sidePagination: "server",
+ // JoseMi Note 2024-01-18: we have added the ajax function for local variants also to support executing
+ // async calls when getting additional data from columns extensions.
+ ajax: params => {
+ const tableOptions = $(this.table).bootstrapTable("getOptions");
+ const limit = params.data.limit || tableOptions.pageSize;
+ const skip = params.data.offset || 0;
+ const rows = this.groups.slice(skip, skip + limit);
+
+ // Get data for extensions
+ this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, null, rows)
+ .then(() => params.success(rows))
+ .catch(error => params.error(error));
+ },
+ // JoseMi Note 2024-01-18: we use this method to tell bootstrap-table how many rows we have in our data
+ responseHandler: response => {
+ return {
+ total: this.groups.length,
+ rows: response,
+ };
+ },
+ iconsPrefix: GridCommons.GRID_ICONS_PREFIX,
+ icons: GridCommons.GRID_ICONS,
+
+ // Set table properties, these are read from config property
+ uniqueId: "id",
+ pagination: this._config.pagination,
+ pageSize: this._config.pageSize,
+ pageList: this._config.pageList,
+ detailView: this._config.detailView,
+ loadingTemplate: () => GridCommons.loadingFormatter(),
+ });
+ }
+
+ _getDefaultColumns() {
+ this._columns = [
+ {
+ title: "Group ID",
+ field: "id",
+ visible: this.gridCommons.isColumnVisible("group.id"),
+ formatter: (value, row) => this.groupIdFormatter(value, row),
+ },
+ {
+ title: "Study ID",
+ field: "studyId",
+ visible: this.gridCommons.isColumnVisible("studyId")
+ },
+ {
+ title: "Project ID",
+ field: "projectId",
+ visible: this.gridCommons.isColumnVisible("projectId")
+ },
+ {
+ title: "No.Users",
+ field: "users",
+ formatter: (value, row) => this.groupNoUsersFormatter(value, row),
+ },
+ {
+ title: "Users IDs",
+ field: "users",
+ formatter: (value, row) => this.groupUsersFormatter(value, row),
+ },
+ ];
+
+ if (this._config.annotations?.length > 0) {
+ this.gridCommons.addColumnsFromAnnotations(this._columns, CatalogGridFormatter.customAnnotationFormatter, this._config);
+ }
+
+ if (this.opencgaSession && this._config.showActions) {
+ this._columns.push({
+ id: "actions",
+ title: "Actions",
+ field: "actions",
+ align: "center",
+ formatter: (value, row) => `
+
+
+ ${row.isProtected ? `
+
+ ` : `
+
+ `}
+ Actions
+
+
+
+ `,
+ events: {
+ "click ul>li>a": (e, value, row) => this.onActionClick(e, value, row),
+ },
+ });
+ }
+
+ this._columns = this.gridCommons.addColumnsFromExtensions(this._columns, this.COMPONENT_ID);
+ return this._columns;
+ }
+
+ // *** FORMATTERS ***
+ groupIdFormatter(value, row) {
+ return row.isProtected ? `
+
+ ${value}
+ PROTECTED
+
+ ` : `
+ ${value}
+ `;
+ }
+
+ groupNoUsersFormatter(value) {
+ return `
+ ${value.length}
+ `;
+ }
+
+ groupUsersFormatter(value) {
+ return value.map(user => `
+ ${user.id}
+ `);
+ }
+
+ // *** EVENTS ***
+ async onActionClick(e, value, row) {
+ this.action = e.currentTarget.dataset.action;
+ this.groupId = row.id;
+ this.group = this.groups.find(g=> g.id === this.groupId);
+ this.studyFqn = row.fqn;
+ this.requestUpdate();
+ await this.updateComplete;
+ ModalUtils.show(this.modals[this.action]["modalId"]);
+ }
+
+ onGroupEvent(e, id) {
+ this.studyFqn = e.detail.studyFqn;
+ ModalUtils.close(id);
+ }
+
+ // *** RENDER METHODS ***
+ renderModalPermissionsUpdate() {
+ return ModalUtils.create(this, `${this._prefix}UpdatePermissionsModal`, {
+ display: {
+ modalTitle: `Permissions Update: Group ${this.groupId} in Study ${this.studyFqn}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-update",
+ modalSize: "modal-lg"
+ },
+ render: active => html`
+ this.onGroupEvent(e, `${this._prefix}UpdatePermissionsModal`)}">
+
+ `,
+ });
+ }
+
+ renderModalDelete() {
+ return ModalUtils.create(this, `${this._prefix}DeleteModal`, {
+ display: {
+ // modalTitle: `Group Delete: ${this.group?.id} in study ${this.studyFqn}`,
+ modalTitle: `Are you sure you want to remove group '${this.groupId}' in study '${this.studyFqn}'?`,
+ modalDraggable: true,
+ modalCyDataName: "modal-update",
+ modalSize: "modal-lg"
+ },
+ render: active => html`
+ this.onGroupEvent(e, `${this._prefix}DeleteModal`)}">
+
+ `,
+ });
+ }
+
+ renderToolbar() {
+ if (this._config.showToolbar) {
+ // @groupCreate="${e => this.onGroupEvent(e, `${this._prefix}Modal`)}"
+ return html `
+
+
+ `;
+ }
+ }
+
+ render() {
+ return html`
+
+ ${this.renderToolbar()}
+
+
+
+ ${this.action ? this.modals[this.action]["render"](): nothing}
+ `;
+ }
+
+ // *** DEFAULT CONFIG ***
+ getDefaultConfig() {
+ return {
+ pagination: true,
+ pageSize: 10,
+ pageList: [5, 10, 25],
+ multiSelection: false,
+ showSelectCheckbox: false,
+
+ showToolbar: true,
+ showActions: true,
+
+ showCreate: true,
+ showExport: false,
+ showSettings: false,
+ exportTabs: ["download", "link", "code"],
+ };
+ }
+
+}
+
+customElements.define("group-admin-grid", GroupAdminGrid);
diff --git a/src/webcomponents/organization/admin/group-admin-permissions-update.js b/src/webcomponents/organization/admin/group-admin-permissions-update.js
new file mode 100644
index 0000000000..f985aef79c
--- /dev/null
+++ b/src/webcomponents/organization/admin/group-admin-permissions-update.js
@@ -0,0 +1,346 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import FormUtils from "../../commons/forms/form-utils";
+import NotificationUtils from "../../commons/utils/notification-utils";
+
+export default class GroupAdminPermissionsUpdate extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ groupId: {
+ type: String
+ },
+ studyFqn: {
+ type: String,
+ },
+ active: {
+ type: Boolean,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.displayConfig = {};
+ this.updatedFields = {};
+
+ this.permissions = {}; // Original permissions
+ this._permissions = {}; // Updated permissions
+
+ this.templates = {
+ "custom": {
+
+ },
+ "analyst": {
+ descriptionShort: "Full READ and WRITE (not DELETE) permissions",
+ description: `The member (user or group) will be given full READ and WRITE (not DELETE) permissions
+ for all the entries related to the study. These users will be able to view and do modifications on
+ all the data that is related to the study.`,
+ permissions: [
+ ""
+ ],
+ },
+ "view_only": {
+ descriptionShort: "Full READ permissions",
+ description: "The member (user or group) will be given full READ permissions.",
+ permissions: [
+ "VIEW_SAMPLES",
+ "VIEW_SAMPLE_ANNOTATIONS",
+ "VIEW_AGGREGATED_VARIANTS",
+ "VIEW_SAMPLE_VARIANTS",
+ "VIEW_INDIVIDUALS",
+ "VIEW_INDIVIDUAL_ANNOTATIONS",
+ "VIEW_FAMILIES",
+ "VIEW_FAMILY_ANNOTATIONS",
+ "VIEW_COHORTS",
+ "VIEW_COHORT_ANNOTATIONS",
+ "VIEW_FILES",
+ "VIEW_FILE_HEADER",
+ "VIEW_FILE_CONTENT",
+ "DOWNLOAD_FILES",
+ "VIEW_JOBS",
+ "EXECUTE_JOBS",
+ "VIEW_PANELS",
+ "VIEW_CLINICAL_ANALYSIS",
+ ],
+ },
+ };
+
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ defaultLayout: "horizontal",
+ labelAlign: "right",
+ labelWidth: 3,
+ buttonOkText: "Update",
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #initConfigNotification() {
+ this._config.notification = {
+ title: "",
+ text: "Some changes have been done in the form. Not saved changes will be lost",
+ type: "notification",
+ display: {
+ visible: () => {
+ return UtilsNew.isNotEmpty(this.updatedFields);
+ },
+ notificationType: "warning",
+ },
+ };
+ }
+
+ #initPermissions() {
+ // 1. Group contains params: (a) id: e.g. "@admins", (b) userIds: e.g. ["test"]
+ // this.group = this._study.groups.find(group => group.id === this.groupId);
+ // 2. In the update form, we need to manage as well the permissions of this group.
+ // Retrieve ACL permissions. Check if this study group has acl
+ // const groupPermissions = this._study?.acl
+ // ?.find(acl => acl.member === this.opencgaSession.user.id)?.groups
+ // ?.find(group => group.id === this.group.id)?.permissions || [];
+ // 3. Add current permissions and template key to the object group
+ // this.group = {
+ // permissions: groupPermissions,
+ // template: "",
+ // };
+ // this.initOriginalObjects();
+ this.permissions = {
+ default: UtilsNew.objectClone(this.permissions.acl[0].permissions),
+ custom: UtilsNew.objectClone(this.permissions.acl[0].permissions),
+ templates: this.templates.keys(),
+ };
+ this._permissions = UtilsNew.objectClone(this.permissions),
+ this.updatedFields = {};
+ }
+
+ initOriginalObjects() {
+ this._permissions = UtilsNew.objectClone(this.permissions);
+ this.updatedFields = {};
+ }
+
+ update(changedProperties) {
+ if ((changedProperties.has("groupId") || (changedProperties.has("studyId")) && this.active)) {
+ this.groupIdObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this._config = this.getDefaultConfig();
+ if (!this._config?.notification) {
+ // this.#initConfigNotification();
+ }
+ }
+ super.update(changedProperties);
+ }
+
+ groupIdObserver() {
+ if (this.groupId && this.studyId && this.opencgaSession) {
+ const params = {
+ member: this.groupId,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.studies()
+ .acl(this.studyId, params)
+ .then(response => {
+ this.#initPermissions(UtilsNew.objectClone(response.responses[0].results[0]));
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "groupAclInfo", this.study, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ // Uncomment to post-process data-form manipulation
+ // onFieldChange(e) {
+ // debugger
+ // this.updatedFields = e.detail?.updatedFields || {};
+ // this.requestUpdate();
+ // }
+
+ onFieldChange(e) {
+ const param = e.detail.param;
+ this.updatedFields = FormUtils.getUpdatedFields(
+ this.group,
+ this.updatedFields,
+ param,
+ e.detail.value,
+ e.detail.action);
+ if (param === "template") {
+ this._group.template = e.detail.value;
+ }
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Discard changes",
+ message: "Are you sure you want to discard the changes made?",
+ ok: () => {
+ this.initOriginalObjects();
+ this.requestUpdate();
+ // We need to dispatch a component clear event
+ LitUtils.dispatchCustomEvent(this, "groupClear", null, {
+ group: this._group,
+ });
+ },
+ });
+ }
+
+ onSubmit() {
+ const paramsAction = {
+ action: "SET"
+ };
+ const studyAclParams = {
+ study: this.studyId,
+ template: this._group.template,
+ // permissions: this._group.permissions,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.studies()
+ .updateAcl(this.groupId, paramsAction, studyAclParams)
+ .then(response => {
+ this.group = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.updatedFields = {};
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `Group Update`,
+ message: `Group ${this.group.id} updated correctly`,
+ });
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "groupUpdate", {
+ group: this.group,
+ studyFqn: this.studyFqn,
+ }, error);
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ return html `
+ this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ icon: "fas fa-edit",
+ buttons: {
+ clearText: "Discard Changes",
+ okText: "Update",
+ },
+ display: this.displayConfig,
+ sections: [
+ {
+ title: "Permissions",
+ display: {
+ titleVisible: false,
+ },
+ elements: [
+ {
+ title: "Templates",
+ field: "templates",
+ type: "toggle-buttons",
+ allowedValues: Object.keys(this.templates).map(name => name.toUpperCase()),
+ },
+ // TODO: Implement customised permissions for the group
+ /*
+ {
+ title: "Permissions",
+ field: "permissions",
+ type: "",
+ },
+ */
+ ],
+ },
+ /*
+ {
+ title: "Users",
+ elements: [
+ {
+ field: "users",
+ type: "custom",
+ display: {
+ layout: "vertical",
+ defaultLayout: "vertical",
+ width: 12,
+ style: "padding-left: 0px",
+ render: family => {
+ if (family && family.members) {
+ const individualGridConfig = {
+ showSelectCheckbox: false,
+ showToolbar: false
+ };
+ return html`
+
+
+ `;
+ }
+ },
+ },
+ }
+ ],
+ },
+ */
+ ],
+ };
+ }
+
+}
+
+customElements.define("group-admin-permissions-update", GroupAdminPermissionsUpdate);
diff --git a/src/webcomponents/organization/admin/organization-admin-audit.js b/src/webcomponents/organization/admin/organization-admin-audit.js
new file mode 100644
index 0000000000..ea55c26218
--- /dev/null
+++ b/src/webcomponents/organization/admin/organization-admin-audit.js
@@ -0,0 +1,410 @@
+/**
+ * Copyright 2015-2019 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement, nothing} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import GridCommons from "../../commons/grid-commons.js";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import {guardPage} from "../../commons/html-utils.js";
+
+export default class OrganizationAdminAudit extends LitElement {
+
+ constructor() {
+ super();
+
+ // Set status and init private properties
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ opencgaSession: {
+ type: Object
+ },
+ studyId: {
+ type: String
+ },
+ study: {
+ type: Object
+ },
+ query: {
+ type: Object
+ },
+ config: {
+ type: Object
+ }
+ };
+ }
+
+ #init() {
+ this._prefix = UtilsNew.randomString(8);
+
+ this._filters = [];
+ this.query = {};
+ this.sortedUserIds = [];
+ this.gridId = this._prefix + "AuditBrowserGrid";
+ this.actionValues = ["SEARCH", "LINK", "INFO", "CREATE"];
+ this.resourceTypeValues = ["AUDIT", "USER", "PROJECT", "STUDY", "FILE", "SAMPLE", "JOB", "INDIVIDUAL", "COHORT", "DISEASE_PANEL",
+ "FAMILY", "CLINICAL_ANALYSIS", "INTERPRETATION", "VARIANT", "ALIGNMENT", "CLINICAL", "EXPRESSION", "FUNCTIONAL"];
+ this.statusTypeValues = ["SUCCESS", "ERROR"];
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ this._config = {...this.getDefaultConfig(), ...this.config};
+ this.gridCommons = new GridCommons(this.gridId, this, this._config);
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("studyId")) {
+ for (const project of this.opencgaSession.projects) {
+ for (const study of project.studies) {
+ if (study.id === this.studyId || study.fqn === this.studyId) {
+ this.study = {...study};
+ break;
+ }
+ }
+ }
+ }
+
+ if (changedProperties.has("study")) {
+ this.studyObserver();
+ }
+ if (changedProperties.has("query")) {
+ this.propertyObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ async studyObserver() {
+ this.groupsMap = new Map();
+ try {
+ const resp = await this.opencgaSession.opencgaClient.studies()
+ .groups(this.study.fqn);
+ const groups = resp.responses[0].results;
+ if (groups[0].users) {
+ for (const group of groups) {
+ this.groupsMap.set(group.id, group.users);
+ }
+ } else {
+ for (const group of response.responses[0].results) {
+ this.groupsMap.set(group.id, group.userIds.map(u => {
+ return {id: u, name: u};
+ }));
+ }
+ }
+ this.users = this.groupsMap.get("@members");
+ this.sortedUserIds = [...this.groupsMap.get("@members").map(user => user.id).sort()];
+ // With the requestUpdate, work to get users for the filter
+ this.requestUpdate();
+ } catch (err) {
+ console.log("An error occurred fetching users: ", err);
+ }
+ this.renderRemoteTable();
+ }
+
+ propertyObserver() {
+ this.renderRemoteTable();
+ }
+
+ renderRemoteTable() {
+ if (this.opencgaSession?.opencgaClient && this.study) {
+ this.table = $("#" + this.gridId);
+ this.table.bootstrapTable("destroy");
+ this.table.bootstrapTable({
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ columns: this._getDefaultColumns(),
+ method: "get",
+ sidePagination: "server",
+ iconsPrefix: GridCommons.GRID_ICONS_PREFIX,
+ icons: GridCommons.GRID_ICONS,
+ uniqueId: "id",
+ // Table properties
+ pagination: this._config.pagination,
+ pageSize: this._config.pageSize,
+ pageList: this._config.pageList,
+ // paginationVAlign: "both",
+ formatShowingRows: this.gridCommons.formatShowingRows,
+ showExport: this._config.showExport,
+ detailView: this._config.detailView,
+ detailFormatter: this.detailFormatter,
+ gridContext: this,
+ // formatLoadingMessage: () => "
",
+ loadingTemplate: () => GridCommons.loadingFormatter(),
+ ajax: params => {
+ const query = {
+ study: this.study.fqn,
+ limit: params.data.limit,
+ skip: params.data.offset || 0,
+ count: !this.table.bootstrapTable("getOptions").pageNumber || this.table.bootstrapTable("getOptions").pageNumber === 1,
+ ...this.query
+ };
+ // Store the current filters
+ // this.lastFilters = {..._filters};
+ this.opencgaSession.opencgaClient.studies().searchAudit(this.study.fqn, query)
+ .then(res => {
+ params.success(res);
+ })
+ .catch(e => {
+ console.error(e);
+ params.error(e);
+ });
+ },
+ responseHandler: response => {
+ const result = this.gridCommons.responseHandler(response, $(this.table).bootstrapTable("getOptions"));
+ return result.response;
+ },
+ onClickRow: (row, selectedElement, field) => this.gridCommons.onClickRow(row.id, row, selectedElement),
+ onDblClickRow: (row, element, field) => {
+ // We detail view is active we expand the row automatically.
+ // FIXME: Note that we use a CSS class way of knowing if the row is expand or collapse, this is not ideal but works.
+ if (this._config.detailView) {
+ if (element[0].innerHTML.includes("fa-plus")) {
+ this.table.bootstrapTable("expandRow", element[0].dataset.index);
+ } else {
+ this.table.bootstrapTable("collapseRow", element[0].dataset.index);
+ }
+ }
+ },
+ onLoadSuccess: data => {
+ this.gridCommons.onLoadSuccess(data, 1);
+ },
+ onLoadError: (e, restResponse) => this.gridCommons.onLoadError(e, restResponse),
+ onPostBody: data => {
+ // Add tooltips?
+ }
+ });
+ }
+ }
+
+ detailFormatter(index, row) {
+ return `
+
+
Action Params
+
${JSON.stringify(row.params, null, 2)}
+
+ `;
+ }
+
+ _getDefaultColumns() {
+ return [
+ {
+ title: "Audit Record ID",
+ field: "id",
+ },
+ {
+ title: "User ID",
+ field: "userId",
+ },
+ {
+ title: "Study ID",
+ field: "studyId",
+ },
+ {
+ title: "Action",
+ field: "action"
+ },
+ {
+ title: "Resource Type",
+ field: "resource"
+ },
+ {
+ title: "Resource ID",
+ field: "resourceId",
+ },
+ {
+ title: "Date",
+ field: "date",
+ formatter: value => value ? UtilsNew.dateFormatter(UtilsNew.getDatetime(value)) : "NA"
+ },
+ {
+ title: "Status",
+ field: "status.name",
+ },
+ ];
+ }
+
+ onFilterChange(key, value) {
+ if (value && value !== "") {
+ this.query = {...this.query, ...{[key]: value}};
+ } else {
+ delete this.query[key];
+ this.query = {...this.query};
+ }
+ }
+
+ clear(e) {
+ this.query = {};
+ }
+
+ render() {
+ if (!OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id)) {
+ return guardPage("No permission to view this page");
+ }
+
+ return html`
+
+
+
+ ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "userId") ? html`
+
+
+ this.onFilterChange("userId", e.detail.value)}">
+
+
+ `: nothing}
+
+ ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "action") ? html`
+
+
+ this.onFilterChange("action", e.detail.value)}">
+
+
+ ` : nothing}
+
+ ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "resource") ? html`
+
+
+ this.onFilterChange("resource", e.detail.value)}">
+
+
+ ` : nothing}
+
+ ${~this._config.filter.sections[0].filters.findIndex(field => field.id === "status") ? html`
+
+
+ this.onFilterChange("status", e.detail.value)}">
+
+
+ ` : nothing}
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ filter: {
+ sections: [
+ {
+ title: "",
+ filters: [
+ {id: "userId"},
+ {id: "resource"},
+ {id: "action"},
+ {id: "status"},
+ ]
+ }
+ ],
+ },
+ pagination: true,
+ pageSize: 10,
+ pageList: [10, 25, 50],
+ showExport: false,
+ detailView: true,
+ multiSelection: false,
+ showSelectCheckbox: true,
+ showToolbar: true,
+ showActions: true,
+ };
+ }
+
+
+}
+
+customElements.define("organization-admin-audit", OrganizationAdminAudit);
diff --git a/src/webcomponents/organization/admin/organization-admin-detail.js b/src/webcomponents/organization/admin/organization-admin-detail.js
new file mode 100644
index 0000000000..9e7a9fe9e9
--- /dev/null
+++ b/src/webcomponents/organization/admin/organization-admin-detail.js
@@ -0,0 +1,293 @@
+/**
+ * Copyright 2015-2024 OpenCB *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {LitElement, html, nothing} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import ModalUtils from "../../commons/modal/modal-utils.js";
+import "./organization-admin-update.js";
+
+export default class OrganizationAdminDetail extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "organization-admin-detail";
+ this._prefix = UtilsNew.randomString(8);
+ this.gridId = this._prefix + this.COMPONENT_ID;
+
+ this.updatedFields = {};
+ this.isLoading = false;
+
+ this.displayConfigDefault = {
+ buttonsVisible: false,
+ collapsable: true,
+ titleVisible: false,
+ titleWidth: 2,
+ defaultValue: "-",
+ pdf: false,
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("opencgaSession") ||
+ changedProperties.has("organization") ||
+ changedProperties.has("displayConfig")) {
+ this.propertyObserver();
+ }
+
+ super.update(changedProperties);
+ }
+
+ propertyObserver() {
+ // With each property change we must be updated config and create the columns again. No extra checks are needed.
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig
+ };
+
+ this.modals = {
+ /*
+ "change-owner": {
+ label: "Change Owner",
+ icon: "fas fa-user-shield",
+ modalId: `${this._prefix}AddAdminOrganizationModal`,
+ render: () => this.renderChangeOwnerOrganization(),
+ permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled",
+ },
+ */
+ "organization-update": {
+ label: "Edit Organization (coming soon...)",
+ icon: "far fa-edit",
+ modalId: `${this._prefix}UpdateOrganizationModal`,
+ render: () => this.renderOrganizationUpdate(),
+ permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled",
+ },
+ };
+ }
+
+ // *** EVENTS ***
+ async onActionClick(e) {
+ this.action = e.currentTarget.dataset.action;
+ this.requestUpdate();
+ await this.updateComplete;
+ ModalUtils.show(this.modals[this.action]["modalId"]);
+ }
+
+ // *** RENDER ***
+ /*
+ renderChangeOwnerOrganization() {
+ return ModalUtils.create(this, `${this._prefix}UpdateOrganizationModal`, {
+ display: {
+ modalTitle: `Update Organization: ${this.organization.id}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-organization-owner-update",
+ modalSize: "modal-lg"
+ },
+ render: () => {
+ return html`
+
+
+ `;
+ },
+ });
+ }
+ */
+
+ renderOrganizationUpdate() {
+ return ModalUtils.create(this, `${this._prefix}UpdateOrganizationModal`, {
+ display: {
+ modalTitle: `Update Organization: ${this.organization.id}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-organization-update",
+ modalSize: "modal-lg"
+ },
+ render: () => {
+ return html`
+
+
+ `;
+ },
+ });
+ }
+
+ renderOrganizationToolbar() {
+ return html `
+
+ ${
+ Object.keys(this.modals).map(modalKey => {
+ const modal = this.modals[modalKey];
+ const color = modal.permission !== "disabled" ? modal.color : "";
+ return html`
+
+
+ `;
+ })
+ }
+
+ `;
+ }
+
+ render() {
+ if (this.organization) {
+ return html`
+
+ ${this.renderOrganizationToolbar()}
+
+
+
+
+ ${this.action ? this.modals[this.action]["render"](): nothing}
+ `;
+ }
+ }
+
+ // *** CONFIG ***
+ getDefaultConfig() {
+ return {
+ title: "Organization",
+ type: "tabs",
+ icon: "",
+ display: this.displayConfig || this.displayConfigDefault,
+ sections: [
+ {
+ title: "Organization View",
+ elements: [
+ {
+ title: "Organization ID",
+ type: "complex",
+ display: {
+ template: "${id} (UUID: ${uuid})",
+ style: {
+ id: {
+ "font-weight": "bold",
+ }
+ },
+ },
+ },
+ {
+ title: "Organization Name",
+ field: "name"
+ },
+ {
+ title: "Owner",
+ field: "name"
+ },
+ {
+ title: "Admins",
+ field: "admins",
+ type: "list",
+ display: {
+ defaultValue: "The organization does not have admins yet.",
+ contentLayout: "bullets",
+ },
+ },
+ {
+ title: "Creation Date",
+ field: "creationDate",
+ display: {
+ format: date => UtilsNew.dateFormatter(date),
+ },
+ },
+ {
+ title: "Modification Date",
+ field: "modificationDate",
+ display: {
+ format: date => UtilsNew.dateFormatter(date),
+ },
+ },
+ ],
+ },
+ {
+ title: "Authentication Origins",
+ elements: [
+ {
+ title: "Authentication origins",
+ field: "configuration.authenticationOrigins",
+ type: "table",
+ display: {
+ columns: [
+ {
+ id: "id",
+ title: "ID",
+ field: "id",
+ formatter: () => {},
+ },
+ {
+ id: "host",
+ title: "Host",
+ field: "host",
+ formatter: () => {},
+ },
+ {
+ id: "type",
+ title: "Type",
+ field: "type",
+ formatter: () => {},
+ },
+ ],
+ },
+ },
+ ]
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("organization-admin-detail", OrganizationAdminDetail);
diff --git a/src/webcomponents/organization/admin/organization-admin-update.js b/src/webcomponents/organization/admin/organization-admin-update.js
new file mode 100644
index 0000000000..efc036d4c5
--- /dev/null
+++ b/src/webcomponents/organization/admin/organization-admin-update.js
@@ -0,0 +1,340 @@
+/**
+ * Copyright 2015-2024 OpenCB *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {LitElement, html} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import FormUtils from "../../commons/forms/form-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+
+export default class OrganizationAdminUpdate extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this._organization = {};
+
+ this.updatedFields = {};
+ this.isLoading = false;
+ this.displayConfig = {};
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ defaultLayout: "horizontal",
+ labelAlign: "right",
+ buttonOkText: "Update",
+ buttonOkDisabled: true,
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #initConfigNotification() {
+ this._config.notification = {
+ title: "",
+ text: "Some changes have been done in the form. Not saved changes will be lost",
+ type: "notification",
+ display: {
+ visible: () => {
+ return UtilsNew.isNotEmpty(this.updatedFields);
+ },
+ notificationType: "warning",
+ },
+ };
+ }
+
+ #initOriginalObjects() {
+ this._organization = UtilsNew.objectClone(this.organization);
+ // const {configuration, ...rest} = UtilsNew.objectClone(this.organization);
+ // this._organization = {
+ // ...UtilsNew.objectClone(configuration),
+ // // token: {
+ // // algorithm: "",
+ // // secretKey: "",
+ // // expiration: 0
+ // // },
+ // // defaultUserExpirationDate: "",
+ // };
+ this.updatedFields = {};
+ this.displayConfigObserver();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("opencgaSession") ||
+ changedProperties.has("organization")) {
+ this.#initOriginalObjects();
+ }
+
+ if (changedProperties.has("displayConfig")) {
+ this.displayConfigObserver();
+ }
+
+ super.update(changedProperties);
+ }
+
+ displayConfigObserver() {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig,
+ };
+ this._config = this.getDefaultConfig();
+ if (!this._config?.notification) {
+ this.#initConfigNotification();
+ }
+ }
+
+ onFieldChange(e, field) {
+ const param = field || e.detail.param;
+ this.updatedFields = FormUtils.getUpdatedFields(
+ this.organization,
+ this.updatedFields,
+ param,
+ e.detail.value,
+ e.detail.action);
+
+ this._config.display.buttonOkDisabled = UtilsNew.isEmpty(this.updatedFields);
+ this._config = {...this._config};
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Discard changes",
+ message: "Are you sure you want to discard the changes made?",
+ ok: () => {
+ this.#initOriginalObjects();
+ // We need to dispatch a component clear event
+ LitUtils.dispatchCustomEvent(this, "organizationClear", null, {
+ organization: this._organization,
+ });
+ },
+ });
+ }
+
+ onSubmit() {
+ const params = {
+ includeResult: true,
+ authenticationOriginsAction: "SET",
+ };
+
+ let updateParams = FormUtils.getUpdateParams(this._organization, this.updatedFields, this.updateCustomisation);
+ const {configuration, ...rest} = UtilsNew.objectClone(updateParams);
+ delete updateParams.configuration;
+ updateParams = {
+ ...configuration,
+ ...updateParams,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.organization()
+ .updateConfiguration(this.organization.id, updateParams, params)
+ .then(() => {
+ this._config = this.getDefaultConfig();
+ this.updatedFields = {};
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `Organization Configuration Update`,
+ message: `Organization ${this.organization.id} updated correctly`,
+ });
+ LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", this._organization, {}, error);
+ LitUtils.dispatchCustomEvent(this, "organizationUpdate", this._organization, {}, error);
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ return html`
+ this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ // *** CONFIG ***
+ getDefaultConfig() {
+ return {
+ display: this.displayConfig,
+ sections: [
+ // CAUTION 20240731 Vero: The update of this datapoint is not included in the endpoint organization().updateConfiuration().
+ // Since currently the interface is just displaying a button for updating the configuration,
+ // and for consistency conceived to use a single endpoint per modal, it needs to be discussed with CTO or line-manager
+ // where to place this endpoint it in IVA (if needed).
+ /*
+ {
+ title: "General Information",
+ elements: [
+ {
+ title: "Organization Name",
+ field: "name",
+ type: "input-text",
+ },
+ ],
+ },
+ */
+ {
+ title: "Token",
+ elements: [
+ {
+ title: "Token",
+ field: "token",
+ type: "object",
+ elements: [
+ {
+ title: "Algorithm",
+ field: "token.algorithm",
+ type: "input-text",
+ display: {
+ placeholder: "Change the algorithm...",
+ helpMessage: "The default algorithm is HS256.",
+ },
+ },
+ {
+ title: "Secret Key",
+ field: "token.secretKey",
+ type: "input-text",
+ display: {
+ placeholder: "Change the secret key...",
+ helpMessage: "",
+ },
+ },
+ {
+ title: "Expiration",
+ field: "token.expiration",
+ type: "input-num",
+ allowedValues: [0],
+ display: {
+ placeholder: "Change the expiration time...",
+ helpMessage: "The expiration time is configured in seconds. The default expiration time is 3600s.",
+ },
+ },
+ ],
+ }
+ ],
+ },
+ {
+ title: "Configurations",
+ elements: [
+ {
+ title: "Optimizations",
+ field: "configuration.optimizations",
+ type: "object",
+ elements: [
+ {
+ title: "Simplify Permissions",
+ field: "configuration.optimizations.simplifyPermissions",
+ type: "checkbox",
+ },
+ ],
+ },
+ {
+ title: "Default User Expiration Date",
+ field: "defaultUserExpirationDate",
+ type: "input-date",
+ display: {
+ placeholder: "Change the default user expiration date"
+ },
+ },
+ {
+ title: "Authentication Origins",
+ field: "configuration.authenticationOrigins",
+ type: "object-list",
+ display: {
+ style: "border-left: 2px solid #0c2f4c; padding-left: 12px; margin-bottom:24px",
+ collapsedUpdate: true,
+ showAddItemListButton: true,
+ showAddBatchListButton: true,
+ showResetListButton: true,
+ view: data => html`
+ ${data?.id} - ${data?.host}
+ `,
+ },
+ elements: [
+ {
+ title: "ID",
+ field: "configuration.authenticationOrigins[].id",
+ type: "input-text",
+ display: {
+ placeholder: "Add an ID...",
+ },
+ },
+ {
+ title: "Type",
+ field: "configuration.authenticationOrigins[].type",
+ type: "select",
+ allowedValues: ["OPENCGA", "LDAP", "AzureAD", "SSO"],
+ display: {
+ placeholder: "Select a type...",
+ },
+ },
+ {
+ title: "Host",
+ field: "configuration.authenticationOrigins[].host",
+ type: "input-text",
+ display: {
+ placeholder: "Add a Host...",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("organization-admin-update", OrganizationAdminUpdate);
diff --git a/src/webcomponents/organization/admin/organization-admin.js b/src/webcomponents/organization/admin/organization-admin.js
new file mode 100644
index 0000000000..6de0b4f0a5
--- /dev/null
+++ b/src/webcomponents/organization/admin/organization-admin.js
@@ -0,0 +1,198 @@
+/**
+ * Copyright 2015-2024 OpenCB *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {LitElement, html} from "lit";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import "./group-admin-browser.js";
+import "./user-admin-browser.js";
+import "../../project/projects-admin.js";
+import "./project-admin-browser.js";
+import "./organization-admin-detail.js";
+
+export default class OrganizationAdmin extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ opencgaSession: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this._config = this.getDefaultConfig();
+ this._activeMenuItem = "";
+ }
+
+ // --- RENDER METHOD ---
+ render() {
+ if (this.opencgaSession?.organization) {
+ if (!OpencgaCatalogUtils.isOrganizationAdmin(this.opencgaSession.organization, this.opencgaSession.user.id)) {
+ return html `
+
+
Restricted access
+ The page you are trying to access has restricted access.
+ Please refer to your system administrator.
+
+ `;
+ }
+ return html `
+
+
+
+ `;
+ }
+ }
+
+ getDefaultConfig() {
+ const menu = [
+ {
+ id: "general",
+ name: "General",
+ description: "",
+ icon: "",
+ featured: "",
+ visibility: "private",
+ submenu: [
+ // TODO
+ {
+ id: "dashboard",
+ name: "Dashboard (Coming soon)",
+ icon: "fas fa-vial",
+ visibility: "private",
+ render: () => html``,
+ },
+ // TODO
+ {
+ id: "audit",
+ name: "Audit (Coming soon)",
+ type: "category",
+ icon: "fas fa-vial",
+ visibility: "private",
+ render: () => html``,
+ },
+ ],
+ },
+ {
+ id: "manage",
+ name: "Manage",
+ description: "",
+ icon: "",
+ featured: "", // true | false
+ visibility: "private",
+ submenu: [
+ /* Vero Note: Maintained for future use in Organization Admin
+ {
+ id: "groups",
+ name: "Groups",
+ icon: "fas fa-vial",
+ visibility: "private",
+ render: (opencgaSession, organization) => html`
+
+
+ `,
+ },
+ */
+ {
+ id: "users",
+ name: "Users",
+ icon: "fas fa-users",
+ visibility: "private",
+ render: (opencgaSession, organization) => html`
+
+
+ `,
+ },
+ {
+ id: "studies",
+ name: "Projects/Studies",
+ icon: "fas fa-project-diagram",
+ visibility: "private",
+ render: (opencgaSession, organization) => {
+ return html`
+
+
+ `;
+ },
+ },
+ ],
+ },
+ {
+ id: "configure",
+ name: "Configure",
+ description: "",
+ icon: "",
+ featured: "",
+ visibility: "private",
+ submenu: [
+ {
+ id: "settings",
+ name: "Organization",
+ icon: "fas fa-sitemap",
+ visibility: "private",
+ render: (opencgaSession, organization) => {
+ return html`
+
+
+ `;
+ },
+ },
+ /*
+ {
+ id: "optimization",
+ name: "Optimizations",
+ icon: "fas fa-vial",
+ visibility: "private",
+ render: (opencgaSession, study) => html``,
+ },
+ */
+ ],
+ },
+ ];
+
+ return {
+ name: "Organization Admin",
+ logo: "",
+ icon: "",
+ visibility: "",
+ menu: menu,
+ };
+ }
+
+}
+
+customElements.define("organization-admin", OrganizationAdmin);
diff --git a/src/webcomponents/organization/admin/project-admin-browser.js b/src/webcomponents/organization/admin/project-admin-browser.js
new file mode 100644
index 0000000000..abcc654c48
--- /dev/null
+++ b/src/webcomponents/organization/admin/project-admin-browser.js
@@ -0,0 +1,258 @@
+/**
+ * Copyright 2015-2024 OpenCB *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {LitElement, html, nothing} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import ModalUtils from "../../commons/modal/modal-utils.js";
+import "../../project/project-create.js";
+import "../../project/project-update.js";
+import "./study-admin-grid.js";
+
+export default class ProjectAdminBrowser extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ config: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "project-admin-browser";
+ this._prefix = UtilsNew.randomString(8);
+ this.gridId = this._prefix + this.COMPONENT_ID;
+ this.projects = [];
+ this._config = this.getDefaultConfig();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("opencgaSession") ||
+ changedProperties.has("organization") ||
+ changedProperties.has("config")) {
+ this.propertyObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ propertyObserver() {
+ // With each property change we must be updated config and create the columns again. No extra checks are needed.
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.config,
+ };
+
+ // Config for the grid toolbar
+ this.toolbarSetting = {
+ ...this._config,
+ };
+
+ this.toolbarConfig = {
+ toolId: this.toolId,
+ resource: "PROJECT",
+ create: {
+ display: {
+ modalTitle: "Project Create",
+ modalDraggable: true,
+ modalCyDataName: "modal-create",
+ modalSize: "modal-lg"
+ // disabled: true,
+ // disabledTooltip: "...",
+ },
+ modalId: `${this._prefix}CreateProjectModal`,
+ render: () => html `
+ this.onProjectCreate(e)}">
+ `
+ },
+ };
+
+ this.modals = {
+ "project-update": {
+ label: "Edit Project",
+ icon: "fas fa-edit",
+ modalId: `${this._prefix}UpdateProjectModal`,
+ render: () => this.renderProjectUpdate(),
+ permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled",
+ },
+ };
+ }
+
+ // *** EVENTS ***
+ async onActionClick(e, project) {
+ this.action = e.currentTarget.dataset.action;
+ this.projectId = project.id;
+ this.requestUpdate();
+ await this.updateComplete;
+ ModalUtils.show(this.modals[this.action]["modalId"]);
+ }
+
+ onProjectCreate() {
+ // Close modal
+ ModalUtils.close(this.toolbarConfig.create.modalId);
+ }
+
+ // *** RENDER ***
+ renderProjectUpdate() {
+ return ModalUtils.create(this, `${this._prefix}UpdateProjectModal`, {
+ display: {
+ modalTitle: `Update Project: Project ${this.projectId} in organization ${this.organization.id}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-project-update",
+ modalSize: "modal-lg"
+ },
+ // @projectUpdate="${e => this.onProjectUpdate(e, `${this._prefix}UpdateDetailsModal`)}"
+ render: () => {
+ return html`
+
+
+ `;
+ },
+ });
+ }
+
+ renderProjectsToolbar() {
+ if (this._config.showToolbar) {
+ return html `
+
+
+ `;
+ }
+ }
+
+ renderProject(project) {
+ return html `
+
+
+
+
+
+
+
+
+ ${project.name || project.id}
+
+
+ [ ${project.fqn} ]
+
+
+
+
+ ${
+ Object.keys(this.modals).map(modalKey => {
+ const modal = this.modals[modalKey];
+ return html`
+ this.onActionClick(e, project)}">
+ ${modal.label}...
+
+ `;
+ })
+ }
+
+
+
+
+
+ ${project.organism?.scientificName.toUpperCase() || "-"} (${project.organism?.assembly || "-"})
+
+
+ Cellbase: ${project.cellbase?.version || "-"}
+
+
+ Data Release: ${project.cellbase?.dataRelease || "-"}
+
+
+
+
+
+ ${project.description}
+
+
+
+
+
+
+
+
+ ${this.action ? this.modals[this.action]["render"](): nothing}
+
+ `;
+ }
+
+ render() {
+ return html`
+
+ ${this.renderProjectsToolbar()}
+
+ ${this.organization.projects.map(project => this.renderProject(project))}
+ `;
+ }
+
+ // *** CONFIG ***
+ getDefaultConfig() {
+ return {
+ showToolbar: true,
+ showExport: false,
+ showSettings: false,
+ showCreate: true,
+ buttonCreateText: "New Project...",
+ showGraphicFilters: false,
+ showProjectToolbar: true,
+ };
+ }
+
+}
+
+customElements.define("project-admin-browser", ProjectAdminBrowser);
diff --git a/src/webcomponents/organization/admin/study-admin-grid.js b/src/webcomponents/organization/admin/study-admin-grid.js
new file mode 100644
index 0000000000..51a53f4e5d
--- /dev/null
+++ b/src/webcomponents/organization/admin/study-admin-grid.js
@@ -0,0 +1,476 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import GridCommons from "../../commons/grid-commons.js";
+import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import ModalUtils from "../../commons/modal/modal-utils.js";
+import UtilsNew from "../../../core/utils-new.js";
+
+import "../../study/admin/study-create.js";
+import "../../study/admin/study-update.js";
+import "./study-users-manage.js";
+
+export default class StudyAdminGrid extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ toolId: {
+ type: String,
+ },
+ project: {
+ type: Object,
+ },
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ active: {
+ type: Boolean
+ },
+ config: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "study-grid";
+ this._prefix = UtilsNew.randomString(8);
+ this.gridId = this._prefix + this.COMPONENT_ID;
+ this.active = true;
+ this._config = this.getDefaultConfig();
+ this._action = "";
+ this.displayConfigDefault = {
+ header: {
+ horizontalAlign: "center",
+ verticalAlign: "bottom",
+ },
+ };
+ }
+
+ // --- LIFE-CYCLE METHODS
+ update(changedProperties) {
+ if (changedProperties.has("opencgaSession") ||
+ changedProperties.has("toolId") ||
+ changedProperties.has("project") ||
+ changedProperties.has("config")) {
+ this.propertyObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.size > 0 && this.active) {
+ this.renderRemoteTable();
+ }
+ }
+
+ propertyObserver() {
+ // With each property change we must be updated config and create the columns again. No extra checks are needed.
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.config,
+ };
+
+ this.gridCommons = new GridCommons(this.gridId, this, this._config);
+
+ // Config for the grid toolbar
+ this.toolbarSetting = {
+ ...this._config,
+ };
+
+ this.toolbarConfig = {
+ toolId: this.toolId,
+ resource: "STUDY",
+ columns: this._getDefaultColumns(),
+ create: {
+ display: {
+ modalTitle: "Study Create",
+ modalDraggable: true,
+ modalCyDataName: "modal-study-create",
+ modalSize: "modal-lg"
+ },
+ modalId: `${this._prefix}CreateStudyModal`,
+ render: () => html `
+ this.onStudyCreate()}">
+
+ `,
+ },
+ };
+
+ this.permissions = {
+ "organization": () => OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) ? "" : "disabled",
+ "study": () => OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) ? "" : "disabled",
+ };
+
+ this.modals = {
+ "edit-study": {
+ label: "Edit Study",
+ icon: "fas fa-edit",
+ modalId: `${this._prefix}UpdateStudyModal`,
+ render: () => this.renderStudyUpdate(),
+ permission: this.permissions["organization"](),
+ divider: true,
+ },
+ "create-group": {
+ label: "Create Group",
+ icon: "fas fa-edit",
+ modalId: `${this._prefix}CreateGroupModal`,
+ render: () => this.renderGroupCreate(),
+ permission: this.permissions["organization"](),
+ divider: false,
+ },
+ "manage-users": {
+ label: "Manage Organization Users in Study",
+ icon: "fas fa-user-plus",
+ modalId: `${this._prefix}ManageUsersStudyModal`,
+ render: () => this.renderManageUsersStudy(),
+ permission: this.permissions["organization"](),
+ divider: true,
+ },
+ "delete": {
+ label: "Delete Study",
+ icon: "fas fa-trash-alt ",
+ // color: "text-danger",
+ // modalId: `${this._prefix}DeleteModal`,
+ // render: () => this.renderModalPasswordReset(),
+ permission: "disabled", // Caution: Not possible to delete studies for now.
+ },
+ };
+ }
+
+ // *** PRIVATE METHODS ***
+ renderRemoteTable() {
+ if (this.opencgaSession?.opencgaClient && this.project.id) {
+ this._columns = this._getDefaultColumns();
+ this.table = $("#" + this.gridId);
+ this.table.bootstrapTable("destroy");
+ this.table.bootstrapTable({
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ columns: this._columns,
+ method: "get",
+ sidePagination: "server",
+ iconsPrefix: GridCommons.GRID_ICONS_PREFIX,
+ icons: GridCommons.GRID_ICONS,
+ uniqueId: "id",
+ // Table properties
+ pagination: this._config.pagination,
+ pageSize: this._config.pageSize,
+ pageList: this._config.pageList,
+ detailView: !!this.detailFormatter,
+ loadingTemplate: () => GridCommons.loadingFormatter(),
+ ajax: params => {
+ let result = null;
+ this.filters = {
+ limit: params.data.limit,
+ skip: params.data.offset || 0,
+ count: !this.table.bootstrapTable("getOptions").pageNumber || this.table.bootstrapTable("getOptions").pageNumber === 1,
+ };
+
+ // Store the current filters
+ this.opencgaSession.opencgaClient.projects()
+ .studies(this.project.id, this.filters)
+ .then(response => {
+ result = response;
+ return response;
+ })
+ .then(() => {
+ // Prepare data for columns extensions
+ const rows = result.responses?.[0]?.results || [];
+ return this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, this.filters, rows);
+ })
+ .then(() => params.success(result))
+ .catch(error => {
+ console.error(error);
+ params.error(error);
+ });
+ },
+ responseHandler: response => {
+ const result = this.gridCommons.responseHandler(response, $(this.table).bootstrapTable("getOptions"));
+ return result.response;
+ },
+ onClickRow: (row, selectedElement) => this.gridCommons.onClickRow(row.id, row, selectedElement),
+ onLoadSuccess: () => UtilsNew.initTooltip(this),
+ onLoadError: (e, restResponse) => this.gridCommons.onLoadError(e, restResponse),
+ });
+ }
+ }
+
+ _getDefaultColumns() {
+ this._columns = [
+ {
+ title: "Study ID",
+ field: "id",
+ visible: this.gridCommons.isColumnVisible("id")
+ },
+ {
+ title: "Study Fqn",
+ field: "fqn",
+ visible: this.gridCommons.isColumnVisible("fqn")
+ },
+ {
+ title: "Name",
+ field: "name",
+ visible: this.gridCommons.isColumnVisible("name")
+ },
+ {
+ title: "Groups",
+ field: "groups",
+ formatter: (groups, row) => this.groupsFormatter(groups, row),
+ visible: this.gridCommons.isColumnVisible("modificationDate")
+ },
+ {
+ title: "Modification / Creation Dates",
+ field: "dates",
+ halign: this.displayConfigDefault.header.horizontalAlign,
+ valign: "middle",
+ formatter: (value, row) => this.datesFormatter(value, row),
+ },
+ ];
+
+ if (this._config.annotations?.length > 0) {
+ this.gridCommons.addColumnsFromAnnotations(this._columns, CatalogGridFormatter.customAnnotationFormatter, this._config);
+ }
+
+ if (this.opencgaSession && this._config.showActions) {
+ this._columns.push({
+ id: "actions",
+ title: "Actions",
+ field: "actions",
+ align: "center",
+ formatter: () => `
+
+
+
+ Actions
+
+
+
+ `,
+ events: {
+ "click ul>li>a": (e, value, row) => this.onActionClick(e, value, row),
+ },
+ });
+ }
+
+ this._columns = this.gridCommons.addColumnsFromExtensions(this._columns, this.COMPONENT_ID);
+ return this._columns;
+ }
+
+ // *** FORMATTERS ***
+ groupsFormatter(groups) {
+ const groupsBadges = groups.map(group => `
+
+
${group.id} [${group.userIds.length}]
+
${group.userIds.join(", ")}
+
+ `);
+
+ const maxShow = 3;
+ const badgesShow = groupsBadges.splice(0, maxShow);
+ return `
+
+ ${badgesShow.join("")}
+ ${groupsBadges.length > 0 ? `
+
+ ... View all groups (${groupsBadges.length})
+
+ ` : ""}
+
+ `;
+ }
+
+ datesFormatter(value, study) {
+ return `
+ ${CatalogGridFormatter.dateFormatter(study.modificationDate, study)}
+ ${CatalogGridFormatter.dateFormatter(study.creationDate, study)}
+ `;
+ }
+
+ // *** EVENTS ***
+ async onActionClick(e, value, row) {
+ this._action = e.currentTarget.dataset.action;
+ this.studyId = row.id;
+ this.studyFqn = row.fqn;
+ if (this._action === "manage-users") {
+ // Manage organization users: (a) add/remove from study, (b) set/unset as study admins
+ this.groups = row.groups.filter(group => ["@members", "@admins"].includes(group.id));
+ }
+ this.requestUpdate();
+ await this.updateComplete;
+ ModalUtils.show(this.modals[this._action]["modalId"]);
+ }
+
+ onStudyEvent(e, id) {
+ this._action = "";
+ ModalUtils.close(id);
+ }
+
+ onStudyCreate() {
+ // Close modal
+ ModalUtils.close(this.toolbarConfig.create.modalId);
+ }
+
+ // *** RENDER METHODS ***
+ renderGroupCreate() {
+ return ModalUtils.create(this, `${this._prefix}CreateGroupModal`, {
+ display: {
+ modalTitle: `Group Create in Study: ${this.studyId}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-group-create",
+ modalSize: "modal-lg"
+ },
+ render: () => html`
+ this.onStudyEvent(e, `${this._prefix}CreateGroupModal`)}">
+
+ `,
+ });
+ }
+
+ renderStudyUpdate() {
+ return ModalUtils.create(this, `${this._prefix}UpdateStudyModal`, {
+ display: {
+ modalTitle: `Update Study: ${this.studyId}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-study-update",
+ modalSize: "modal-lg"
+ },
+ render: () => html`
+ this.onStudyEvent(e, `${this._prefix}UpdateStudyModal`)}">
+
+ `,
+ });
+ }
+
+ renderManageUsersStudy() {
+ return ModalUtils.create(this, `${this._prefix}ManageUsersStudyModal`, {
+ display: {
+ modalTitle: `Manage Organization Users in Study: ${this.studyId}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-users-study-update",
+ modalSize: "modal-lg"
+ },
+ render: () => {
+ return html`
+ this.onStudyEvent(e, `${this._prefix}ManageUsersStudyModal`)}">
+
+ `;
+ }
+ });
+ }
+
+ renderToolbar() {
+ if (this._config.showToolbar) {
+ return html `
+
+
+ `;
+ }
+ }
+
+ render() {
+ return html`
+
+ ${this.renderToolbar()}
+
+
+
+ ${this._action ? this.modals[this._action]["render"](): nothing}
+ `;
+ }
+
+ // *** DEFAULT CONFIG ***
+ getDefaultConfig() {
+ return {
+ // Settings
+ pagination: true,
+ pageSize: 10,
+ pageList: [5, 10, 25],
+ pageInfoShort: true,
+ multiSelection: false,
+ showSelectCheckbox: false,
+
+ showToolbar: true,
+ showActions: true,
+
+ buttonCreateText: "New Study...",
+ showCreate: true,
+ showExport: false,
+ showSettings: false,
+ exportTabs: ["download", "link", "code"],
+ };
+ }
+
+}
+
+customElements.define("study-admin-grid", StudyAdminGrid);
diff --git a/src/webcomponents/organization/admin/study-users-manage.js b/src/webcomponents/organization/admin/study-users-manage.js
new file mode 100644
index 0000000000..8492a50490
--- /dev/null
+++ b/src/webcomponents/organization/admin/study-users-manage.js
@@ -0,0 +1,379 @@
+/**
+ * Copyright 2015-2023 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement, nothing} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import FormUtils from "../../commons/forms/form-utils.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+
+export default class StudyUsersManage extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ studyFqn: {
+ type: String,
+ },
+ groups: {
+ type: Array,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object,
+ },
+ };
+ }
+
+ #init() {
+ this.updateParams = {};
+ this.isLoading = false;
+ this.displayConfig = {};
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ defaultLayout: "horizontal",
+ labelAlign: "right",
+ labelWidth: 3,
+ buttonOkText: "Update",
+ };
+ }
+
+ #initOriginalObjects() {
+ // 1. Data in data-form
+ this.userRole = {
+ "org-owner": {
+ displayName: "OWNER",
+ check: userId => this.opencgaSession.organization.owner === userId,
+ },
+ "org-admin": {
+ displayName: "ADMIN",
+ check: userId => this.opencgaSession.organization.admins.includes(userId),
+ },
+ "study-admin": {
+ displayName: "STUDY ADMIN",
+ check: userId => this.groups.find(group => group.id === "@admins").userIds.includes(userId),
+ },
+ };
+ // Original object
+ this.component = {
+ selectedGroups: this.groups.map(group => group.id).join(",") || "",
+ selectedUsers: this.users?.map(user => user.id) || [],
+ };
+ // Modified object
+ this._component = UtilsNew.objectClone(this.component);
+
+ // 2. Query variables
+ this._userGroupUpdates = [];
+
+ // 3. Display
+ this.forceDisable = [];
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ // --- LIT LIFE CYCLE
+ update(changedProperties) {
+ if (changedProperties.has("studyFqn") ||
+ changedProperties.has("groups") ||
+ changedProperties.has("opencgaSession")) {
+ this.propertyObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig,
+ };
+ }
+ super.update(changedProperties);
+ }
+
+ // --- OBSERVERS ---
+ async propertyObserver() {
+ if (this.opencgaSession?.organization?.id && this.groups && this.studyFqn) {
+ const filters = {
+ organization: this.opencgaSession.organization.id,
+ include: "id",
+ count: true,
+ limit: 1,
+ };
+ this.#setLoading(true);
+ try {
+ const responseNoUsers = await this.opencgaSession.opencgaClient.users().search(filters);
+ const noUsers = responseNoUsers.responses[0].numTotalResults;
+ if (noUsers > 0) {
+ filters.limit = noUsers;
+ const responseUsers = await this.opencgaSession.opencgaClient.users().search(filters);
+ this.users = UtilsNew.objectClone(responseUsers.responses[0].result);
+ this.#initOriginalObjects();
+ }
+ } catch (error) {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
+ }
+ this.#setLoading(false);
+ }
+ }
+
+ // --- EVENTS ---
+ onFieldChange(e) {
+ const param = e.detail.param;
+ this.updatedFields = FormUtils.getUpdatedFields(this.component, this.updatedFields, param, e.detail.value, e.detail.action);
+ this._config = this.getDefaultConfig();
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Discard changes",
+ message: "Are you sure you want to discard the changes made?",
+ ok: () => {
+ this.#initOriginalObjects();
+ this.requestUpdate();
+ },
+ });
+ }
+
+ onSubmit() {
+ // 1. Create promises with updates
+ this.#setLoading(true);
+ const _userGroupPromises = this._userGroupUpdates
+ .map(update => {
+ let error;
+ const params= {
+ includeResult: true,
+ action: update.isChecked ? "ADD" : "REMOVE",
+ };
+ const data = {
+ users: [update.userId],
+ };
+ return this.opencgaSession.opencgaClient.studies()
+ .updateGroupsUsers(this.studyFqn, update.groupId, data, params)
+ .then(() => {
+ const studyId = this.studyFqn.split(":").pop();
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User in Group Update`,
+ message: `
+ ${update.userId} ${update.isChecked ? "ADDED to" : "REMOVED from"}
+ ${update.groupId} in study ${studyId} correctly.
+ `,
+ });
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "userGroupUpdate", {}, {
+ user: update.userId,
+ group: update.groupId,
+ }, error);
+ });
+
+ });
+ // 2. Execute all changes and refresh session
+ Promise.all(_userGroupPromises)
+ .finally(() => {
+ this.#setLoading(false);
+ LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", {});
+ LitUtils.dispatchCustomEvent(this, "studyUpdate", this.study, {});
+ });
+ }
+
+ onUserGroupChange(e, userId, groupId) {
+ const pos = this._findChangePosition(userId, groupId);
+ if (pos >= 0) {
+ // Remove change from this._userGroupUpdates if the change has been undone.
+ this._userGroupUpdates.splice(pos, 1);
+ } else {
+ // Create an object with the params needed
+ this._userGroupUpdates.push({
+ isChecked: e.currentTarget.checked,
+ userId: userId,
+ groupId: groupId,
+ });
+ }
+ // If a user is member not admin:
+ // - If removed from member => disable admin
+ // - If added to admin => disable member
+ // - And enable them back if the change is undone
+ // If a user is member and admin:
+ // - If removed from admin and added back => do not disable member
+ // If a user is not member and not admin:
+ // - If added to member and removed back => do not disable admin
+ // To avoid undetermined user permissions on submit if contradictory changes are submitted.
+ if (groupId === "@admins" && this.groups.find(group => group.id === "@members" && group.userIds.includes(userId))) {
+ this.forceDisable[`${userId}.@members`] = (e.currentTarget.checked && pos === -1);
+ }
+ if (groupId === "@members" && this.groups.find(group => group.id === "@admins" && !group.userIds.includes(userId))) {
+ this.forceDisable[`${userId}.@admins`] = (!e.currentTarget.checked && pos === -1);
+ }
+
+ this._config = {...this._config};
+ this.requestUpdate();
+ }
+
+ // Double-check if the user is undoing a previous change on a specific user / group.
+ _findChangePosition(userId, groupId) {
+ // pos will equal -1 if a previous changes has been undone
+ return this._userGroupUpdates.findIndex(update => update.userId === userId && update.groupId === groupId);
+ }
+
+ _findCurrentValue(userId, groupId) {
+ // Check if this user has been added/removed from this group
+ const change = this._userGroupUpdates.find(update => update.userId === userId && update.groupId === groupId);
+ if (change) {
+ // 2. If added/removed, return value
+ return change.isChecked;
+ } else {
+ // 2. If not, check if the user was initially on this group
+ const group = this.groups.find(group => group.id === groupId);
+ return group.userIds.includes(userId);
+ }
+ }
+
+ renderStyle() {
+ // Note 20240724 Vero: This css class enables vertical scroll on tbody
+ return html `
+
+ `;
+ }
+
+ // --- RENDER ---
+ render() {
+ if (!this.component) {
+ return nothing;
+ }
+
+ return html`
+ ${this.renderStyle()}
+ this.onFieldChange(e)}"
+ @clear="${e => this.onClear(e)}"
+ @submit="${e => this.onSubmit(e)}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ const sections = [
+ {
+ display: {
+ descriptionClassName: "d-block text-secondary",
+ visible: data => data?.selectedGroups !== "" && data?.selectedUsers?.length > 0,
+ },
+ elements: [
+ {
+ // title: "Table",
+ field: "selectedUsers",
+ type: "table",
+ display: {
+ className: "study-users-manage-table",
+ width: 12,
+ columns: [
+ {
+ id: "id",
+ title: "User Id",
+ type: "custom",
+ display: {
+ render: (value, update, params, data, userId) => {
+ const role = Object.values(this.userRole)
+ .find(role => role.check(userId)) || {};
+ return html`
+
+ ${role?.displayName ? html`
+
${userId}
+
${role.displayName}
+ ` : html`
+
${userId}
+ `}
+
+ `;
+ }
+ },
+ },
+ ...this.groups?.map(group => ({
+ id: group.id,
+ title: group.id === "@members" ? "Study Member" : "Study Admin",
+ type: "custom",
+ display: {
+ helpMessage: "",
+ render: (checked, dataFormFilterChange, updateParams, data, userId) => {
+ const currentValue = this._findCurrentValue(userId, group.id);
+ const changePosition = this._findChangePosition(userId, group.id);
+ // If the user is organization admin or owner,
+ // the checkboxes in members and admins need to be disabled
+ const userIsOrganizationAdmin = OpencgaCatalogUtils.isOrganizationAdmin(this.opencgaSession.organization, userId);
+ return html`
+
+ this.onUserGroupChange(e, userId, group.id)}">
+ ${changePosition >= 0 ? html`* ` : ""}
+
+ `;
+ }
+ },
+ })),
+ ],
+ },
+ },
+ ],
+ },
+ ];
+ return {
+ id: "",
+ display: this.displayConfig,
+ sections: sections,
+ };
+ }
+
+}
+
+customElements.define("study-users-manage", StudyUsersManage);
+
diff --git a/src/webcomponents/organization/admin/user-admin-admins-change.js b/src/webcomponents/organization/admin/user-admin-admins-change.js
new file mode 100644
index 0000000000..ecb84ce4bc
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-admins-change.js
@@ -0,0 +1,183 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import UtilsNew from "../../../core/utils-new.js";
+import "./filters/user-status-filter.js";
+
+export default class UserAdminAdminsChange extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ userId: {
+ type: String
+ },
+ action: {
+ type: String
+ },
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.userId = "";
+ this.displayTitle = "";
+ this.displayText = "";
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ titleWidth: 3,
+ titleStyle: "color: var(--main-bg-color);margin-bottom:16px;font-weight:bold;",
+ defaultLayout: "horizontal",
+ buttonOkText: "Confirm",
+ buttonClearText: "",
+ };
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("userId") || changedProperties.has("action")) {
+ this.propertyObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this._config = this.getDefaultConfig();
+ }
+ super.update(changedProperties);
+ }
+
+ propertyObserver() {
+ if (this.action && this.userId) {
+ this.displayMessages = {
+ "REMOVE": {
+ title: `Remove user '${this.userId}' from the list of organization users.`,
+ text: `The user '${this.userId}' will be REMOVED from the list of organization administrators.
+ As a result, they will no longer be able to perform specific actions such as creating or editing users,
+ modifying organization information, or creating projects and studies.`,
+ successText: `The '${this.userId}' has been successfully REMOVED from the list of organization administrators`,
+ },
+ "ADD": {
+ title: `Add user '${this.userId}' to the list of organization users.`,
+ text: `The user '${this.userId}' will be ADDED to the list of organization administrators.
+ As a result, they be able to perform specific actions such as creating or editing users,
+ modifying organization information, or creating projects and studies.`,
+ successText: `The '${this.userId}' has been successfully ADDED to the list of organization administrators`,
+ },
+ };
+ this.displayTitle = this.displayMessages[this.action].title;
+ this.displayText = this.displayMessages[this.action].text;
+ this.displaySuccessText = this.displayMessages[this.action].successText;
+ }
+ }
+
+ onSubmit() {
+ const params = {
+ includeResult: true,
+ adminsAction: this.action,
+ };
+ let admins = this.organization.admins;
+ this.action === "ADD" ? admins.push(this.userId) : admins = [this.userId]; // REMOVE userId from array of admins
+
+ const updateParams = {
+ admins: admins,
+ };
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.organization()
+ .update(this.organization.id, updateParams, params)
+ .then(response => {
+ this.user = UtilsNew.objectClone(response.responses[0].results[0]);
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `Organization Admin`,
+ message: this.displaySuccessText,
+ });
+ LitUtils.dispatchCustomEvent(this, "userUpdate", this.user, {});
+ LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", this._study, {});
+ })
+ .catch(reason => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ LitUtils.dispatchCustomEvent(this, "userUpdateFailed", this.user, {}, reason);
+ })
+ .finally(() => {
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ return html`
+
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ icon: "fas fa-edit",
+ buttons: {
+ okText: "Confirm",
+ },
+ display: this.displayConfig || this.displayConfigDefault,
+ sections: [
+ {
+ // title: this.displayTitle,
+ elements: [
+ {
+ type: "notification",
+ text: this.displayText,
+ display: {
+ visible: true,
+ icon: "fas fa-exclamation-triangle",
+ notificationType: "warning",
+ },
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("user-admin-admins-change", UserAdminAdminsChange);
diff --git a/src/webcomponents/organization/admin/user-admin-browser.js b/src/webcomponents/organization/admin/user-admin-browser.js
new file mode 100644
index 0000000000..9803bfa211
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-browser.js
@@ -0,0 +1,137 @@
+/**
+ * Copyright 2015-2024 OpenCB *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {LitElement, html} from "lit";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import UtilsNew from "../../../core/utils-new.js";
+import "./user-admin-grid.js";
+
+export default class UserAdminBrowser extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ studyId: {
+ type: String,
+ },
+ study: {
+ type: Object,
+ },
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ // QUESTION: pending to decide if we allow browser settings here.
+ settings: {
+ type: Object,
+ },
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "user-admin-browser";
+ this.users = [];
+ this._config = this.getDefaultConfig();
+ this.isLoading = false;
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("settings")) {
+ this.settingsObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ settingsObserver() {
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.settings,
+ };
+ }
+
+ // Vero 09072024 Note: Maintained for future use of this component in the Study Admin
+ studyIdObserver() {
+ if (this.studyId && this.opencgaSession) {
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.studies()
+ .info(this.studyId)
+ .then(response => {
+ this._study = UtilsNew.objectClone(response.responses[0].results[0]);
+ })
+ .catch(reason => {
+ this._study = {};
+ error = reason;
+ console.error(reason);
+ })
+ .finally(() => {
+ this._config = this.getDefaultConfig();
+ LitUtils.dispatchCustomEvent(this, "studyChange", this.study, {}, error);
+ this.#setLoading(false);
+ });
+ } else {
+ this._study = {};
+ }
+ }
+
+ renderFilterGraphics() {
+ if (this._config.showGraphicFilters) {
+ return html `
+
+ `;
+ }
+ }
+
+ render() {
+ return html `
+
+ ${this.renderFilterGraphics()}
+
+
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ showGraphicFilters: false,
+ };
+ }
+
+}
+
+customElements.define("user-admin-browser", UserAdminBrowser);
diff --git a/src/webcomponents/organization/admin/user-admin-create.js b/src/webcomponents/organization/admin/user-admin-create.js
new file mode 100644
index 0000000000..eb7221d57c
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-create.js
@@ -0,0 +1,225 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html} from "lit";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import UtilsNew from "../../../core/utils-new.js";
+
+export default class UserAdminCreate extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this._user = {};
+ this.isLoading = false;
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ titleWidth: 3,
+ defaultLayout: "horizontal",
+ buttonOkText: "Create"
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ #initOriginalObjects() {
+ this._user = {};
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("displayConfig")) {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig
+ };
+ }
+ if (changedProperties.has("opencgaSession")) {
+ this.opencgaSessionObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ opencgaSessionObserver() {
+ this.#initOriginalObjects();
+ }
+
+ onFieldChange(e, field) {
+ this._user = {...e.detail.data}; // force to refresh the object-list
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Clear user",
+ message: "Are you sure to clear?",
+ ok: () => {
+ this._user = {};
+ this._config = this.getDefaultConfig();
+ this.requestUpdate();
+ },
+ });
+ }
+
+ onSubmit() {
+ // Prepare object to be submitted
+ this._user.organization = this.organization.id;
+ delete this._user.confirmPassword;
+
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.users()
+ .create(this._user)
+ .then(response => {
+ const newUser = UtilsNew.objectClone(response.responses[0].results[0]);
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User Create`,
+ message: `User ${newUser.id} created in organization ${this.organization.id} successfully`,
+ });
+ LitUtils.dispatchCustomEvent(this, "userCreate", newUser, {});
+ LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", {}, {});
+ })
+ .catch(reason => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ this.#setLoading(false);
+ this.#initOriginalObjects();
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ return html`
+ this.onFieldChange(e)}"
+ @clear="${e => this.onClear(e)}"
+ @submit="${e => this.onSubmit(e)}">
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ display: this.displayConfig || this.displayConfigDefault,
+ sections: [
+ {
+ elements: [
+ {
+ title: "User ID",
+ field: "id",
+ type: "input-text",
+ required: true,
+ display: {
+ placeholder: "Add a short ID...",
+ },
+ },
+ {
+ title: "User Name",
+ field: "name",
+ type: "input-text",
+ required: true,
+ display: {
+ placeholder: "Add the user name...",
+ },
+ },
+ {
+ title: "User Email",
+ field: "email",
+ type: "input-text",
+ required: true,
+ display: {
+ placeholder: "Add the user email...",
+ },
+ },
+ {
+ title: "User Password",
+ field: "password",
+ type: "input-password",
+ required: true,
+ validation: {
+ validate: (value, user) => !!user.password,
+ message: "The user password can not be empty.",
+ },
+ display: {
+ helpMessage: `
+ Type a strong password of a minimum length of 8 characters, combining at least:
+ 1 upper-case letter, 1 lower-case letter, 1 digit, and 1 special character.
+ `,
+ },
+ },
+ {
+ title: "Confirm user password",
+ field: "confirmPassword",
+ type: "input-password",
+ required: true,
+ defaultValue: "",
+ validation: {
+ validate: (value, user) => {
+ return !!user.confirmPassword && user.confirmPassword === user.password;
+ },
+ message: "The user passwords do not match.",
+ },
+ },
+ {
+ title: "Change password",
+ field: "requiredAction",
+ type: "toggle-switch",
+ required: false,
+ display: {
+ disabled: true,
+ helpMessage: "Coming soon: Required user action for changing password.",
+ },
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("user-admin-create", UserAdminCreate);
diff --git a/src/webcomponents/organization/admin/user-admin-details-update.js b/src/webcomponents/organization/admin/user-admin-details-update.js
new file mode 100644
index 0000000000..a7e0c4dd9f
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-details-update.js
@@ -0,0 +1,257 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import FormUtils from "../../commons/forms/form-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+
+export default class UserAdminDetailsUpdate extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ userId: {
+ type: String
+ },
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.user = {}; // Original object
+ this._user = {}; // Updated object
+ this.userId = "";
+ this.displayConfig = {};
+ this.updatedFields = {};
+ // Some of the fields modeled for USER cannot be updated at all or updated through the endpoint used in this component.
+ // They need to be removed from the object.
+ this.updateCustomisation = [
+ "internal.status",
+ "internal.registrationDate",
+ "internal.lastModified",
+ "internal.account.password",
+ "internal.account.failedAttempts",
+ "internal.account.authentication",
+ ];
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #initConfigNotification() {
+ this._config.notification = {
+ title: "",
+ text: "Some changes have been done in the form. Not saved changes will be lost",
+ type: "notification",
+ display: {
+ visible: () => {
+ return UtilsNew.isNotEmpty(this.updatedFields);
+ },
+ notificationType: "warning",
+ },
+ };
+ }
+
+ #initOriginalObjects() {
+ this._user = UtilsNew.objectClone(this.user);
+ this.updatedFields = {};
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("userId")) {
+ this.userIdObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this._config = this.getDefaultConfig();
+ if (!this._config?.notification) {
+ this.#initConfigNotification();
+ }
+ }
+ super.update(changedProperties);
+ }
+
+ userIdObserver() {
+ if (this.userId && this.opencgaSession) {
+ const params = {
+ organization: this.organization.id,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.users()
+ .info(this.userId, params)
+ .then(response => {
+ this.user = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.#initOriginalObjects();
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "userInfo", this.user, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ onFieldChange(e, field) {
+ const param = field || e.detail.param;
+ this.updatedFields = FormUtils.getUpdatedFields(
+ this.user,
+ this.updatedFields,
+ param,
+ e.detail.value,
+ e.detail.action);
+
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Discard changes",
+ message: "Are you sure you want to discard the changes made?",
+ ok: () => {
+ this.#initOriginalObjects();
+ this.requestUpdate();
+ // We need to dispatch a component clear event
+ LitUtils.dispatchCustomEvent(this, "userClear", null, {
+ user: this.user,
+ });
+ },
+ });
+ }
+
+ onSubmit() {
+ const params = {
+ includeResult: true,
+ };
+ debugger
+ const updateParams = FormUtils.getUpdateParams(this._user, this.updatedFields, this.updateCustomisation);
+
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.organization()
+ .updateUser(this.userId, updateParams, params)
+ .then(response => {
+ this.user = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.#initOriginalObjects();
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User Details Update`,
+ message: `User ${this.userId} updated correctly`,
+ });
+ LitUtils.dispatchCustomEvent(this, "userUpdate", this.user, {});
+ })
+ .catch(error => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
+ LitUtils.dispatchCustomEvent(this, "userUpdateFailed", this.user, {}, error);
+ })
+ .finally(() => {
+ this.#setLoading(false);
+ });
+
+ }
+
+ render() {
+ return html `
+ this.onFieldChange(e)}"
+ @submit="${this.onSubmit}"
+ @clear="${this.onClear}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ icon: "fas fa-edit",
+ buttons: {
+ clearText: "Discard Changes",
+ okText: "Update",
+ },
+ display: this.displayConfig,
+ sections: [
+ {
+ title: "Details",
+ display: {
+ titleVisible: false,
+ },
+ elements: [
+ {
+ title: "User Name",
+ field: "name",
+ type: "input-text",
+ display: {
+ helpMessage: "Edit the user name...",
+ },
+ },
+ {
+ title: "User email",
+ field: "email",
+ type: "input-text",
+ display: {
+ helpMessage: "Edit the user email...",
+ },
+ },
+ ],
+ },
+ {
+ title: "Account",
+ display: {
+ titleVisible: false,
+ },
+ elements: [
+ {
+ title: "Expiration Date",
+ field: "internal.account.expirationDate",
+ type: "input-date",
+ display: {
+ format: date => UtilsNew.dateFormatter(date)
+ },
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("user-admin-details-update", UserAdminDetailsUpdate);
diff --git a/src/webcomponents/organization/admin/user-admin-grid.js b/src/webcomponents/organization/admin/user-admin-grid.js
new file mode 100644
index 0000000000..f998cd36a9
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-grid.js
@@ -0,0 +1,630 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LitElement, html, nothing} from "lit";
+import GridCommons from "../../commons/grid-commons.js";
+import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js";
+import ModalUtils from "../../commons/modal/modal-utils.js";
+import UtilsNew from "../../../core/utils-new.js";
+import "./user-admin-create.js";
+import "./user-admin-details-update.js";
+import "./user-admin-password-reset.js";
+import "./user-admin-status-update.js";
+import "./user-admin-admins-change.js";
+// import "./user-admin-password-change.js";
+
+export default class UserAdminGrid extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ toolId: {
+ type: String,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ organization: {
+ type: Object,
+ },
+ users: {
+ type: Array
+ },
+ active: {
+ type: Boolean
+ },
+ config: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.COMPONENT_ID = "user-grid";
+ this._prefix = UtilsNew.randomString(8);
+ this.gridId = this._prefix + this.COMPONENT_ID;
+ this.active = true;
+ this._config = this.getDefaultConfig();
+ this.action = "";
+ this.displayConfigDefault = {
+ header: {
+ horizontalAlign: "center",
+ verticalAlign: "bottom",
+ },
+ };
+ }
+
+ // --- LIFE-CYCLE METHODS
+ update(changedProperties) {
+ if (changedProperties.has("opencgaSession") ||
+ changedProperties.has("toolId") ||
+ changedProperties.has("organization") ||
+ changedProperties.has("config")) {
+ this.propertyObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.size > 0 && this.active) {
+ this.renderRemoteTable();
+ }
+ }
+
+ propertyObserver() {
+ // With each property change we must be updated config and create the columns again. No extra checks are needed.
+ this._config = {
+ ...this.getDefaultConfig(),
+ ...this.config,
+ };
+
+ this.gridCommons = new GridCommons(this.gridId, this, this._config);
+
+ // Config for the grid toolbar
+ this.toolbarSetting = {
+ ...this._config,
+ };
+
+ this.toolbarConfig = {
+ toolId: this.toolId,
+ resource: "USER",
+ columns: this._getDefaultColumns(),
+ create: {
+ display: {
+ modalTitle: "User Create",
+ modalDraggable: true,
+ modalCyDataName: "modal-create",
+ modalSize: "modal-lg"
+ // disabled: true,
+ // disabledTooltip: "...",
+ },
+ modalId: `${this._prefix}CreateUserModal`,
+ render: () => html `
+ this.onUserCreate(e)}">
+ `
+ },
+ };
+
+ this.permissions = {
+ "organization": () => OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) ? "" : "disabled",
+ "study": () => OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) ? "" : "disabled",
+ };
+
+ this.modals = {
+ "edit-details": {
+ label: "Edit Details",
+ icon: "fas fa-edit",
+ modalId: `${this._prefix}UpdateDetailsModal`,
+ render: () => this.renderModalDetailsUpdate(),
+ permission: this.permissions["organization"](),
+ divider: true,
+ },
+ // ToDo 20240529 Vero: Nacho/Pedro to discuss:
+ // - Organization admin/owner can change usr pwd without entering current pwd
+ /*
+ "change-password": {
+ label: "Change Password",
+ icon: "fas fa-edit",
+ modalId: `${this._prefix}ChangePasswordModal`,
+ render: () => this.renderModalPasswordUpdate(),
+ permission: OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id) || "disabled",
+ },
+ */
+ "reset-password": {
+ label: "Reset Password",
+ icon: "fas fa-key",
+ modalId: `${this._prefix}ResetPasswordModal`,
+ render: () => this.renderModalPasswordReset(),
+ permission: this.permissions["organization"](),
+ divider: true,
+ },
+ "change-status": {
+ label: "Change Status",
+ icon: "fas fa-sign-in-alt",
+ modalId: `${this._prefix}ChangeStatusModal`,
+ render: () => this.renderModalStatusUpdate(),
+ permission: this.permissions["organization"](),
+ },
+ "change-admin": {
+ labelAdd: "Add as Admin",
+ labelRemove: "Remove as Admin",
+ iconAdd: "fas fa-user-plus",
+ iconRemove: "fas fa-user-minus",
+ modalId: `${this._prefix}ChangeAdminModal`,
+ render: action => this.renderModalAdminChange(action),
+ permission: this.permissions["organization"](),
+ divider: true,
+ },
+ "delete": {
+ label: "Delete User",
+ icon: "fas fa-trash-alt ",
+ color: "text-danger",
+ // modalId: `${this._prefix}DeleteUserModal`,
+ // render: () => this.renderModalDeleteUser(),
+ permission: "disabled", // CAUTION: Not possible to delete users for now
+ },
+ };
+ }
+
+ // *** PRIVATE METHODS ***
+ renderRemoteTable() {
+ if (this.opencgaSession?.opencgaClient && this.organization.id) {
+ this._columns = this._getDefaultColumns();
+ this.table = $("#" + this.gridId);
+ this.table.bootstrapTable("destroy");
+ this.table.bootstrapTable({
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ columns: this._columns,
+ method: "get",
+ sidePagination: "server",
+ iconsPrefix: GridCommons.GRID_ICONS_PREFIX,
+ icons: GridCommons.GRID_ICONS,
+ uniqueId: "id",
+ // Table properties
+ pagination: this._config.pagination,
+ pageSize: this._config.pageSize,
+ pageList: this._config.pageList,
+ paginationVAlign: "both",
+ // formatShowingRows: this.gridCommons.formatShowingRows,
+ detailView: !!this.detailFormatter,
+ loadingTemplate: () => GridCommons.loadingFormatter(),
+ ajax: params => {
+ let result = null;
+ this.filters = {
+ organization: this.organization.id,
+ limit: params.data.limit,
+ skip: params.data.offset || 0,
+ count: !this.table.bootstrapTable("getOptions").pageNumber || this.table.bootstrapTable("getOptions").pageNumber === 1,
+ };
+
+ // Store the current filters
+ this.opencgaSession.opencgaClient.users()
+ .search(this.filters)
+ .then(response => {
+ result = response;
+ return response;
+ })
+ .then(() => {
+ // Prepare data for columns extensions
+ const rows = result.responses?.[0]?.results || [];
+ return this.gridCommons.prepareDataForExtensions(this.COMPONENT_ID, this.opencgaSession, this.filters, rows);
+ })
+ .then(() => params.success(result))
+ .catch(error => {
+ console.error(error);
+ params.error(error);
+ });
+ },
+ responseHandler: response => {
+ const result = this.gridCommons.responseHandler(response, $(this.table).bootstrapTable("getOptions"));
+ return result.response;
+ },
+ onClickRow: (row, selectedElement) => this.gridCommons.onClickRow(row.id, row, selectedElement),
+ onLoadSuccess: data => this.gridCommons.onLoadSuccess(data, 1),
+ onLoadError: (e, restResponse) => this.gridCommons.onLoadError(e, restResponse),
+ });
+ }
+ }
+
+ _getDefaultColumns() {
+ this._columns = [
+ {
+ id: "id",
+ title: "User ID",
+ field: "id",
+ visible: this.gridCommons.isColumnVisible("id"),
+ formatter: (value, row) => this.userIdFormatter(value, row),
+ },
+ {
+ id: "name",
+ title: "Name",
+ field: "name",
+ visible: this.gridCommons.isColumnVisible("name"),
+ },
+ {
+ id: "email",
+ title: "Email",
+ field: "email",
+ visible: this.gridCommons.isColumnVisible("email"),
+ },
+ {
+ id: "authentication",
+ title: "Authentication",
+ field: "internal.account.authentication.id",
+ visible: this.gridCommons.isColumnVisible("authentication")
+ },
+ {
+ id: "failedAttempts",
+ title: "Failed Attempts",
+ field: "internal.account.failedAttempts",
+ visible: this.gridCommons.isColumnVisible("failedAttempts"),
+ },
+ {
+ id: "status",
+ title: "Status",
+ field: "internal.status",
+ formatter: value => CatalogGridFormatter.userStatusFormatter(value, this._config.userStatus),
+ visible: this.gridCommons.isColumnVisible("status")
+ },
+ {
+ id: "lastModifiedDate",
+ title: "Last Modified Date",
+ field: "internal.lastModified",
+ formatter: (value, row) => UtilsNew.dateFormatter(row.internal.lastModified),
+ visible: this.gridCommons.isColumnVisible("lastModifiedDate")
+ },
+ {
+ id: "dates",
+ title: "Expiration / Creation Dates",
+ field: "dates",
+ halign: this.displayConfigDefault.header.horizontalAlign,
+ valign: "middle",
+ formatter: (value, row) => this.datesFormatter(value, row),
+ visible: this.gridCommons.isColumnVisible("dates")
+ },
+ ];
+
+ if (this._config.annotations?.length > 0) {
+ this.gridCommons.addColumnsFromAnnotations(this._columns, CatalogGridFormatter.customAnnotationFormatter, this._config);
+ }
+
+ if (this.opencgaSession && this._config.showActions) {
+ this._columns.push({
+ id: "actions",
+ title: "Actions",
+ field: "actions",
+ align: "center",
+ formatter: (value, row) => `
+
+
+
+ Actions
+
+
+
+ `,
+ events: {
+ "click ul>li>a": (e, value, row) => this.onActionClick(e, value, row),
+ },
+ });
+ }
+
+ this._columns = this.gridCommons.addColumnsFromExtensions(this._columns, this.COMPONENT_ID);
+ return this._columns;
+ }
+
+ userIdFormatter(value, user) {
+ // Note 20240620 vero: Viz and change owner will be implemented in following release
+ // const organizationOwner = this.organization.owner;
+ return this.organization.admins.includes(user.id) ? `
+
+
+ ${value}
+
+ ` : value;
+ }
+
+ datesFormatter(value, user) {
+ const expirationDateString = UtilsNew.dateFormatter(user.internal.account.expirationDate);
+ const expirationDate = new Date(expirationDateString);
+ const currentDate = new Date();
+ let expirationDateClass = null;
+ if (currentDate > expirationDate) {
+ expirationDateClass = "text-danger";
+ }
+ return `
+ ${expirationDateString}
+ ${UtilsNew.dateFormatter(user.creationDate)}
+ `;
+ }
+
+ // *** EVENTS ***
+ async onActionClick(e, value, row) {
+ this.action = e.currentTarget.dataset.action;
+ this.userId = row.id;
+ this.adminAction = e.currentTarget.dataset.admin ?? "";
+ this.requestUpdate();
+ await this.updateComplete;
+ // NOTE 20240804 Vero: Since reset password does not need inputs, it has been decided that it should be
+ // a notification instead of the regular update modal. Therefore, in this case, a modal is not created and
+ // should not be shown.
+ if (this.action !== "reset-password") {
+ ModalUtils.show(this.modals[this.action]["modalId"]);
+ }
+ }
+
+ onUserUpdate(e, id) {
+ ModalUtils.close(id);
+ this.renderRemoteTable();
+ }
+
+ onUserCreate() {
+ // Close modal
+ ModalUtils.close(this.toolbarConfig.create.modalId);
+ }
+
+ onCloseNotification() {
+ this.userId = null;
+ this.action = "";
+ this.requestUpdate();
+ }
+
+ // *** RENDER METHODS ***
+ renderModalDetailsUpdate() {
+ return ModalUtils.create(this, `${this._prefix}UpdateDetailsModal`, {
+ display: {
+ modalTitle: `Update Details: User ${this.userId} in organization ${this.organization.id}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-details-update",
+ modalSize: "modal-lg"
+ },
+ render: () => {
+ return html`
+ this.onUserUpdate(e, `${this._prefix}UpdateDetailsModal`)}">
+
+ `;
+ },
+ });
+ }
+
+ /*
+ // Caution 20240616 Vero: Uncomment this code when endpoint fixed in OpenCGA for admin/owner change usr pwd
+ renderModalPasswordUpdate() {
+ return ModalUtils.create(this, `${this._prefix}ChangePasswordModal`, {
+ display: {
+ modalTitle: `Change Password: User ${this.userId} in organization ${this.organization.id}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-password-change",
+ modalSize: "modal-lg"
+ },
+ render: () => {
+ return html`
+ this.onUserUpdate(e, `${this._prefix}ChangePasswordModal`)}">
+
+ `;
+ },
+ });
+ }
+ */
+
+ renderModalPasswordReset() {
+ return html`
+ this.onCloseNotification(e)}">
+
+ `;
+ }
+
+ renderModalStatusUpdate() {
+ return ModalUtils.create(this, `${this._prefix}ChangeStatusModal`, {
+ display: {
+ modalTitle: `Update Status: User '${this.userId}' in organization '${this.organization.id}'`,
+ modalDraggable: true,
+ modalCyDataName: "modal-user-admin-status-update",
+ modalSize: "modal-lg"
+ },
+ render: () => html`
+ this.onUserUpdate(e, `${this._prefix}ChangeStatusModal`)}">
+
+ `,
+ });
+ }
+
+ renderModalAdminChange(action) {
+ return ModalUtils.create(this, `${this._prefix}ChangeAdminModal`, {
+ display: {
+ modalTitle: `Update Organization Admins: User ${this.userId} in organization ${this.organization.id}`,
+ modalDraggable: true,
+ modalCyDataName: "modal-user-admin-admin-set",
+ modalSize: "modal-lg"
+ },
+ render: () => html`
+ this.onUserUpdate(e, `${this._prefix}ChangeAdminModal`)}">
+
+ `,
+ });
+ }
+
+ renderToolbar() {
+ if (this._config.showToolbar) {
+ return html `
+
+
+ `;
+ }
+ }
+
+ render() {
+ return html`
+
+ ${this.renderToolbar()}
+
+
+
+ ${this.action ? this.modals[this.action]["render"](this.adminAction || null) : nothing}
+ `;
+ }
+
+ // *** DEFAULT CONFIG ***
+ getDefaultConfig() {
+ return {
+ // Settings
+ pagination: true,
+ pageSize: 10,
+ pageList: [5, 10, 25],
+ multiSelection: false,
+ showSelectCheckbox: false,
+
+ showToolbar: true,
+ showActions: true,
+
+ showCreate: true,
+ showExport: false,
+ showSettings: false,
+ exportTabs: ["download", "link", "code"],
+ // Config
+ userStatus: [
+ {
+ id: "READY",
+ displayLabel: "ACTIVE", // Fixme: ACTIVE | ENABLED | READY?
+ displayColor: "#16A83C",
+ displayOutline: "btn-outline-success",
+ description: "The user can login",
+ isSelectable: true, // Choice selectable by org admin/owner
+ isEnabled: true, // Choice visible by org admin/owner
+ },
+ {
+ id: "SUSPENDED",
+ displayLabel: "SUSPENDED",
+ displayColor: "#E1351E",
+ displayOutline: "btn-outline-danger",
+ description: "The user can not login into the system",
+ isSelectable: true,
+ isEnabled: true,
+ },
+ {
+ id: "BANNED",
+ displayLabel: "BANNED",
+ displayColor: "#E17F1E",
+ description: "User locked for more than allowed login attempts. The admin/owner can enable the user back.",
+ isSelectable: false,
+ isEnabled: true,
+ },
+ {
+ id: "UNDEFINED",
+ displayLabel: "-",
+ displayColor: "#E1E2E5",
+ description: "The user status is unknown",
+ isSelectable: false,
+ isEnabled: false,
+ },
+ ],
+
+ };
+ }
+
+}
+
+customElements.define("user-admin-grid", UserAdminGrid);
diff --git a/src/webcomponents/organization/admin/user-admin-password-change.js b/src/webcomponents/organization/admin/user-admin-password-change.js
new file mode 100644
index 0000000000..25fdd3fbc9
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-password-change.js
@@ -0,0 +1,218 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import "../../user/user-password-change.js";
+
+export default class UserAdminPasswordChange extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ userId: {
+ type: String
+ },
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this.user = {}; // Original object
+ this._user = {}; // Updated object
+ this.userId = "";
+ this.displayConfig = {};
+ this.updatedFields = {};
+
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #initConfigNotification() {
+ this._config.notification = {
+ title: "",
+ text: "Some changes have been done in the form. Not saved changes will be lost",
+ type: "notification",
+ display: {
+ visible: () => {
+ return UtilsNew.isNotEmpty(this.updatedFields);
+ },
+ notificationType: "warning",
+ },
+ };
+ }
+
+ #initOriginalObjects() {
+ this._user = UtilsNew.objectClone(this.user);
+ this.updatedFields = {};
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("userId")) {
+ this.userIdObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this._config = this.getDefaultConfig();
+ if (!this._config?.notification) {
+ this.#initConfigNotification();
+ }
+ }
+ super.update(changedProperties);
+ }
+
+ userIdObserver() {
+ if (this.userId && this.opencgaSession) {
+ const params = {
+ organization: this.organization.id,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.users()
+ .info(this.userId, params)
+ .then(response => {
+ this.user = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.#initOriginalObjects();
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "userInfo", this.user, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ render() {
+ return html `
+
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ title: "",
+ showTitle: false,
+ items: [
+ {
+ id: "change-password",
+ name: "Change Password",
+ active: true,
+ render: (user, active, opencgaSession) => {
+ return html`
+
+
+ `;
+ },
+ },
+ ],
+ };
+ /*
+ return {
+ icon: "fas fa-edit",
+ buttons: {
+ clearText: "Discard Changes",
+ okText: "Update",
+ },
+ display: this.displayConfig,
+ sections: [
+ {
+ title: "Change Password",
+ elements: [
+ {
+ type: "custom",
+ display: {
+ render: () => {
+ debugger
+ return html`
+
+
+ `;
+ },
+ },
+ },
+ /*
+ {
+ title: "Reset password",
+ field: "pwdReset",
+ type: "toggle-switch",
+ display: {
+ disabled: true,
+ helpMessage: "Coming soon: Force user to reset the password",
+ },
+ },
+ {
+ title: "Expires in",
+ field: "pwdExpiration",
+ type: "input-text",
+ display: {
+ disabled: true,
+ helpMessage: "Coming soon: Enable password expiration",
+ },
+ },
+ */
+ /*
+ ],
+ },
+ ],
+ };
+ */
+ }
+
+}
+
+customElements.define("user-admin-password-change", UserAdminPasswordChange);
diff --git a/src/webcomponents/organization/admin/user-admin-password-reset.js b/src/webcomponents/organization/admin/user-admin-password-reset.js
new file mode 100644
index 0000000000..6e99e8e280
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-password-reset.js
@@ -0,0 +1,132 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import "../../user/user-password-reset.js";
+
+export default class UserAdminPasswordReset extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ userId: {
+ type: String
+ },
+ opencgaSession: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this._user = null;
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("userId") || changedProperties.has("opencgaSession")) {
+ this.userIdObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ userIdObserver() {
+ if (this.opencgaSession && this.userId) {
+ this.opencgaSession.opencgaClient.users()
+ .info(this.userId, {
+ organization: this.opencgaSession.organization.id,
+ })
+ .then(response => {
+ this._user = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.requestUpdate();
+ })
+ .catch(response => {
+ console.error(response);
+ });
+ }
+ }
+
+ onSubmit() {
+ let error;
+ this.#setLoading(true);
+ // Reset password
+ this.opencgaSession.opencgaClient.users()
+ .resetPassword(this._user.id)
+ .then(() => {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User Reset Password`,
+ message: `User ${this._user.id} password reset correctly`,
+ });
+ LitUtils.dispatchCustomEvent(this, "closeNotification", this._user.id, {}, error);
+ this._user = null;
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ return NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: `Reset Password: User ${this._user.id} in organization ${this.opencgaSession.organization.id}`,
+ message: `
+ Are you sure you want to reset ${this._user.id}'s password?
+
+ The user ${this._user.id} will receive an email with a temporary password in the following
+ email address:
+ ${this._user.email} .
+ `,
+ ok: () => {
+ this.onSubmit();
+ },
+ cancel: () => {
+ this._user = null;
+ LitUtils.dispatchCustomEvent(this, "closeNotification", null);
+ },
+ });
+ }
+
+ getDefaultConfig() {
+ return {};
+ }
+
+}
+
+customElements.define("user-admin-password-reset", UserAdminPasswordReset);
diff --git a/src/webcomponents/organization/admin/user-admin-status-update.js b/src/webcomponents/organization/admin/user-admin-status-update.js
new file mode 100644
index 0000000000..d16b214db2
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-status-update.js
@@ -0,0 +1,284 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import CatalogGridFormatter from "../../commons/catalog-grid-formatter.js";
+import FormUtils from "../../commons/forms/form-utils.js";
+import "./filters/user-status-filter.js";
+
+export default class UserAdminStatusUpdate extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ userId: {
+ type: String,
+ },
+ organization: {
+ type: Object,
+ },
+ opencgaSession: {
+ type: Object,
+ },
+ displayConfig: {
+ type: Object,
+ },
+ };
+ }
+
+ #init() {
+ this.user = {}; // Original object
+ this._user = {}; // Updated object
+ this.userId = "";
+ this.updatedFields = {};
+ this.displayConfig = {};
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ defaultLayout: "horizontal",
+ labelAlign: "right",
+ buttonOkText: "Update Status",
+ buttonOkDisabled: true,
+ };
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #initConfigNotification() {
+ this._config.notification = {
+ title: "",
+ text: "Some changes have been done in the form. Not saved changes will be lost",
+ type: "notification",
+ display: {
+ visible: () => UtilsNew.isNotEmpty(this.updatedFields),
+ notificationType: "warning",
+ },
+ };
+ }
+
+ #initOriginalObjects() {
+ this._user = UtilsNew.objectClone(this.user);
+ this.updatedFields = {};
+ this.displayConfigObserver();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("userId")) {
+ this.userIdObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this.displayConfigObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ userIdObserver() {
+ if (this.userId && this.opencgaSession) {
+ const params = {
+ organization: this.organization.id,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.users()
+ .info(this.userId, params)
+ .then(response => {
+ this.user = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.#initOriginalObjects();
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "userInfo", this.user, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ displayConfigObserver() {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig,
+ };
+ this._config = this.getDefaultConfig();
+ if (!this._config?.notification) {
+ this.#initConfigNotification();
+ }
+ }
+
+ onFieldChange(e) {
+ const param = e.detail.param;
+ this.updatedFields = FormUtils.getUpdatedFields(
+ this.user,
+ this.updatedFields,
+ param,
+ e.detail.value,
+ e.detail.action);
+
+ this._config.display.buttonOkDisabled = UtilsNew.isEmpty(this.updatedFields);
+ this._config = {...this._config};
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Discard changes",
+ message: "Are you sure you want to discard the changes made?",
+ ok: () => {
+ this.#initOriginalObjects();
+ // We need to dispatch a component clear event
+ LitUtils.dispatchCustomEvent(this, "userClear", null, {
+ user: this.user,
+ });
+ },
+ });
+ }
+
+ onSubmit() {
+ let error;
+ const params = {
+ includeResult: true,
+ };
+ const updateParams = FormUtils.getUpdateParams(this._user, this.updatedFields);
+
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.organization()
+ .userUpdateStatus(this.user.id, updateParams.internal.status.id, params)
+ .then(response => {
+ this.user = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.updatedFields = {};
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User Status Update`,
+ message: `User ${this.user.id} status has been updated correctly`,
+ });
+ LitUtils.dispatchCustomEvent(this, "userUpdate", this.user, {}, error);
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ if (this.isLoading) {
+ return html` `;
+ }
+
+ if (!this.user.internal?.status?.id) {
+ return html `
+
+
+ The ${this.user.id} does not have a status ID.
+
+ `;
+ }
+
+ return html`
+ this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ display: this.displayConfig,
+ sections: [
+ {
+ title: "Current Status",
+ elements: [
+ {
+ title: "User ID",
+ field: "id",
+ type: "input-text",
+ display: {
+ disabled: true,
+ }
+ },
+ {
+ title: "Status",
+ type: "complex",
+ display: {
+ containerClassName: "d-flex align-items-center",
+ template: "${internal.status}",
+ format: {
+ // FIXME: Displaying original data this.user.internal.status. Should not be a complex element
+ "internal.status": () => CatalogGridFormatter.userStatusFormatter(this.user?.internal?.status, this._config.display?.userStatus),
+ },
+ },
+ },
+ ],
+ },
+ {
+ title: "Change Status",
+ description: "You can change the status of the user here",
+ display: {
+ // titleHeader: "h4",
+ // titleClassName: "d-block text-secondary"
+ // titleStyle: "",
+ // descriptionClassName: "d-block text-secondary",
+ // descriptionStyle: "",
+ // visible: () =>
+ },
+ elements: [
+ {
+ title: "New Status",
+ field: "internal.status.id",
+ type: "custom",
+ display: {
+ render: (status, dataFormFilterChange) => html `
+ dataFormFilterChange(e.detail.value)}">
+
+ `,
+ },
+ },
+ ],
+ },
+ ],
+ };
+ }
+
+}
+
+customElements.define("user-admin-status-update", UserAdminStatusUpdate);
diff --git a/src/webcomponents/organization/admin/user-admin-update.js b/src/webcomponents/organization/admin/user-admin-update.js
new file mode 100644
index 0000000000..ebd8d6236e
--- /dev/null
+++ b/src/webcomponents/organization/admin/user-admin-update.js
@@ -0,0 +1,344 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import LitUtils from "../../commons/utils/lit-utils.js";
+import FormUtils from "../../commons/forms/form-utils.js";
+import NotificationUtils from "../../commons/utils/notification-utils.js";
+import "../../user/user-password-change.js";
+import "../../user/user-password-reset.js";
+
+export default class UserAdminUpdate extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ userId: {
+ type: String
+ },
+ organization: {
+ type: Object,
+ },
+ studyId: {
+ type: String,
+ },
+ active: {
+ type: Boolean,
+ },
+ opencgaSession: {
+ type: Object
+ },
+ displayConfig: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this._user = {};
+ this.userId = "";
+ this.studyId = "";
+ this.displayConfig = {};
+ this.updatedFields = {};
+
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #initConfigNotification() {
+ this._config.notification = {
+ title: "",
+ text: "Some changes have been done in the form. Not saved changes will be lost",
+ type: "notification",
+ display: {
+ visible: () => {
+ return UtilsNew.isNotEmpty(this.updatedFields);
+ },
+ notificationType: "warning",
+ },
+ };
+ }
+
+ #initUser() {
+ // 1. Group contains params: (a) id: e.g. "@admins", (b) userIds: e.g. ["test"]
+ this._user = UtilsNew.objectClone(this.user);
+ // 2. In the update form, we need to manage as well the permissions of this group.
+ // Retrieve ACL permissions. Check if this study group has acl
+ // CAUTION: study does not have acl?
+ // const groupPermissions = this._study?.acl
+ // ?.find(acl => acl.member === this.opencgaSession.user.id)?.groups
+ // ?.find(group => group.id === this.group.id)?.permissions || [];
+ // // 3. Add current permissions and template key to the object group
+ // this.group = {
+ // ...this.group,
+ // permissions: groupPermissions,
+ // template: "", // Fixme: not sure how to retrieve template
+ // };
+ this.initOriginalObjects();
+ }
+
+ #initOriginalObjects() {
+ this._user = UtilsNew.objectClone(this.user);
+ this.updatedFields = {};
+ }
+
+ update(changedProperties) {
+ if ((changedProperties.has("userId") && this.active)) {
+ this.userIdObserver();
+ }
+ if (changedProperties.has("displayConfig")) {
+ this._config = this.getDefaultConfig();
+ if (!this._config?.notification) {
+ this.#initConfigNotification();
+ }
+ }
+ super.update(changedProperties);
+ }
+
+ userIdObserver() {
+ if (this.userId && this.opencgaSession) {
+ const params = {
+ organization: this.organization.id,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.users()
+ .info(this.userId, params)
+ .then(response => {
+ this.user = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.#initOriginalObjects();
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "userInfo", this.study, {}, error);
+ this.#setLoading(false);
+ });
+ }
+ }
+
+ // Uncomment to post-process data-form manipulation
+ // onFieldChange(e) {
+ // debugger
+ // this.updatedFields = e.detail?.updatedFields || {};
+ // this.requestUpdate();
+ // }
+
+ onFieldChange(e) {
+ const param = e.detail.param;
+ this.updatedFields = FormUtils.getUpdatedFields(
+ this.group,
+ this.updatedFields,
+ param,
+ e.detail.value,
+ e.detail.action);
+ if (param === "template") {
+ this._group.template = e.detail.value;
+ }
+ this.requestUpdate();
+ }
+
+ onClear() {
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Discard changes",
+ message: "Are you sure you want to discard the changes made?",
+ ok: () => {
+ this.initOriginalObjects();
+ this.requestUpdate();
+ // We need to dispatch a component clear event
+ LitUtils.dispatchCustomEvent(this, "groupClear", null, {
+ group: this._group,
+ });
+ },
+ });
+ }
+
+ onSubmit() {
+ const paramsAction = {
+ action: "SET"
+ };
+ const studyAclParams = {
+ study: this.studyId,
+ template: this._group.template,
+ // permissions: this._group.permissions,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.studies()
+ .updateAcl(this.groupId, paramsAction, studyAclParams)
+ .then(response => {
+ this.group = UtilsNew.objectClone(response.responses[0].results[0]);
+ this.updatedFields = {};
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User Update`,
+ message: `User ${this.userId} updated correctly`,
+ });
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "updateUser", this.group, {}, error);
+ this.#setLoading(false);
+ });
+ }
+
+ render() {
+ return html `
+ this.onFieldChange(e)}"
+ @clear="${this.onClear}"
+ @submit="${this.onSubmit}">
+
+ `;
+ }
+
+ getDefaultConfig() {
+ return {
+ icon: "fas fa-edit",
+ buttons: {
+ clearText: "Discard Changes",
+ okText: "Update",
+ },
+ display: this.displayConfig,
+ sections: [
+ {
+ title: "Details",
+ elements: [
+ {
+ title: "User Name",
+ field: "name",
+ type: "input-text",
+ display: {
+ helpMessage: "Edit the user name...",
+ },
+ },
+ {
+ title: "User email",
+ field: "email",
+ type: "input-text",
+ display: {
+ helpMessage: "Edit the user email...",
+ },
+ },
+ {
+ title: "Enable user",
+ field: "enabled",
+ type: "toggle-switch",
+ display: {
+ disabled: true,
+ helpMessage: "Coming soon: Enable/Disable a user in an organization",
+ },
+ },
+
+ ],
+ },
+ {
+ title: "Credentials",
+ elements: [
+ {
+ title: "Set password",
+ type: "custom",
+ display: {
+ render: (data, active, opencgaSession) => html`
+
+
+ `,
+ },
+ },
+ {
+ title: "Reset password",
+ type: "custom",
+ display: {
+ render: (data, active, opencgaSession) => html`
+
+
+ `,
+ },
+ },
+ /*
+ {
+ title: "Reset password",
+ field: "pwdReset",
+ type: "toggle-switch",
+ display: {
+ disabled: true,
+ helpMessage: "Coming soon: Force user to reset the password",
+ },
+ },
+ {
+ title: "Expires in",
+ field: "pwdExpiration",
+ type: "input-text",
+ display: {
+ disabled: true,
+ helpMessage: "Coming soon: Enable password expiration",
+ },
+ },
+ */
+ ],
+ },
+ {
+ title: "Permissions",
+ elements: [
+ {
+ title: "Templates",
+ field: "template",
+ type: "toggle-buttons",
+ allowedValues: ["analyst", "view_only"],
+ },
+ // TODO: Implement customised permissions for the group
+ // {
+ // title: "Permissions",
+ // field: "permissions",
+ // type: "toggle-buttons",
+ // },
+ ],
+ },
+ // {
+ // title: "Users",
+ // elements: [
+ // ],
+ // },
+ ],
+ };
+ }
+
+}
+
+customElements.define("user-admin-update", UserAdminUpdate);
diff --git a/src/webcomponents/panel/opencga-panel-filter.js b/src/webcomponents/panel/opencga-panel-filter.js
index 75d4566f74..8e26c3b4c2 100644
--- a/src/webcomponents/panel/opencga-panel-filter.js
+++ b/src/webcomponents/panel/opencga-panel-filter.js
@@ -18,7 +18,6 @@ import {LitElement, html} from "lit";
import UtilsNew from "../../core/utils-new.js";
import PolymerUtils from "../PolymerUtils.js";
-
export default class OpencgaPanelFilter extends LitElement {
constructor() {
diff --git a/src/webcomponents/project/project-create.js b/src/webcomponents/project/project-create.js
index 0a3715db54..1c806947e2 100644
--- a/src/webcomponents/project/project-create.js
+++ b/src/webcomponents/project/project-create.js
@@ -16,10 +16,8 @@
import {LitElement, html} from "lit";
import LitUtils from "../commons/utils/lit-utils.js";
-import FormUtils from "../commons/forms/form-utils.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
import UtilsNew from "../../core/utils-new.js";
-import Types from "../commons/types.js";
export default class ProjectCreate extends LitElement {
@@ -53,7 +51,6 @@ export default class ProjectCreate extends LitElement {
defaultLayout: "horizontal",
buttonOkText: "Create"
};
- this._config = this.getDefaultConfig();
}
#initOriginalObject() {
@@ -64,7 +61,7 @@ export default class ProjectCreate extends LitElement {
},
cellbase: {
url: "https://ws.zettagenomics.com/cellbase",
- version: "v5.1"
+ version: "v5.8"
}
};
this._project = UtilsNew.objectClone(this.project);
@@ -86,72 +83,45 @@ export default class ProjectCreate extends LitElement {
super.update(changedProperties);
}
- onFieldChange(e, field) {
- const param = field || e.detail.param;
- switch (param) {
- case "id":
- case "name":
- case "description":
- case "organism.scientificName":
- case "organism.assembly":
- case "cellbase":
- this.project = {
- ...FormUtils.createObject(
- this.project,
- param,
- e.detail.value
- )
- };
- }
+ onFieldChange(e) {
+ this._project = {...e.detail.data}; // force to refresh the object-list
this.requestUpdate();
}
onClear() {
- const resetForm = () => {
- this.#initOriginalObject();
- this._config = this.getDefaultConfig();
- this.requestUpdate();
- };
- if (!this.displayConfig?.modal) {
- NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
- title: "Clear project",
- message: "Are you sure to clear?",
- ok: () => {
- resetForm();
- },
- });
- } else {
- LitUtils.dispatchCustomEvent(this, "clearProject");
- resetForm();
- }
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Clear project",
+ message: "Are you sure to clear?",
+ ok: () => {
+ this.#initOriginalObject();
+ this._config = this.getDefaultConfig();
+ this.requestUpdate();
+ },
+ });
}
onSubmit() {
const params = {
- includeResult: true
+ includeResult: true,
};
- let project, error;
+ let error;
this.#setLoading(true);
this.opencgaSession.opencgaClient.projects()
- .create(this.project, params)
- .then(response => {
+ .create(this._project, params)
+ .then(() => {
this.#initOriginalObject();
- this._config = this.getDefaultConfig();
-
- project = response.responses[0].results[0];
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
title: "Project Create",
message: "New project created correctly"
});
- LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest");
+ LitUtils.dispatchCustomEvent(this, "projectCreate", {}, {});
+ LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", {}, {});
})
.catch(reason => {
- project = this.project;
error = reason;
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
})
.finally(() => {
- LitUtils.dispatchCustomEvent(this, "projectCreate", project, {}, error);
this.#setLoading(false);
});
}
@@ -163,7 +133,7 @@ export default class ProjectCreate extends LitElement {
return html`
this.onFieldChange(e)}"
@clear="${e => this.onClear(e)}"
@@ -172,20 +142,11 @@ export default class ProjectCreate extends LitElement {
}
getDefaultConfig() {
- return Types.dataFormConfig({
- type: "form",
- display: this.displayConfig || this.displayConfigDefault,
+ return {
+ display: this.displayConfig,
sections: [
{
elements: [
- {
- type: "notification",
- text: "Some changes have been done in the form. Not saved, changes will be lost",
- display: {
- visible: () => !UtilsNew.objectCompare(this.project, this._project),
- notificationType: "warning",
- },
- },
{
name: "Project ID",
field: "id",
@@ -239,8 +200,12 @@ export default class ProjectCreate extends LitElement {
title: "Version",
field: "cellbase.version",
type: "select",
- allowedValues: ["v5.0", "v5.1"],
- defaultValue: "v5.1",
+ // FIXME Vero 20240712: Waiting for Nacho's advise
+ // Can they be queried? In cellbase more versions are responding:
+ // 5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.8
+ // https://ws.zettagenomics.com/cellbase/webservices/#!/Gene/getInfo_1
+ allowedValues: ["v5.0", "v5.1", "v5.2", "v5.8"],
+ defaultValue: "v5.8",
display: {
// placeholder: "Add version"
}
@@ -259,7 +224,7 @@ export default class ProjectCreate extends LitElement {
]
}
]
- });
+ };
}
}
diff --git a/src/webcomponents/project/project-update.js b/src/webcomponents/project/project-update.js
index fcae974962..6356384293 100644
--- a/src/webcomponents/project/project-update.js
+++ b/src/webcomponents/project/project-update.js
@@ -80,7 +80,10 @@ export default class ProjectUpdate extends LitElement {
this.projectIdObserver();
}
if (changedProperties.has("displayConfig")) {
- this.displayConfig = {...this.displayConfigDefault, ...this.displayConfig};
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig
+ };
this._config = this.getDefaultConfig();
}
super.update(changedProperties);
@@ -165,7 +168,7 @@ export default class ProjectUpdate extends LitElement {
const params = {
includeResult: true
};
- let project, error;
+ let error;
this.#setLoading(true);
this.opencgaSession.opencgaClient.projects()
.update(this.project?.fqn, this.updateParams, params)
@@ -177,15 +180,14 @@ export default class ProjectUpdate extends LitElement {
title: "Project Update",
message: "Project updated correctly"
});
- LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest");
+ LitUtils.dispatchCustomEvent(this, "sessionUpdateRequest", this._project, {}, error);
})
.catch(reason => {
- project = this.project;
error = reason;
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
})
.finally(() => {
- LitUtils.dispatchCustomEvent(this, "projectUpdate", this.project, {}, error);
+ // LitUtils.dispatchCustomEvent(this, "projectUpdate", project, {}, error);
this.#setLoading(false);
});
}
diff --git a/src/webcomponents/project/projects-admin.js b/src/webcomponents/project/projects-admin.js
index 8f55543463..58a30c4031 100644
--- a/src/webcomponents/project/projects-admin.js
+++ b/src/webcomponents/project/projects-admin.js
@@ -20,7 +20,7 @@ import OpencgaCatalogUtils from "../../core/clients/opencga/opencga-catalog-util
import {guardPage} from "../commons/html-utils.js";
import "../commons/tool-header.js";
import "../study/study-form.js";
-import "../study/study-create.js";
+import "../study/admin/study-create.js";
import "./project-create.js";
import "./project-update.js";
@@ -218,7 +218,7 @@ export default class ProjectsAdmin extends LitElement {
let content;
switch (id) {
case "createProject":
- content=html`
+ content = html`
{
- return html`
- params.onClickRow(e, "sample")}"
- @sampleUpdate="${e => params.onComponentUpdate(e, "sample")}"
- @settingsUpdate="${() => this.onSettingsUpdate()}">
-
+ render: params => html`
+ params.onClickRow(e)}"
+ @sampleUpdate="${e => params.onComponentUpdate(e)}"
+ @settingsUpdate="${() => this.onSettingsUpdate()}">
+
+ ${params?.detail ? html`
+ .sampleId="${params.detail?.id}">
- `;
- }
+ ` : nothing}
+ `,
},
{
id: "facet-tab",
@@ -154,7 +154,8 @@ export default class SampleBrowser extends LitElement {
.active="${params.active}"
.query="${params.facetQuery}"
.data="${params.facetResults}">
- `
+
+ `,
}
],
filter: {
diff --git a/src/webcomponents/sample/sample-cancer-variant-stats-browser.js b/src/webcomponents/sample/sample-cancer-variant-stats-browser.js
index 748610dd19..56802ebb21 100644
--- a/src/webcomponents/sample/sample-cancer-variant-stats-browser.js
+++ b/src/webcomponents/sample/sample-cancer-variant-stats-browser.js
@@ -496,6 +496,12 @@ export default class SampleCancerVariantStatsBrowser extends LitElement {
return;
}
+ const hasWritePermission = OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ "WRITE_CLINICAL_ANALYSIS",
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions);
+
return html`
${this.sample && this._config.showTitle ? html`
- ${OpencgaCatalogUtils.checkPermissions(this.opencgaSession.study, this.opencgaSession.user.id, "WRITE_CLINICAL_ANALYSIS") ? html`
+ ${hasWritePermission ? html`
@@ -548,7 +554,6 @@ export default class SampleCancerVariantStatsBrowser extends LitElement {
` : null}
-
`
// }
};
+
+ this.permissionID = WebUtils.getPermissionID(this.toolbarConfig.resource, "WRITE");
}
- fetchClinicalAnalysis(rows, casesLimit) {
+ fetchClinicalAnalysis(rows, individuals, casesLimit) {
if (rows && rows.length > 0) {
return this.opencgaSession.opencgaClient.clinical()
.search({
- individual: rows.map(sample => sample.individualId).join(","),
+ individual: individuals,
study: this.opencgaSession.study.fqn,
include: "id,proband.id,family.members",
limit: casesLimit * 10,
@@ -229,7 +233,14 @@ export default class SampleGrid extends LitElement {
.then(response => {
sampleResponse = response;
// Fetch clinical analysis to display the Case ID
- return this.fetchClinicalAnalysis(sampleResponse?.responses?.[0]?.results || [], casesLimit);
+ const samples = sampleResponse?.responses?.[0]?.results;
+ const individuals = (samples || [])
+ .map(sample => sample.individualId)
+ .filter(individualId => !!individualId)
+ .join(",");
+ if (individuals) {
+ return this.fetchClinicalAnalysis(samples || [], individuals, casesLimit);
+ }
})
.then(() => {
// Prepare data for columns extensions
@@ -345,7 +356,7 @@ export default class SampleGrid extends LitElement {
case "download-json":
UtilsNew.downloadData([JSON.stringify(row, null, "\t")], row.id + ".json");
break;
- case "qualityControl":
+ case "quality-control":
alert("Not implemented yet");
break;
}
@@ -431,67 +442,78 @@ export default class SampleGrid extends LitElement {
title: "Actions",
field: "actions",
align: "center",
- formatter: (value, row) => `
-
-
-
- Actions
-
-
+
+ `;
+ },
events: {
"click a": this.onActionClick.bind(this)
},
diff --git a/src/webcomponents/sample/sample-update.js b/src/webcomponents/sample/sample-update.js
index 4d0c075ceb..c88d83b800 100644
--- a/src/webcomponents/sample/sample-update.js
+++ b/src/webcomponents/sample/sample-update.js
@@ -206,14 +206,6 @@ export default class SampleUpdate extends LitElement {
placeholder: "Add an ID",
}
},
- {
- title: "Name",
- field: "status.name",
- type: "input-text",
- display: {
- placeholder: "Add source name"
- }
- },
{
title: "Description",
field: "status.description",
diff --git a/src/webcomponents/sample/sample-variant-stats-browser.js b/src/webcomponents/sample/sample-variant-stats-browser.js
index 0542ec3ba7..a763cbb379 100644
--- a/src/webcomponents/sample/sample-variant-stats-browser.js
+++ b/src/webcomponents/sample/sample-variant-stats-browser.js
@@ -407,7 +407,11 @@ export default class SampleVariantStatsBrowser extends LitElement {
modalTitle: "Save Variant Stats",
modalButtonIcon: "fas fa-save",
modalButtonClassName: "btn btn-primary",
- modalDisabled: !OpencgaCatalogUtils.checkPermissions(this.opencgaSession.study, this.opencgaSession.user.id, "WRITE_CLINICAL_ANALYSIS"),
+ modalDisabled: !OpencgaCatalogUtils.getStudyEffectivePermission(
+ this.opencgaSession.study,
+ this.opencgaSession.user.id,
+ "WRITE_CLINICAL_ANALYSIS",
+ this.opencgaSession?.organization?.configuration?.optimizations?.simplifyPermissions),
labelWidth: 3,
labelAlign: "right",
defaultValue: "",
diff --git a/src/webcomponents/sample/sample-variant-stats-view.js b/src/webcomponents/sample/sample-variant-stats-view.js
index b644146dc3..6f20dcf8b9 100644
--- a/src/webcomponents/sample/sample-variant-stats-view.js
+++ b/src/webcomponents/sample/sample-variant-stats-view.js
@@ -194,31 +194,31 @@ class SampleVariantStatsView extends LitElement {
return html`
No Variant Stats found.
-
`;
+
+ `;
}
return html`
- ${this.sample ?
- html`
-
-
Select Variant Stat
-
-
-
-
-
` : nothing
- }
-
+ ${this.sample ? html`
+
+
Select Variant Stat
+
+
+
+
+
+ ` : nothing}
diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js
index f081202e4c..01dfe18c6d 100644
--- a/src/webcomponents/sample/sample-view.js
+++ b/src/webcomponents/sample/sample-view.js
@@ -194,7 +194,10 @@ export default class SampleView extends LitElement {
},
{
title: "Individual ID",
- field: "individualId"
+ field: "individualId",
+ display: {
+ defaultValue: "No individuals or no clinical analyses",
+ }
},
{
title: "Files",
@@ -217,7 +220,7 @@ export default class SampleView extends LitElement {
title: "Status",
type: "complex",
display: {
- template: "${internal.status.name} (${internal.status.date})",
+ template: "${internal.status.id} (${internal.status.date})",
format: {
"internal.status.date": date => UtilsNew.dateFormatter(date),
}
diff --git a/src/webcomponents/study/admin/study-admin-audit.js b/src/webcomponents/study/admin/study-admin-audit.js
index 9fd0a6f1e3..8cacd90148 100644
--- a/src/webcomponents/study/admin/study-admin-audit.js
+++ b/src/webcomponents/study/admin/study-admin-audit.js
@@ -235,7 +235,7 @@ export default class StudyAdminAudit extends LitElement {
},
{
title: "Status",
- field: "status.name",
+ field: "status.id",
},
];
}
diff --git a/src/webcomponents/study/admin/study-admin-iva.js b/src/webcomponents/study/admin/study-admin-iva.js
index cbb7d34eab..a69d3cd295 100644
--- a/src/webcomponents/study/admin/study-admin-iva.js
+++ b/src/webcomponents/study/admin/study-admin-iva.js
@@ -21,6 +21,7 @@ import "../../commons/tool-settings-update.js";
import LitUtils from "../../commons/utils/lit-utils";
import NotificationUtils from "../../commons/utils/notification-utils";
import UtilsNew from "../../../core/utils-new";
+import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils";
export default class StudyAdminIva extends LitElement {
@@ -36,6 +37,9 @@ export default class StudyAdminIva extends LitElement {
static get properties() {
return {
+ organizationId: {
+ type: String,
+ },
studyId: {
type: String,
},
@@ -61,6 +65,9 @@ export default class StudyAdminIva extends LitElement {
}
update(changedProperties) {
+ if (changedProperties.has("organizationId")) {
+ this.organizationIdObserver();
+ }
if (changedProperties.has("studyId")) {
this.studyIdObserver();
}
@@ -76,9 +83,31 @@ export default class StudyAdminIva extends LitElement {
}
/* -- OBSERVER METHODS -- */
- opencgaSessionObserver() {
- this.study = this.opencgaSession.study;
- this._config = this.getDefaultConfig();
+ organizationIdObserver() {
+ // FIXME Vero: on creating a new group, for instance,
+ // the session is updated but the org id does not change.
+ // I need to get the organization info again to refresh the grid.
+ // For now, I will query org info only with property opencgaSession change.
+ // TO think about it.
+ // if (this.organizationId && this.opencgaSession) {
+ if (this.organizationId || this.opencgaSession) {
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.organization()
+ .info(this.opencgaSession.organization.id)
+ .then(response => {
+ this.organization = UtilsNew.objectClone(response.responses[0].results[0]);
+ })
+ .catch(reason => {
+ // this.organization = {};
+ error = reason;
+ console.error(reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "organizationInfo", this.organization, {}, error);
+ this.#setLoading(false);
+ });
+ }
}
studyIdObserver() {
@@ -102,9 +131,27 @@ export default class StudyAdminIva extends LitElement {
}
}
+ opencgaSessionObserver() {
+ this.study = this.opencgaSession.study;
+ this._config = this.getDefaultConfig();
+ }
+
// --- RENDER METHOD ---
render() {
- return html`
+ if (this.opencgaSession.study && this.organization) {
+ if (!OpencgaCatalogUtils.isOrganizationAdmin(this.organization, this.opencgaSession.user.id) &&
+ !OpencgaCatalogUtils.isAdmin(this.opencgaSession.study, this.opencgaSession.user.id)) {
+ return html`
+
+
+
Restricted access
+ The page you are trying to access has restricted access.
+ Please refer to your system administrator.
+
+ `;
+ }
+
+ return html`
`;
+ }
}
getDefaultConfig() {
diff --git a/src/webcomponents/study/admin/study-admin-permissions.js b/src/webcomponents/study/admin/study-admin-permissions.js
index 5cd5f4110d..087500d182 100644
--- a/src/webcomponents/study/admin/study-admin-permissions.js
+++ b/src/webcomponents/study/admin/study-admin-permissions.js
@@ -70,11 +70,11 @@ export default class StudyAdminPermissions extends LitElement {
active: true,
render: (study, active, opencgaSession) => {
return html`
-
- `;
+ `;
}
},
{
diff --git a/src/webcomponents/study/admin/study-admin-users.js b/src/webcomponents/study/admin/study-admin-users.js
index b91f7f408e..8ea61f04ea 100644
--- a/src/webcomponents/study/admin/study-admin-users.js
+++ b/src/webcomponents/study/admin/study-admin-users.js
@@ -99,9 +99,11 @@ export default class StudyAdminUsers extends LitElement {
studyObserver() {
if (this.study) {
+ // FIXME!!!!!: admin/owner organization. Ask Pedro again
this.owner = this.study.fqn.split("@")[0];
this.groupsMap = new Map();
- this.opencgaSession.opencgaClient.studies().groups(this.study.fqn)
+ this.opencgaSession.opencgaClient.studies()
+ .groups(this.study.fqn)
.then(response => {
const groups = response.responses[0].results;
// Remove in OpenCGA 2.1
@@ -157,14 +159,36 @@ export default class StudyAdminUsers extends LitElement {
}
groupFormatter(value, row) {
- // const isOwner = this.field.owner === this.field.loggedUser;
- const isOwner = OpencgaCatalogUtils.checkUserAccountView(this.field.owner, this.field.loggedUser);
+ // NOTE:
+ // - Only organization owner/admin can manage study admins
+ // - Study admins can add/remove users to the study and groups (except group admins)
+ // - Regular users (no org owner/admin, no study admin) can not access this site
+ const isOrganizationAdmin = OpencgaCatalogUtils.isOrganizationAdmin(this.field.opencgaSession.organization, row.id);
+ const isLoggedUserOrganizationAdmin = OpencgaCatalogUtils.isOrganizationAdmin(this.field.opencgaSession.organization, this.field.loggedUser);
+ const isStudyAdmin = OpencgaCatalogUtils.isAdmin(this.field.study, row.id);
+ const isLoggedUserStudyAdmin = OpencgaCatalogUtils.isAdmin(this.field.study, this.field.loggedUser);
const checked = this.field.groupsMap?.get(this.field.groupId).findIndex(e => e.id === row.id) !== -1;
if (this.field.groupId === "@admins") {
- return `
`;
+ if (isLoggedUserOrganizationAdmin) {
+ // return `
`;
+ return `
`;
+ } else {
+ return `
`;
+ }
} else {
- return `
`;
+ // return `
`;
+ return `
`;
}
+ // Todo: the checked attribute is not working. Investigate why and fix
+ // return `
+ //
+ //
+ //
+ // `;
}
async onCheck(e, value, row, group, context) {
@@ -189,9 +213,8 @@ export default class StudyAdminUsers extends LitElement {
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
message: messageAlert,
});
- // this.notifyStudyUpdateRequest();
LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn);
- this.requestUpdate();
+ // this.requestUpdate();
} catch (error) {
// console.error("Message error: ", error);
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
@@ -212,7 +235,10 @@ export default class StudyAdminUsers extends LitElement {
groupId: group,
groupsMap: this.groupsMap,
owner: this.owner,
- loggedUser: this.opencgaSession?.user?.id
+ loggedUser: this.opencgaSession?.user?.id,
+ // Terrible two hacks for having access to the organization
+ opencgaSession: this.opencgaSession,
+ study: this.study,
},
rowspan: 1,
colspan: 1,
@@ -231,9 +257,6 @@ export default class StudyAdminUsers extends LitElement {
{
title: "User Name",
field: "name",
- formatter: (value, row) => {
- return value === this.owner ? `
${value} (owner) ` : value;
- },
rowspan: 2,
colspan: 1,
sortable: true
@@ -252,8 +275,8 @@ export default class StudyAdminUsers extends LitElement {
},
{
title: "Created on",
- field: "account.creationDate",
- formatter: (value, row) => {
+ field: "creationDate",
+ formatter: value => {
return value ? UtilsNew.dateFormatter(value) : "NA";
},
rowspan: 2,
@@ -334,20 +357,18 @@ export default class StudyAdminUsers extends LitElement {
console.log("User already exists in the study");
return;
}
+ const params = {
+ study: this.study.fqn,
+ template: "",
+ permissions: "",
+ };
- this.opencgaSession.opencgaClient.studies().updateUsers(this.study.fqn, "@members", {users: [this.addUserId]}, {action: "ADD"})
- .then(res => {
+ this.opencgaSession.opencgaClient.studies()
+ // .updateUsers(this.study.fqn, "@members", {users: [this.addUserId]}, {action: "ADD"})
+ .updateAcl(this.addUserId, {action: "ADD"}, params)
+ .then(() => {
this.addUserId = "";
- this.requestUpdate();
-
- this.dispatchEvent(new CustomEvent("studyUpdateRequest", {
- detail: {
- value: this.study.fqn
- },
- bubbles: true,
- composed: true
- }));
-
+ LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn);
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
message: "User added.",
});
@@ -373,20 +394,31 @@ export default class StudyAdminUsers extends LitElement {
onRemoveUserTest(e, row) {
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
title: `Remove user '${row.id}'`,
- message: `Are you sure you want to remove user
'${row.id}' ?`,
+ message: `Are you sure you want to remove user
'${row.id}' from the study ${this.study.fqn}?`,
display: {
okButtonText: "Yes, remove user",
},
ok: () => {
- this.opencgaSession.opencgaClient.studies().updateUsers(this.study.fqn, "@members", {users: [row.id]}, {action: "REMOVE"})
+ // this.opencgaSession.opencgaClient.studies()
+ // .updateUsers(this.study.fqn, "@members", {users: [row.id]}, {action: "REMOVE"})
+ const params= {
+ includeResult: true,
+ action: "REMOVE",
+ };
+ const data = {
+ users: [row.id],
+ };
+ this.opencgaSession.opencgaClient.studies()
+ .updateGroupsUsers(this.study.fqn, "@members", data, params)
.then(() => {
- this.requestUpdate();
LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn);
- this.requestUpdate();
-
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
message: "User removed",
});
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User in Group Update`,
+ message: `User ${row.id} REMOVED from @members in study ${this.study.id} correctly`,
+ });
}).catch(err => {
console.error(err);
});
@@ -403,16 +435,8 @@ export default class StudyAdminUsers extends LitElement {
this.opencgaSession.opencgaClient.studies().updateUsers(this.study.fqn, "@members", {users: userIds}, {action: "REMOVE"})
.then(res => {
this.removeUserSet = new Set();
- this.requestUpdate();
-
- this.dispatchEvent(new CustomEvent("studyUpdateRequest", {
- detail: {
- value: this.study.fqn
- },
- bubbles: true,
- composed: true
- }));
-
+ // this.requestUpdate();
+ LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn);
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
message: "User removed",
});
@@ -436,8 +460,7 @@ export default class StudyAdminUsers extends LitElement {
this.opencgaSession.opencgaClient.studies().updateGroups(this.study.fqn, {id: this.addGroupId}, {action: "ADD"})
.then(res => {
this.addGroupId = "";
- this.requestUpdate();
- // this.notifyStudyUpdateRequest();
+ // this.requestUpdate();
LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn);
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
@@ -492,20 +515,8 @@ export default class StudyAdminUsers extends LitElement {
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_INFO, {
message: messageAlert,
});
-
- // this.notifyStudyUpdateRequest();
LitUtils.dispatchCustomEvent(this, "studyUpdateRequest", this.study.fqn);
- this.requestUpdate();
- }
-
- notifyStudyUpdateRequest() {
- this.dispatchEvent(new CustomEvent("studyUpdateRequest", {
- detail: {
- value: this.study.fqn
- },
- bubbles: true,
- composed: true
- }));
+ // this.requestUpdate();
}
render() {
@@ -543,9 +554,16 @@ export default class StudyAdminUsers extends LitElement {
-
`;
}
diff --git a/src/webcomponents/user/user-password-change.js b/src/webcomponents/user/user-password-change.js
index 70a514cc8b..af0140f566 100644
--- a/src/webcomponents/user/user-password-change.js
+++ b/src/webcomponents/user/user-password-change.js
@@ -1,7 +1,8 @@
import {LitElement, html} from "lit";
-import UtilsNew from "../../core/utils-new.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
import "../commons/forms/data-form.js";
+import LitUtils from "../commons/utils/lit-utils";
+import UtilsNew from "../../core/utils-new";
export default class UserPasswordChange extends LitElement {
@@ -16,55 +17,101 @@ export default class UserPasswordChange extends LitElement {
static get properties() {
return {
+ userId: {
+ type: String,
+ },
opencgaSession: {
type: Object
},
+ organizationId: {
+ type: String,
+ },
+ displayConfig: {
+ type: Object
+ },
};
}
#init() {
- this.updateParams = {};
- this.config = this.getDefaultConfig();
+ this._password = {};
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ titleWidth: 3,
+ titleStyle: "color: var(--main-bg-color);margin-bottom:16px;font-weight:bold;",
+ defaultLayout: "horizontal",
+ buttonOkText: "Change password",
+ };
+ this._config = this.getDefaultConfig();
+ }
+
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
+ }
+
+ #initOriginalObjects() {
+ this._password = {};
+ this._config = this.getDefaultConfig();
+ this.requestUpdate();
+ }
+
+ update(changedProperties) {
+ if (changedProperties.has("displayConfig")) {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig
+ };
+ this._config = this.getDefaultConfig();
+ }
+ super.update(changedProperties);
}
onFieldChange(e) {
- this.updateParams[e.detail.param] = e.detail.value;
- this.updateParams = {...this.updateParams};
+ this._password = {...e.detail.data}; // force to refresh the object-list
this.requestUpdate();
}
onSubmit() {
- this.opencgaSession.opencgaClient.getClient("user").password({
- user: this.opencgaSession.user.id,
- password: this.updateParams.oldPassword,
- newPassword: this.updateParams.newPassword,
- })
+ const params = {
+ user: this.userId,
+ password: this._password.oldPassword,
+ newPassword: this._password.newPassword,
+ organizationId: this.organizationId,
+ };
+ let error;
+ this.#setLoading(true);
+ this.opencgaSession.opencgaClient.users()
+ .password(params)
.then(() => {
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
message: "Your password has been changed",
});
- this.onClear();
+ LitUtils.dispatchCustomEvent(this, "userUpdate");
+ this.#initOriginalObjects();
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, error);
})
- .catch(response => {
- // console.error(response);
- NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, response);
+ .finally(() => {
+ this.#setLoading(false);
});
}
onClear() {
- this.updateParams = {};
- // Terrible hack to reset the values in the input elements
- // eslint-disable-next-line no-param-reassign
- Array.from(this.querySelectorAll("input")).forEach(el => el.value = "");
- this.requestUpdate();
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_CONFIRMATION, {
+ title: "Clear password",
+ message: "Are you sure to clear?",
+ ok: () => this.#initOriginalObjects(),
+ });
}
render() {
// TODO: check if opencgaSession has been provided
return html`
this.onFieldChange(e)}"
@submit="${() => this.onSubmit()}"
@clear="${() => this.onClear()}">
@@ -74,45 +121,47 @@ export default class UserPasswordChange extends LitElement {
getDefaultConfig() {
return {
- icon: "",
- display: {
- buttonOkText: "Change password",
- },
- // validation: {
- // validate: () => this.updateParams.newPassword === this.updateParams.confirmNewPassword,
- // message: "New passwords do not match",
+ // icon: "",
+ // display: {
+ // buttonOkText: "Change password",
// },
+ // // validation: {
+ // // validate: () => this.updateParams.newPassword === this.updateParams.confirmNewPassword,
+ // // message: "New passwords do not match",
+ // // },
+ title: "Change Password",
+ display: this.displayConfig || this.displayConfigDefault,
sections: [
{
// title: "Change password",
// description: "Here you can change your password. Make sure it has at least 8 characters.",
elements: [
- {
- type: "text",
- text: "Change password",
- display: {
- icon: "user-shield",
- textClassName: "h2",
- textStyle: "color: var(--main-bg-color);margin-bottom:16px;font-weight:bold;",
- },
- },
- {
- type: "text",
- text: `
- Here you can change your password. We recommend that your new password has at least
- 8 characters long and uses uppercase and lowercase letters and numbers.
- `,
- display: {
- textStyle: "margin-bottom:16px;",
- },
- },
+ // {
+ // type: "text",
+ // text: "Change password",
+ // display: {
+ // icon: "user-shield",
+ // textClassName: "h2",
+ // textStyle: "color: var(--main-bg-color);margin-bottom:16px;font-weight:bold;",
+ // },
+ // },
+ // {
+ // type: "text",
+ // text: `
+ // Here you can change your password. We recommend that your new password has at least
+ // 8 characters long and uses uppercase and lowercase letters and numbers.
+ // `,
+ // display: {
+ // textStyle: "margin-bottom:16px;",
+ // },
+ // },
{
title: "Current password",
type: "input-password",
field: "oldPassword",
defaultValue: "",
validation: {
- validate: () => !!this.updateParams.oldPassword,
+ validate: () => !!this._password.oldPassword,
message: "Please enter your existing password.",
},
},
@@ -122,7 +171,7 @@ export default class UserPasswordChange extends LitElement {
field: "newPassword",
defaultValue: "",
validation: {
- validate: () => !!this.updateParams.newPassword,
+ validate: () => !!this._password.newPassword,
message: "Your new password can not be empty.",
},
},
@@ -133,7 +182,7 @@ export default class UserPasswordChange extends LitElement {
defaultValue: "",
validation: {
validate: () => {
- return !!this.updateParams.confirmNewPassword && this.updateParams.confirmNewPassword === this.updateParams.newPassword;
+ return !!this._password.confirmNewPassword && this._password.confirmNewPassword === this._password.newPassword;
},
message: "New passwords do not match.",
},
diff --git a/src/webcomponents/user/user-password-reset.js b/src/webcomponents/user/user-password-reset.js
index 3773f0c89f..790740ad58 100644
--- a/src/webcomponents/user/user-password-reset.js
+++ b/src/webcomponents/user/user-password-reset.js
@@ -1,12 +1,28 @@
+/**
+ * Copyright 2015-2024 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
import {LitElement, html} from "lit";
import LitUtils from "../commons/utils/lit-utils.js";
import NotificationUtils from "../commons/utils/notification-utils.js";
-
export default class UserPasswordReset extends LitElement {
constructor() {
super();
+
this.#init();
}
@@ -16,83 +32,129 @@ export default class UserPasswordReset extends LitElement {
static get properties() {
return {
+ user: {
+ type: Object,
+ },
opencgaSession: {
type: Object
},
+ displayConfig: {
+ type: Object
+ },
};
}
#init() {
- this.hasEmptyUser = false;
+ this._user = {};
+ this.displayConfigDefault = {
+ style: "margin: 10px",
+ titleWidth: 3,
+ titleStyle: "color: var(--main-bg-color);margin-bottom:16px;font-weight:bold;",
+ defaultLayout: "horizontal",
+ buttonOkText: "Reset password",
+ };
+ this._config = this.getDefaultConfig();
}
- redirect(to) {
- LitUtils.dispatchCustomEvent(this, "redirect", null, {hash: to});
+ #setLoading(value) {
+ this.isLoading = value;
+ this.requestUpdate();
}
- onSubmit(e) {
- e.preventDefault();
- const user = (this.querySelector("#user").value || "").trim();
+ #initOriginalObjects() {
+ this._config = this.getDefaultConfig();
+ this.requestUpdate();
+ }
- // Check for empty user ID
- this.hasEmptyUser = user.length === 0;
- if (this.hasEmptyUser) {
- return this.requestUpdate();
+ update(changedProperties) {
+ if (changedProperties.has("displayConfig") || changedProperties.has("user")) {
+ this.displayConfig = {
+ ...this.displayConfigDefault,
+ ...this.displayConfig
+ };
+ this._config = this.getDefaultConfig();
}
+ super.update(changedProperties);
+ }
- // Reset password mockup
- // TODO: call openCGA to the correct endpoint
- Promise.resolve().then(() => {
- NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
- "message": "We have just send you an email with the new password.",
+ onSubmit() {
+ // QUESTION:
+ // - TASK-1667, includeResult
+ // - JS client do not have argument for params, only user
+ let error;
+ this.#setLoading(true);
+ // Reset password
+ this.opencgaSession.opencgaClient.users()
+ .resetPassword(this.user.id)
+ .then(() => {
+ this.#initOriginalObjects();
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
+ title: `User Reset Password`,
+ message: `User ${this.user.id} password reset correctly`,
+ });
+ })
+ .catch(reason => {
+ error = reason;
+ NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_RESPONSE, reason);
+ })
+ .finally(() => {
+ LitUtils.dispatchCustomEvent(this, "userUpdate", this.user.id, {}, error);
+ this.#setLoading(false);
});
- this.querySelector("#user").value = "";
- this.hasEmptyUser = false;
- this.requestUpdate();
- });
- }
-
- // Handle keyup event --> check for enter key to submit the form
- onKeyUp(e) {
- if (e.key === "Enter") {
- return this.onSubmit(e);
- }
}
render() {
+ // TODO: check if opencgaSession has been provided
return html`
-
-
-
-
-
- Reset your password
-
-
-
- Please enter your user ID and we will send you an email with your password reset link.
-
-
-
this.onSubmit(e)}">
- Reset Password
-
-
-
-
-
+ this.onSubmit()}"
+ @clear="${() => this.onClear()}">
+
`;
}
+ getDefaultConfig() {
+ return {
+ title: "Reset Password",
+ display: this.displayConfig || this.displayConfigDefault,
+ sections: [
+ {
+ title: `Do you really want to reset ${this.user?.id}'s password?`,
+ elements: [
+ {
+ type: "notification",
+ text: `The user ${this.user?.id} will receive an email with a temporary password`,
+ display: {
+ visible: true,
+ icon: "fas fa-exclamation-triangle",
+ notificationType: "error",
+ },
+ },
+ {
+ title: "User ID",
+ field: "id",
+ type: "input-text",
+ display: {
+ disabled: true,
+ }
+ },
+ {
+ title: "Email",
+ field: "email",
+ type: "input-text",
+ display: {
+ disabled: true,
+ }
+ },
+ ],
+ },
+ ],
+ };
+ }
+
}
customElements.define("user-password-reset", UserPasswordReset);
diff --git a/src/webcomponents/user/user-profile.js b/src/webcomponents/user/user-profile.js
index 0b29cf2040..c0848f9aab 100644
--- a/src/webcomponents/user/user-profile.js
+++ b/src/webcomponents/user/user-profile.js
@@ -95,6 +95,8 @@ export default class UserProfile extends LitElement {
render: (data, active, opencgaSession) => html`
diff --git a/src/webcomponents/variant/annotation/variant-annotation-clinical-view.js b/src/webcomponents/variant/annotation/variant-annotation-clinical-view.js
index 4da83bc067..91d4af34fe 100644
--- a/src/webcomponents/variant/annotation/variant-annotation-clinical-view.js
+++ b/src/webcomponents/variant/annotation/variant-annotation-clinical-view.js
@@ -22,7 +22,6 @@ export default class VariantAnnotationClinicalView extends LitElement {
constructor() {
super();
-
this.#init();
}
@@ -44,6 +43,10 @@ export default class VariantAnnotationClinicalView extends LitElement {
assembly: {
type: String
},
+ groupedBySource: {
+ type: Object,
+ state: true
+ },
opencgaSession: {
type: Object
},
@@ -52,16 +55,21 @@ export default class VariantAnnotationClinicalView extends LitElement {
#init() {
this._prefix = UtilsNew.randomString(8);
+ this.traitAssociation = [];
+ this.groupedBySource = {};
}
- firstUpdated() {
- this.renderVariantTraitTable();
- }
-
- updated(changedProperties) {
+ update(changedProperties) {
if (changedProperties.has("variantId")) {
this.variantIdObserver();
}
+ if (changedProperties.has("traitAssociation")) {
+ this.traitAssociationObserver();
+ }
+ super.update(changedProperties);
+ }
+
+ updated(changedProperties) {
if (changedProperties.has("traitAssociation")) {
this.renderVariantTraitTable();
}
@@ -80,6 +88,13 @@ export default class VariantAnnotationClinicalView extends LitElement {
}
}
+ traitAssociationObserver() {
+ this.groupedBySource = {};
+ if (this.traitAssociation?.length > 0) {
+ this.groupedBySource = UtilsNew.groupBy(this.traitAssociation, "source.name");
+ }
+ }
+
idFormatter(value, row) {
let html = "-";
if (row) {
@@ -94,6 +109,9 @@ export default class VariantAnnotationClinicalView extends LitElement {
case "cosmic":
html = `${row.id} `;
break;
+ case "hgmd":
+ html = `${row.id}`;
+ break;
}
}
return html;
@@ -101,11 +119,13 @@ export default class VariantAnnotationClinicalView extends LitElement {
sourceFormatter(value) {
if (value) {
- switch (value.name) {
+ switch (value.name?.toLowerCase()) {
case "clinvar":
- return "ClinVar";
+ return `ClinVar (${value.version})`;
case "cosmic":
- return "COSMIC";
+ return `COSMIC (${value.version})`;
+ case "hgmd":
+ return `HGMD (${value.version})`;
default:
console.error("Source not valid: " + value.name);
return "-";
@@ -162,14 +182,6 @@ export default class VariantAnnotationClinicalView extends LitElement {
let result, data;
const name = row?.source?.name;
switch (name.toLowerCase()) {
- case "cosmic":
- data = row?.additionalProperties?.filter(item => item.id === "FATHMM_PREDICTION");
- result = `
-
- FATHMM Prediction: ${Number.parseFloat(data?.[0]?.value) || "NA"}
-
- `;
- break;
case "clinvar":
data = row?.additionalProperties?.filter(item => item.name === "ClinicalSignificance_in_source_file");
// Prepare star rating HTML
@@ -191,6 +203,14 @@ export default class VariantAnnotationClinicalView extends LitElement {
`;
break;
+ case "cosmic":
+ data = row?.additionalProperties?.filter(item => item.id === "FATHMM_PREDICTION");
+ result = `
+
+ FATHMM Prediction: ${Number.parseFloat(data?.[0]?.value) || "NA"}
+
+ `;
+ break;
default:
result = "-";
break;
@@ -236,15 +256,76 @@ export default class VariantAnnotationClinicalView extends LitElement {
}
renderVariantTraitTable() {
- if (!this.traitAssociation) {
- this.traitAssociation = [];
- }
+ $("#" + this._prefix + "ClinvarTraitAssociation").bootstrapTable("destroy");
+ $("#" + this._prefix + "ClinvarTraitAssociation").bootstrapTable({
+ data: this.groupedBySource.clinvar,
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ pagination: false,
+ columns: [
+ {
+ title: "ID",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.idFormatter,
+ halign: "center"
+ },
+ {
+ title: "Source",
+ field: "source",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.sourceFormatter,
+ halign: "center"
+ },
+ {
+ title: "Gene",
+ field: "genomicFeatures",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.geneFormatter,
+ halign: "center"
+ },
+ {
+ title: "Heritable Traits",
+ field: "heritableTraits",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.heritableTraitsFormatter,
+ halign: "center"
+ },
+ {
+ title: "Mode of Inheritance",
+ field: "heritableTraits",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.inheritanceModeFormatter,
+ halign: "center"
+ },
+ {
+ title: "Clinical Significance",
+ field: "variantClassification",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.clinicalSignificanceFormatter,
+ halign: "center"
+ },
+ {
+ title: "Origin Type",
+ field: "alleleOrigin",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.alleleOriginFormatter,
+ halign: "center"
+ },
+ ]
+ });
- $("#" + this._prefix + "VariantTraitAssociation").bootstrapTable("destroy");
- $("#" + this._prefix + "VariantTraitAssociation").bootstrapTable({
+ $("#" + this._prefix + "CosmicTraitAssociation").bootstrapTable("destroy");
+ $("#" + this._prefix + "CosmicTraitAssociation").bootstrapTable({
+ data: this.groupedBySource.cosmic,
theadClasses: "table-light",
buttonsClass: "light",
- data: this.traitAssociation,
pagination: false,
columns: [
[
@@ -271,22 +352,6 @@ export default class VariantAnnotationClinicalView extends LitElement {
formatter: this.geneFormatter,
halign: "center"
},
- {
- title: "Heritable Traits",
- field: "heritableTraits",
- rowspan: 2,
- colspan: 1,
- formatter: this.heritableTraitsFormatter,
- halign: "center"
- },
- {
- title: "Mode of Inheritance",
- field: "heritableTraits",
- rowspan: 2,
- colspan: 1,
- formatter: this.inheritanceModeFormatter,
- halign: "center"
- },
{
title: "Clinical Significance",
field: "variantClassification",
@@ -296,19 +361,25 @@ export default class VariantAnnotationClinicalView extends LitElement {
halign: "center"
},
{
- title: "Origin Type",
- field: "alleleOrigin",
- rowspan: 2,
- colspan: 1,
- formatter: this.alleleOriginFormatter,
+ title: "Somatic Information",
+ rowspan: 1,
+ colspan: 4,
halign: "center"
},
{
- title: "Cancer",
- rowspan: 1,
- colspan: 3,
+ title: "Bibliography",
+ field: "bibliography",
+ rowspan: 2,
+ colspan: 1,
+ formatter: value => {
+ if (value?.length > 0) {
+ return value
+ .map(v => `
${v} `)
+ .join("
");
+ }
+ },
halign: "center"
- }
+ },
], [
{
title: "Tumour Site",
@@ -332,17 +403,116 @@ export default class VariantAnnotationClinicalView extends LitElement {
rowspan: 1,
colspan: 1,
halign: "center"
- }
+ },
+ {
+ title: "Mutation Zygosity",
+ field: "additionalProperties",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => {
+ const data = value?.find(item => item.name === "MUTATION_ZYGOSITY");
+ return data?.value || "-";
+ },
+ halign: "center"
+ },
]
]
});
+
+ $("#" + this._prefix + "HgmdTraitAssociation").bootstrapTable("destroy");
+ $("#" + this._prefix + "HgmdTraitAssociation").bootstrapTable({
+ data: this.groupedBySource.hgmd,
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ pagination: false,
+ columns: [
+ {
+ title: "ID",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.idFormatter,
+ halign: "center"
+ },
+ {
+ title: "Source",
+ field: "source",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.sourceFormatter,
+ halign: "center"
+ },
+ {
+ title: "Gene",
+ field: "genomicFeatures",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.geneFormatter,
+ halign: "center"
+ },
+ {
+ title: "Heritable Traits",
+ field: "heritableTraits",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.heritableTraitsFormatter,
+ halign: "center"
+ },
+ {
+ title: "Mode of Inheritance",
+ field: "heritableTraits",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.inheritanceModeFormatter,
+ halign: "center"
+ },
+ {
+ title: "Clinical Significance",
+ field: "variantClassification",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.clinicalSignificanceFormatter,
+ halign: "center"
+ },
+ {
+ title: "Origin Type",
+ field: "alleleOrigin",
+ rowspan: 1,
+ colspan: 1,
+ formatter: this.alleleOriginFormatter,
+ halign: "center"
+ },
+ ]
+ });
}
render() {
return html`
Variant Trait Association
-
+
ClinVar
+
+ ${this.groupedBySource?.clinvar?.length > 0 ? html`
+
+ ` : html`
+
No data available
+ `}
+
+
Cosmic
+
+ ${this.groupedBySource?.cosmic?.length > 0 ? html`
+
+ ` : html`
+
No data available
+ `}
+
+
HGMD
+
+ ${this.groupedBySource?.hgmd?.length > 0 ? html`
+
+ ` : html`
+
No data available
+ `}
+
`;
}
diff --git a/src/webcomponents/variant/annotation/variant-annotation-pharmacogenomics-view.js b/src/webcomponents/variant/annotation/variant-annotation-pharmacogenomics-view.js
new file mode 100644
index 0000000000..9ebbfe29d8
--- /dev/null
+++ b/src/webcomponents/variant/annotation/variant-annotation-pharmacogenomics-view.js
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2015-2016 OpenCB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {html, LitElement} from "lit";
+import UtilsNew from "../../../core/utils-new.js";
+import BioinfoUtils from "../../../core/bioinfo/bioinfo-utils.js";
+
+export default class VariantAnnotationPharmacogenomicsView extends LitElement {
+
+ constructor() {
+ super();
+
+ this.#init();
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ static get properties() {
+ return {
+ variantId: {
+ type: String
+ },
+ pharmacogenomics: {
+ type: Array
+ },
+ cellbaseClient: {
+ type: Object
+ },
+ assembly: {
+ type: String
+ },
+ opencgaSession: {
+ type: Object
+ },
+ };
+ }
+
+ #init() {
+ this._prefix = UtilsNew.randomString(8);
+
+ this.pharmacogenomics = [];
+ }
+
+ firstUpdated() {
+ this.renderVariantTraitTable();
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.has("variantId")) {
+ this.variantIdObserver();
+ }
+ if (changedProperties.has("pharmacogenomics")) {
+ this.renderVariantTraitTable();
+ }
+ }
+
+ variantIdObserver() {
+ if (this.cellbaseClient) {
+ if (this.variantId) {
+ this.cellbaseClient
+ .get("genomic", "variant", this.variantId, "annotation", {assembly: this.assembly || this.opencgaSession.project.organism.assembly}, {})
+ .then(restResponse => {
+ this.variantAnnotation = restResponse.getResult(0);
+ this.pharmacogenomics = this.variantAnnotation.pharmacogenomics || [];
+ });
+ }
+ }
+ }
+
+ geneFormatter(value) {
+ if (value) {
+ const genes = value
+ .filter(v => v.featureType === "gene")
+ .map(v => v.xrefs.symbol);
+ return genes?.length > 0 ? genes.join(", ") : "-";
+ }
+ return "-";
+ }
+
+ renderVariantTraitTable() {
+ if (!this.pharmacogenomics) {
+ this.pharmacogenomics = [];
+ }
+
+ $("#" + this._prefix + "Pharmacogenomics").bootstrapTable("destroy");
+ $("#" + this._prefix + "Pharmacogenomics").bootstrapTable({
+ data: this.pharmacogenomics,
+ theadClasses: "table-light",
+ buttonsClass: "light",
+ pagination: false,
+ columns: [
+ [
+ {
+ title: "ID",
+ field: "id",
+ rowspan: 2,
+ colspan: 1,
+ formatter: value => `
${value} `,
+ halign: "center"
+ },
+ {
+ title: "Name",
+ field: "name",
+ rowspan: 2,
+ colspan: 1,
+ halign: "center"
+ },
+ {
+ title: "Type",
+ field: "types",
+ rowspan: 2,
+ colspan: 1,
+ formatter: value => value.join(", "),
+ halign: "center"
+ },
+ // {
+ // title: "Source",
+ // field: "source",
+ // rowspan: 2,
+ // colspan: 1,
+ // halign: "center"
+ // },
+ {
+ title: "Annotation",
+ // field: "source",
+ rowspan: 1,
+ colspan: 8,
+ halign: "center"
+ },
+ ],
+ [
+ {
+ title: "Variant ID",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value.map(v => v.variantId).join("
"),
+ halign: "center"
+ },
+ {
+ title: "PharmGKB Annotation",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value.map(v => `
View `).join("
"),
+ halign: "center"
+ },
+ {
+ title: "Gene",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value.map(v => v.geneNames?.join(";")).join("
"),
+ halign: "center"
+ },
+ {
+ title: "Phenotypes",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value.map(v => v.phenotypes?.join(";
")).join("
"),
+ halign: "center"
+ },
+ {
+ title: "Score",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value.map(v => v.score).join("
"),
+ halign: "center"
+ },
+ {
+ title: "Confidence",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value.map(v => v.confidence).join("
"),
+ halign: "center"
+ },
+ {
+ title: "PubMed",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value
+ .map(v => v.pubmed?.map(p => `
${p} `).join("
"))
+ .join("
"),
+ halign: "center"
+ },
+ {
+ title: "Summary",
+ field: "annotations",
+ rowspan: 1,
+ colspan: 1,
+ formatter: value => value.map(v => v.summary).join("
"),
+ halign: "center"
+ },
+ ]
+ ]
+ });
+ }
+
+ render() {
+ return html`
+
Pharmacogenomics
+
+
PharmaGKB
+
+ ${this.pharmacogenomics?.length > 0 ? html`
+
+ ` : html`
No data available
`}
+
+
+ `;
+ }
+
+}
+
+customElements.define("variant-annotation-pharmacogenomics-view", VariantAnnotationPharmacogenomicsView);
diff --git a/src/webcomponents/variant/custom/steiner-report.js b/src/webcomponents/variant/custom/steiner-report.js
index 080ec79b5a..b341941eaa 100644
--- a/src/webcomponents/variant/custom/steiner-report.js
+++ b/src/webcomponents/variant/custom/steiner-report.js
@@ -58,9 +58,9 @@ class SteinerReport extends LitElement {
#init() {
this.gridTypes = {
- snv: "variantInterpreterCancerSNV",
- cnv: "variantInterpreterCancerCNV",
- rearrangements: "variantInterpreterRearrangement",
+ snv: "variant-interpreter-cancer-snv",
+ cnv: "variant-interpreter-cancer-cnv",
+ rearrangements: "variant-interpreter-rearrangement",
};
this.callersInfo = {
@@ -99,7 +99,7 @@ class SteinerReport extends LitElement {
this.clinicalAnalysisObserver();
}
- if (changedProperties.has("config")) {
+ if (changedProperties.has("opencgaSession") || changedProperties.has("config")) {
this._config = {
...this.getDefaultConfig(),
...this.config,
@@ -299,6 +299,19 @@ class SteinerReport extends LitElement {
}
}
+ getGroupedClinicalAnalsysisStatus() {
+ const statusList = this.opencgaSession?.study?.internal?.configuration?.clinical?.interpretation?.status || [];
+ const groupedStatus = UtilsNew.groupBy(statusList, "type");
+ return Object.keys(groupedStatus)
+ .filter(type => ["ACTIVE", "DONE", "CLOSED"].includes(type))
+ .map(type => {
+ return {
+ id: type,
+ fields: groupedStatus[type],
+ };
+ });
+ }
+
onSignatureChange(event, type) {
this.selectedSignatures[type] = event.detail.value;
this._config = {
@@ -1251,7 +1264,7 @@ class SteinerReport extends LitElement {
title: "Case Status",
field: "status",
type: "select",
- allowedValues: ["CLOSED", "REJECTED"],
+ allowedValues: this.getGroupedClinicalAnalsysisStatus(),
required: true,
},
{
diff --git a/src/webcomponents/variant/family-genotype-filter.js b/src/webcomponents/variant/family-genotype-filter.js
index 6ed53e703d..408dc7908d 100644
--- a/src/webcomponents/variant/family-genotype-filter.js
+++ b/src/webcomponents/variant/family-genotype-filter.js
@@ -297,7 +297,7 @@ export default class FamilyGenotypeFilter extends LitElement {
// Return the default genotype values according the role
defaultGenotype(sample) {
- return sample.id === this.clinicalAnalysis.proband.samples[0].id ? ["0/1", "1/1", "1/2"] : [...this._config.defaultGenotypes];
+ return sample.id === this.clinicalAnalysis.proband.samples[0].id ? ["0/1", "1/1", "1", "1/2"] : [...this._config.defaultGenotypes];
}
render() {
@@ -340,12 +340,13 @@ export default class FamilyGenotypeFilter extends LitElement {
${BioinfoUtils.getIdName(this.clinicalAnalysis.disorder.id, this.clinicalAnalysis.disorder.name)}
-
Genotypes
+
Genotypes
HOM_REF (0/0)
HET (0/1)
HOM_ALT (1/1)
+ HAPLOID (1)
BIALLELIC (1/2)
@@ -416,6 +417,14 @@ export default class FamilyGenotypeFilter extends LitElement {
?disabled="${this.mode !== "CUSTOM"}"
@change="${this.onSampleTableChange}">
+
+
+
file.format.toUpperCase() === "VCF");
}
- // 6.1. Read defaultFilter from study internal configuration
- if (this.opencgaSession.study.internal?.configuration?.clinical?.interpretation?.defaultFilter) {
- this.query = {
- ...this.query,
- ...this.opencgaSession.study.internal.configuration.clinical.interpretation.defaultFilter,
- };
- }
-
- // 6.2. Read defaultFilter from browser settings
+ // 6. Read defaultFilter from browser settings
if (this.settings?.menu?.defaultFilter) {
this.query = {
...this.query,
@@ -239,7 +231,7 @@ class VariantInterpreterBrowserCancer extends LitElement {
// 2. Add default initial query the active filter menu
_activeFilterFilters.unshift({
- id: "Default Initial Query",
+ id: "Default Filter",
active: false,
query: this.query,
});
@@ -292,13 +284,13 @@ class VariantInterpreterBrowserCancer extends LitElement {
getDefaultConfig() {
const lockedFields = [
{id: "sample"},
- {id: "sampleData"},
- {id: "file"},
- {id: "fileData"},
+ // {id: "sampleData"},
+ // {id: "file"},
+ // {id: "fileData"},
];
// Add panels to locked fields
- if (this.clinicalAnalysis?.panels?.length > 0 && this.clinicalAnalysis.panelLock) {
+ if (this.clinicalAnalysis?.panels?.length > 0 && this.clinicalAnalysis.panelLocked) {
lockedFields.push({id: "panel"});
lockedFields.push({id: "panelIntersection"});
}
@@ -334,22 +326,6 @@ class VariantInterpreterBrowserCancer extends LitElement {
{
id: "sample-genotype",
title: "Sample Genotype",
- params: {
- genotypes: [
- {
- id: "0/1", name: "HET"
- },
- {
- id: "1/1", name: "HOM ALT"
- },
- {
- separator: true
- },
- {
- id: "NA", name: "NA"
- }
- ]
- },
tooltip: tooltips.sample,
},
{
@@ -395,7 +371,7 @@ class VariantInterpreterBrowserCancer extends LitElement {
id: "region",
title: "Genomic Location",
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Regions will be intersected with selected panels.",
},
tooltip: tooltips.region,
@@ -404,7 +380,7 @@ class VariantInterpreterBrowserCancer extends LitElement {
id: "feature",
title: "Feature IDs (gene, SNPs, ...)",
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Feature regions will be intersected with selected panels.",
},
tooltip: tooltips.feature,
@@ -425,6 +401,33 @@ class VariantInterpreterBrowserCancer extends LitElement {
}
]
},
+ {
+ title: "Consequence Type",
+ collapsed: true,
+ filters: [
+ {
+ id: "consequence-type",
+ title: "Select SO terms",
+ tooltip: tooltips.consequenceTypeSelect
+ }
+ ]
+ },
+ {
+ title: "Population Frequency",
+ collapsed: true,
+ filters: [
+ {
+ id: "populationFrequency",
+ title: "Select Population Frequency",
+ tooltip: tooltips.populationFrequencies,
+ params: {
+ showSetAll: false,
+ populationFrequencyIndexConfiguration: this.opencgaSession?.study?.internal?.configuration
+ ?.variantEngine?.sampleIndex?.annotationIndexConfiguration?.populationFrequency,
+ },
+ }
+ ]
+ },
{
title: "Clinical",
collapsed: true,
@@ -432,9 +435,9 @@ class VariantInterpreterBrowserCancer extends LitElement {
{
id: "diseasePanels",
title: "Disease Panels",
- disabled: () => this.clinicalAnalysis.panelLock,
+ disabled: () => this.clinicalAnalysis.panelLocked,
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Case Panel is locked, you are not allowed to change selected panel(s)."
},
tooltip: tooltips.diseasePanels
@@ -459,33 +462,6 @@ class VariantInterpreterBrowserCancer extends LitElement {
},
]
},
- {
- title: "Consequence Type",
- collapsed: true,
- filters: [
- {
- id: "consequence-type",
- title: "Select SO terms",
- tooltip: tooltips.consequenceTypeSelect
- }
- ]
- },
- {
- title: "Population Frequency",
- collapsed: true,
- filters: [
- {
- id: "populationFrequency",
- title: "Select Population Frequency",
- tooltip: tooltips.populationFrequencies,
- params: {
- showSetAll: false,
- populationFrequencyIndexConfiguration: this.opencgaSession?.study?.internal?.configuration
- ?.variantEngine?.sampleIndex?.annotationIndexConfiguration?.populationFrequency,
- },
- }
- ]
- },
{
title: "Phenotype",
collapsed: true,
diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js
index 8676af4af0..f822ea1f99 100644
--- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js
+++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js
@@ -139,7 +139,7 @@ class VariantInterpreterBrowserCNV extends LitElement {
}
// 4. panelIntersection param: if panel lock is enabled, this param should be also enabled
- if (this.clinicalAnalysis.panelLock) {
+ if (this.clinicalAnalysis.panelLocked) {
this.query.panelIntersection = true;
}
@@ -208,7 +208,7 @@ class VariantInterpreterBrowserCNV extends LitElement {
// 2. Add default initial query the active filter menu
_activeFilterFilters.unshift({
- id: "Default Initial Query",
+ id: "Default Filter",
active: false,
query: this.query,
});
@@ -251,14 +251,14 @@ class VariantInterpreterBrowserCNV extends LitElement {
getDefaultConfig() {
const lockedFields = [
{id: "sample"},
- {id: "sampleData"},
- {id: "file"},
- {id: "fileData"},
+ // {id: "sampleData"},
+ // {id: "file"},
+ // {id: "fileData"},
{id: "type"},
];
// Add panels to locked fields
- if (this.clinicalAnalysis?.panels?.length > 0 && this.clinicalAnalysis.panelLock) {
+ if (this.clinicalAnalysis?.panels?.length > 0 && this.clinicalAnalysis.panelLocked) {
lockedFields.push({id: "panel"});
lockedFields.push({id: "panelIntersection"});
}
@@ -293,22 +293,6 @@ class VariantInterpreterBrowserCNV extends LitElement {
{
id: "sample-genotype",
title: "Sample Genotype",
- params: {
- genotypes: [
- {
- id: "0/1", name: "HET"
- },
- {
- id: "1/1", name: "HOM ALT"
- },
- {
- separator: true
- },
- {
- id: "NA", name: "NA"
- }
- ]
- },
tooltip: tooltips.sample,
},
{
@@ -345,7 +329,7 @@ class VariantInterpreterBrowserCNV extends LitElement {
id: "region",
title: "Genomic Location",
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Regions will be intersected with selected panels.",
},
tooltip: tooltips.region,
@@ -354,7 +338,7 @@ class VariantInterpreterBrowserCNV extends LitElement {
id: "feature",
title: "Feature IDs (gene, ...)",
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Feature regions will be intersected with selected panels.",
},
tooltip: tooltips.feature,
@@ -383,9 +367,9 @@ class VariantInterpreterBrowserCNV extends LitElement {
{
id: "diseasePanels",
title: "Disease Panels",
- disabled: () => this.clinicalAnalysis.panelLock,
+ disabled: () => this.clinicalAnalysis.panelLocked,
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Case Panel is locked, you are not allowed to change selected panel(s)."
},
tooltip: tooltips.diseasePanels
diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js
index 061715de89..e08dd3ff1e 100644
--- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js
+++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-import {LitElement, html} from "lit";
+import {html, LitElement} from "lit";
import UtilsNew from "../../../core/utils-new.js";
import "./variant-interpreter-browser-template.js";
import "./exomiser/variant-interpreter-exomiser-view.js";
import "../variant-samples.js";
import "../../visualization/protein-lollipop-variant-view.js";
+import "../annotation/variant-annotation-pharmacogenomics-view.js";
class VariantInterpreterBrowserRd extends LitElement {
@@ -135,15 +136,17 @@ class VariantInterpreterBrowserRd extends LitElement {
switch (this.clinicalAnalysis.type.toUpperCase()) {
case "SINGLE":
case "CANCER":
- this._sampleQuery = this.sample.id + ":" + ["0/1", "1/1", "1/2"].join(",");
+ // Nacho (29-7-24): we have found too many exceptions in the genotypes, we should NOT add the genotypes here
+ // this._sampleQuery = this.sample.id + ":" + ["0/1", "1/1", "1", "1/2"].join(",");
+ this._sampleQuery = this.sample.id;
break;
case "FAMILY":
// Add proband genotypes
- const sampleIds = [this.sample.id + ":" + ["0/1", "1/1", "1/2"].join(",")];
+ const sampleIds = [this.sample.id + ":" + ["0/1", "1/1", "1", "1/2"].join(",")];
for (const member of this.clinicalAnalysis.family?.members) {
// Proband is already in the array in the first position, we add other family members
if (member.id !== this.clinicalAnalysis.proband?.id && member.samples?.length > 0) {
- sampleIds.push(member.samples[0].id + ":" + ["0/0", "0/1", "1/1", "1/2"].join(","));
+ sampleIds.push(member.samples[0].id + ":" + ["0/0", "0/1", "1/1", "1", "1/2"].join(","));
}
}
this._sampleQuery = sampleIds.join(";");
@@ -166,7 +169,7 @@ class VariantInterpreterBrowserRd extends LitElement {
}
// 3. panelIntersection param: if panel lock is enabled, this param should be also enabled
- if (this.clinicalAnalysis.panelLock) {
+ if (this.clinicalAnalysis.panelLocked) {
this.query.panelIntersection = true;
}
@@ -217,15 +220,7 @@ class VariantInterpreterBrowserRd extends LitElement {
this.files = this.clinicalAnalysis.files?.filter(file => file.format.toUpperCase() === "VCF") || [];
}
- // 5.1. Read defaultFilter from study internal configuration
- if (this.opencgaSession.study.internal?.configuration?.clinical?.interpretation?.defaultFilter) {
- this.query = {
- ...this.query,
- ...this.opencgaSession.study.internal.configuration.clinical.interpretation.defaultFilter,
- };
- }
-
- // 5.2. Read defaultFilter from browser settings
+ // 5. Read defaultFilter from browser settings
if (this.settings?.menu?.defaultFilter) {
this.query = {
...this.query,
@@ -254,7 +249,7 @@ class VariantInterpreterBrowserRd extends LitElement {
// 2. Add default initial query the active filter menu
_activeFilterFilters.unshift({
- id: "Default Initial Query",
+ id: "Default Filter",
active: false,
query: this.query,
});
@@ -307,13 +302,13 @@ class VariantInterpreterBrowserRd extends LitElement {
getDefaultConfig() {
const lockedFields = [
{id: "sample"},
- {id: "sampleData"},
- {id: "file"},
- {id: "fileData"},
+ // {id: "sampleData"},
+ // {id: "file"},
+ // {id: "fileData"},
];
// Add panels to locked fields
- if (this.clinicalAnalysis?.panels?.length > 0 && this.clinicalAnalysis.panelLock) {
+ if (this.clinicalAnalysis?.panels?.length > 0 && this.clinicalAnalysis.panelLocked) {
lockedFields.push({id: "panel"});
lockedFields.push({id: "panelIntersection"});
}
@@ -347,26 +342,29 @@ class VariantInterpreterBrowserRd extends LitElement {
{
id: "sample-genotype",
title: "Sample Genotype",
- visible: () => this.clinicalAnalysis.type.toUpperCase() === "SINGLE",
- params: {
- genotypes: [
- {
- id: "0/1", name: "HET (0/1)"
- },
- {
- id: "1/1", name: "HOM_ALT (1/1)"
- },
- {
- separator: true
- },
- {
- id: "1/2", name: "BIALLELIC (1/2)"
- },
- // {
- // id: "1", name: "HEMI"
- // }
- ]
- },
+ visible: () => this.clinicalAnalysis.type.toUpperCase() === "SINGLE" || this.clinicalAnalysis.type.toUpperCase() === "CANCER",
+ // params: {
+ // genotypes: [
+ // {
+ // id: "0/1", name: "HET (0/1)"
+ // },
+ // {
+ // id: "1/1", name: "HOM_ALT (1/1)"
+ // },
+ // {
+ // separator: true
+ // },
+ // {
+ // id: "1", name: "HAPLOID (1)"
+ // },
+ // {
+ // id: "1/2", name: "BIALLELIC (1/2)"
+ // },
+ // {
+ // id: "NA", name: "NA"
+ // }
+ // ]
+ // },
tooltip: tooltips.sample,
},
{
@@ -389,6 +387,15 @@ class VariantInterpreterBrowserRd extends LitElement {
individual: this.clinicalAnalysis?.proband
}
},
+ {
+ id: "variant-file",
+ title: "VCF File Filter",
+ visible: () => this.files?.length > 1,
+ params: {
+ files: this.files,
+ },
+ tooltip: tooltips.vcfFile,
+ },
{
id: "variant-file-sample-filter",
title: "Variant Caller Sample Filters",
@@ -422,7 +429,7 @@ class VariantInterpreterBrowserRd extends LitElement {
id: "region",
title: "Genomic Location",
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Regions will be intersected with selected panels.",
},
tooltip: tooltips.region,
@@ -431,7 +438,7 @@ class VariantInterpreterBrowserRd extends LitElement {
id: "feature",
title: "Feature IDs (gene, SNPs, ...)",
message: {
- visible: () => this.clinicalAnalysis.panelLock,
+ visible: () => this.clinicalAnalysis.panelLocked,
text: "Feature regions will be intersected with selected panels.",
},
tooltip: tooltips.feature,
@@ -452,27 +459,6 @@ class VariantInterpreterBrowserRd extends LitElement {
}
]
},
- {
- title: "Clinical",
- collapsed: true,
- filters: [
- {
- id: "diseasePanels",
- title: "Disease Panels",
- disabled: () => this.clinicalAnalysis.panelLock,
- message: {
- visible: () => this.clinicalAnalysis.panelLock,
- text: "Case Panel is locked, you are not allowed to change selected panel(s)."
- },
- tooltip: tooltips.diseasePanels
- },
- {
- id: "clinical-annotation",
- title: "Clinical Annotation",
- tooltip: tooltips.clinical
- }
- ]
- },
{
title: "Consequence Type",
collapsed: true,
@@ -500,6 +486,27 @@ class VariantInterpreterBrowserRd extends LitElement {
}
]
},
+ {
+ title: "Clinical",
+ collapsed: true,
+ filters: [
+ {
+ id: "diseasePanels",
+ title: "Disease Panels",
+ disabled: () => this.clinicalAnalysis.panelLock,
+ message: {
+ visible: () => this.clinicalAnalysis.panelLock,
+ text: "Case Panel is locked, you are not allowed to change selected panel(s)."
+ },
+ tooltip: tooltips.diseasePanels
+ },
+ {
+ id: "clinical-annotation",
+ title: "Clinical Annotation",
+ tooltip: tooltips.clinical
+ }
+ ]
+ },
{
title: "Phenotype",
collapsed: true,
@@ -633,6 +640,15 @@ class VariantInterpreterBrowserRd extends LitElement {
`,
},
+ {
+ id: "annotationPharmacogenomics",
+ name: "Pharmacogenomics",
+ render: variant => html`
+
+
+ `,
+ },
{
id: "fileMetrics",
name: "File Metrics",
diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js
index 22ee2726de..29e6f1686b 100644
--- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js
+++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js
@@ -139,7 +139,7 @@ class VariantInterpreterBrowserRearrangement extends LitElement {
}
// 3. panelIntersection param: if panel lock is enabled, this param should be also enabled
- if (this.clinicalAnalysis.panelLock) {
+ if (this.clinicalAnalysis.panelLocked) {
this.query.panelIntersection = true;
}
@@ -219,7 +219,7 @@ class VariantInterpreterBrowserRearrangement extends LitElement {
// 2. Add default initial query the active filter menu
_activeFilterFilters.unshift({
- id: "Default Initial Query",
+ id: "Default Filter",
active: false,
query: this.query,
});
@@ -291,9 +291,9 @@ class VariantInterpreterBrowserRearrangement extends LitElement {
hiddenFields: [],
lockedFields: [
{id: "sample"},
- {id: "sampleData"},
- {id: "file"},
- {id: "fileData"},
+ // {id: "sampleData"},
+ // {id: "file"},
+ // {id: "fileData"},
],
},
callers: [],
diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js
index b118a7f3e2..9e21921976 100644
--- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js
+++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js
@@ -116,6 +116,7 @@ class VariantInterpreterBrowserTemplate extends LitElement {
if (this.currentQueryBeforeSaveEvent) {
this.query = {...this.currentQueryBeforeSaveEvent};
this.currentQueryBeforeEvent = null;
+ this.variant = null;
}
}
@@ -124,6 +125,7 @@ class VariantInterpreterBrowserTemplate extends LitElement {
this.preparedQuery = {study: this.opencgaSession.study.fqn, ...this.query};
this.executedQuery = {study: this.opencgaSession.study.fqn, ...this.query};
this.searchActive = false;
+ this.variant = null;
}
}
@@ -284,6 +286,7 @@ class VariantInterpreterBrowserTemplate extends LitElement {
this.preparedQuery = e.detail.query;
this.executedQuery = e.detail.query;
this.query = {...e.detail.query}; // We need to update the internal query to propagate to filters
+ this.variant = null;
this.notifyQueryChange();
this.requestUpdate();
}
@@ -291,6 +294,7 @@ class VariantInterpreterBrowserTemplate extends LitElement {
onActiveFilterChange(e) {
VariantUtils.validateQuery(e.detail);
this.query = {...e.detail};
+ this.variant = null;
this.notifyQueryChange();
this.requestUpdate();
}
@@ -318,7 +322,7 @@ class VariantInterpreterBrowserTemplate extends LitElement {
});
// Check if panelLock is enabled
- if (this.clinicalAnalysis.panelLock) {
+ if (this.clinicalAnalysis.panelLocked) {
_query.panel = this.query.panel;
_query.panelIntersection = true;
}
@@ -383,7 +387,8 @@ class VariantInterpreterBrowserTemplate extends LitElement {
this.onChangeView(id)}">
${title}
- `;
+
+ `;
}
render() {
@@ -412,8 +417,8 @@ class VariantInterpreterBrowserTemplate extends LitElement {
-
- ${this._config.filter?.searchButtonText || "Search"}
+
+ ${this._config.filter?.searchButtonText || "Search"}
-
+
${this.renderViewButton("table", "Table Result", "table")}
${!this.settings?.hideGenomeBrowser ? this.renderViewButton("genome-browser", "Genome Browser", "dna") : nothing}
@@ -458,7 +463,7 @@ class VariantInterpreterBrowserTemplate extends LitElement {
.clinicalAnalysis="${this.clinicalAnalysis}"
.state="${this.clinicalAnalysisManager.state}"
.variantInclusionState="${this.variantInclusionState}"
- .write="${OpencgaCatalogUtils.checkPermissions(this.opencgaSession.study, this.opencgaSession.user.id, "WRITE_CLINICAL_ANALYSIS")}"
+ .write="${OpencgaCatalogUtils.getStudyEffectivePermission(this.opencgaSession.study, this.opencgaSession.user.id, "WRITE_CLINICAL_ANALYSIS", this.opencgaSession.organization?.configuration?.optimizations?.simplifyPermissions)}"
@filterVariants="${this.onFilterVariants}"
@resetVariants="${this.onResetVariants}"
@saveInterpretation="${this.onSaveVariants}">
@@ -495,14 +500,16 @@ class VariantInterpreterBrowserTemplate extends LitElement {
`
}
-
-
+ ${this.variant ? html`
+
+
+ ` : nothing}
${!this.settings?.hideGenomeBrowser ? html`
@@ -557,6 +564,7 @@ class VariantInterpreterBrowserTemplate extends LitElement {
},
{
type: "opencga-variant",
+ visible: ["SINGLE", "FAMILY"].includes(this.clinicalAnalysis?.type),
config: {
title: "Variants",
query: {
@@ -564,6 +572,44 @@ class VariantInterpreterBrowserTemplate extends LitElement {
},
},
},
+ {
+ type: "opencga-variant",
+ visible: this.clinicalAnalysis?.type === "CANCER",
+ config: {
+ title: "Small Variants",
+ query: {
+ sample: this.clinicalAnalysis.proband.samples.map(s => s.id).join(","),
+ type: "SNV,INDEL",
+ },
+ headerHeight: 0,
+ },
+ },
+ {
+ type: "opencga-variant",
+ visible: this.clinicalAnalysis?.type === "CANCER",
+ config: {
+ title: "Copy Number Variants",
+ query: {
+ sample: this.clinicalAnalysis.proband.samples.map(s => s.id).join(","),
+ type: "COPY_NUMBER",
+ },
+ lollipopVisible: false,
+ highlightVisible: false,
+ headerHeight: 0,
+ },
+ },
+ {
+ type: "opencga-variant",
+ visible: this.clinicalAnalysis?.type === "CANCER",
+ config: {
+ title: "Structural Variants",
+ query: {
+ sample: this.clinicalAnalysis.proband.samples.map(s => s.id).join(","),
+ type: "BREAKEND,INSERTION,DELETION,DUPLICATION",
+ },
+ headerHeight: 0,
+ },
+ },
...(this.clinicalAnalysis.proband?.samples || []).map(sample => ({
type: "opencga-alignment",
config: {
diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid-config.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid-config.js
index c2c2809313..560bd64851 100644
--- a/src/webcomponents/variant/interpretation/variant-interpreter-grid-config.js
+++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid-config.js
@@ -207,8 +207,13 @@ export default class VariantInterpreterGridConfig extends LitElement {
type: "pills",
validation: {
validate: data => {
- return data.geneSet?.ensembl || data.geneSet?.refseq;
- }
+ // make sure that geneSet is present in the data object
+ if (typeof data.geneSet !== "undefined") {
+ return data.geneSet?.ensembl || data.geneSet?.refseq;
+ }
+ return true;
+ },
+ message: "You must select at least one Gene Set (Ensembl or RefSeq) in Transcript Filter.",
},
display: {
width: 12,
diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js
index 7c2b76c068..3737bf52b7 100644
--- a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js
+++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js
@@ -311,16 +311,16 @@ export default class VariantInterpreterGridFormatter {
` : `
${panel.id}
`
- }
+ }
${gene.modesOfInheritance ? `
${gene.modesOfInheritance.join(", ")}
` : ""
- }
+ }
${gene.confidence ? `
${gene.confidence}
` : ""
- }
+ }
`;
} else {
panelHtml = re.panelId;
@@ -624,8 +624,12 @@ export default class VariantInterpreterGridFormatter {
allelesHtml.push(`${allelesSeq[i]} `);
}
- const bar = genotype.includes("/") ? "/" : "|";
- res = `${allelesHtml[0]} ${bar} ${allelesHtml[1]} `;
+ if (allelesHtml.length === 1) {
+ res = `${allelesHtml[0]} `;
+ } else {
+ const bar = genotype.includes("/") ? "/" : "|";
+ res = `${allelesHtml[0]} ${bar} ${allelesHtml[1]} `;
+ }
}
return res;
}
@@ -634,12 +638,12 @@ export default class VariantInterpreterGridFormatter {
let res = "-";
if (variant?.studies?.length > 0 && sampleEntry?.data.length > 0) {
let sex;
- if (ca.type === "FAMILY") {
+ if (ca?.type === "FAMILY") {
// we need to find the sex of each member of the family
const individual = ca.family.members.find(m => m.samples[0].id === sampleEntry.sampleId);
sex = UtilsNew.isEmpty(individual?.sex) ? "Not specified" : individual.sex?.id || individual.sex;
} else {
- sex = ca?.proband?.sex !== "UNKOWN" ? ca.proband.sex : "";
+ sex = (!!ca?.proband?.sex && ca?.proband?.sex !== "UNKNOWN") ? ca.proband.sex : "";
}
const genotype = sampleEntry.data[0];
diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js
index 6da45893d3..a194c750ca 100644
--- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js
+++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js
@@ -664,14 +664,19 @@ export default class VariantInterpreterGrid extends LitElement {
},
{
id: "deleteriousness",
- title: `Deleteriousness
- `,
+ title: `Deleteriousness
+ `,
field: "deleteriousness",
rowspan: 1,
- colspan: 2,
+ colspan: 5,
align: "center"
},
{
@@ -734,7 +739,7 @@ export default class VariantInterpreterGrid extends LitElement {
"
tooltip-position-at="left bottom" tooltip-position-my="right top"> `,
rowspan: 1,
- colspan: 4,
+ colspan: 6,
align: "center"
},
{
@@ -800,8 +805,8 @@ export default class VariantInterpreterGrid extends LitElement {